diff --git a/packages/dom/src/serialize-frames.js b/packages/dom/src/serialize-frames.js index d692b2a28..b34c3590b 100644 --- a/packages/dom/src/serialize-frames.js +++ b/packages/dom/src/serialize-frames.js @@ -1,5 +1,28 @@ import serializeDOM from './serialize-dom'; +let policy = null; + +export function resetPolicy() { + policy = null; +} + +function getPolicy() { + if (typeof window !== 'undefined' && window.trustedTypes && window.trustedTypes.createPolicy) { + if (policy && policy.createHTML) return policy; + + try { + policy = window.trustedTypes.createPolicy('percy-dom', { + // codeql[js/dom-text-reinterpreted-as-html] + createHTML: html => html + }); + } catch (e) { + // ignore + } + /* istanbul ignore next */ + return policy || {}; + } +} + // Adds a `` element to the serialized iframe's ``. This is necessary when // embedded documents are serialized and their contents become root-relative. function setBaseURI(dom) { @@ -46,7 +69,11 @@ export function serializeFrames({ dom, clone, warnings, resources, enableJavaScr for (let r of serialized.resources) resources.add(r); // assign serialized html to srcdoc and remove src - cloneEl.setAttribute('srcdoc', serialized.html); + let p = getPolicy() || {}; + try { + cloneEl.setAttribute('srcdoc', p.createHTML ? p.createHTML(serialized.html) : serialized.html); + } catch {} + cloneEl.removeAttribute('src'); // delete inaccessible frames built with js when js is disabled because they diff --git a/packages/dom/test/serialize-frames.test.js b/packages/dom/test/serialize-frames.test.js index 0f4e17ef6..0a9240649 100644 --- a/packages/dom/test/serialize-frames.test.js +++ b/packages/dom/test/serialize-frames.test.js @@ -1,6 +1,7 @@ import { when } from 'interactor.js'; import { assert, withExample, parseDOM, platforms, platformDOM, getTestBrowser, chromeBrowser, firefoxBrowser } from './helpers'; -import serializeDOM from '@percy/dom'; +import serializeDOM from '../src/serialize-dom'; +import { resetPolicy } from '../src/serialize-frames'; describe('serializeFrames', () => { let serialized, cache = { shadow: {}, plain: {} }; @@ -159,5 +160,33 @@ describe('serializeFrames', () => { it(`${platform}: removes inaccessible JS frames`, () => { expect($('#frame-inject')).toHaveSize(0); }); + + if (platform === 'plain') { + it('uses Trusted Types policy to create srcdoc when available', () => { + let createHTML = jasmine.createSpy('createHTML').and.callFake(html => html); + let createPolicy = jasmine.createSpy('createPolicy').and.returnValue({ createHTML }); + let trustedTypesDescriptor = Object.getOwnPropertyDescriptor(window, 'trustedTypes'); + + // Reset policy to ensure we don't use a cached version from a previous test/environment + resetPolicy(); + + Object.defineProperty(window, 'trustedTypes', { + value: { createPolicy }, + configurable: true + }); + + try { + serializeDOM(); + expect(createPolicy).toHaveBeenCalledWith('percy-dom', jasmine.objectContaining({ createHTML: jasmine.any(Function) })); + expect(createHTML).toHaveBeenCalled(); + } finally { + if (trustedTypesDescriptor) { + Object.defineProperty(window, 'trustedTypes', trustedTypesDescriptor); + } else { + delete window.trustedTypes; + } + } + }); + } }); });