Skip to content

Commit

Permalink
Merge pull request #1399 from jeffwcx/feature/iframe-srcdoc
Browse files Browse the repository at this point in the history
feat: [#1398] Add support for iframe srcdoc
  • Loading branch information
capricorn86 committed May 6, 2024
2 parents c29f36c + d48fd27 commit a289408
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 6 deletions.
Expand Up @@ -93,6 +93,8 @@ export default class BrowserFrameNavigator {
const width = frame.window.innerWidth;
const height = frame.window.innerHeight;
const devicePixelRatio = frame.window.devicePixelRatio;
const parentWindow = frame.window.parent !== frame.window ? frame.window.parent : null;
const topWindow = frame.window.top !== frame.window ? frame.window.top : null;

for (const childFrame of frame.childFrames) {
BrowserFrameFactory.destroyFrame(childFrame);
Expand All @@ -104,6 +106,8 @@ export default class BrowserFrameNavigator {
frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager();

(<BrowserWindow>frame.window) = new windowClass(frame, { url: targetURL.href, width, height });
(<BrowserWindow>frame.window.parent) = parentWindow;
(<BrowserWindow>frame.window.top) = topWindow;
(<number>frame.window.devicePixelRatio) = devicePixelRatio;

if (referrer) {
Expand Down
Expand Up @@ -46,8 +46,14 @@ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeM
public override setNamedItem(item: Attr): Attr | null {
const replacedAttribute = super.setNamedItem(item);

if (item[PropertySymbol.name] === 'srcdoc') {
this.#pageLoader.loadPage();
}

// If the src attribute and the srcdoc attribute are both specified together, the srcdoc attribute takes priority.
if (
item[PropertySymbol.name] === 'src' &&
this[PropertySymbol.ownerElement][PropertySymbol.attributes]['srcdoc']?.value === undefined &&
item[PropertySymbol.value] &&
item[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value]
) {
Expand All @@ -70,6 +76,21 @@ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeM
return replacedAttribute || null;
}

/**
* @override
*/
public override [PropertySymbol.removeNamedItem](name: string): Attr | null {
const removedItem = super[PropertySymbol.removeNamedItem](name);
if (
removedItem &&
(removedItem[PropertySymbol.name] === 'srcdoc' || removedItem[PropertySymbol.name] === 'src')
) {
this.#pageLoader.loadPage();
}

return removedItem;
}

/**
*
* @param tokens
Expand Down
Expand Up @@ -19,6 +19,7 @@ export default class HTMLIFrameElementPageLoader {
#contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null };
#browserParentFrame: IBrowserFrame;
#browserIFrame: IBrowserFrame;
#srcdoc: string | null = null;

/**
* Constructor.
Expand All @@ -44,15 +45,43 @@ export default class HTMLIFrameElementPageLoader {
*/
public loadPage(): void {
if (!this.#element[PropertySymbol.isConnected]) {
if (this.#browserIFrame) {
BrowserFrameFactory.destroyFrame(this.#browserIFrame);
this.#browserIFrame = null;
}
this.#contentWindowContainer.window = null;
this.unloadPage();
return;
}

const srcdoc = this.#element.getAttribute('srcdoc');
const window = this.#element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow];

if (srcdoc !== null) {
if (this.#srcdoc === srcdoc) {
return;
}

this.unloadPage();

this.#browserIFrame = BrowserFrameFactory.createChildFrame(this.#browserParentFrame);
this.#browserIFrame.url = 'about:srcdoc';

this.#contentWindowContainer.window = this.#browserIFrame.window;

(<BrowserWindow>this.#browserIFrame.window.top) = this.#browserParentFrame.window.top;
(<BrowserWindow>this.#browserIFrame.window.parent) = this.#browserParentFrame.window;

this.#browserIFrame.window.document.open();
this.#browserIFrame.window.document.write(srcdoc);

this.#srcdoc = srcdoc;

this.#element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].requestAnimationFrame(
() => this.#element.dispatchEvent(new Event('load'))
);
return;
}

if (this.#srcdoc !== null) {
this.unloadPage();
}

const originURL = this.#browserParentFrame.window.location;
const targetURL = BrowserFrameURL.getRelativeURL(this.#browserParentFrame, this.#element.src);

Expand Down Expand Up @@ -105,5 +134,6 @@ export default class HTMLIFrameElementPageLoader {
this.#browserIFrame = null;
}
this.#contentWindowContainer.window = null;
this.#srcdoc = null;
}
}
Expand Up @@ -35,7 +35,7 @@ describe('HTMLIFrameElement', () => {
});
});

for (const property of ['src', 'allow', 'height', 'width', 'name', 'srcdoc']) {
for (const property of ['src', 'allow', 'height', 'width', 'name']) {
describe(`get ${property}()`, () => {
it(`Returns the "${property}" attribute.`, () => {
element.setAttribute(property, 'value');
Expand Down Expand Up @@ -126,6 +126,105 @@ describe('HTMLIFrameElement', () => {
});
});

describe('get srcdoc()', () => {
it('Returns string', () => {
expect(element.srcdoc).toBe('');
element.srcdoc = '<div></div>';
expect(element.getAttribute('srcdoc')).toBe('<div></div>');
});
});

describe('set srcdoc()', () => {
it("Navigate the element's browsing context to a resource whose Content-Type is text/html", async () => {
const actualHTML = await new Promise((resolve) => {
element.srcdoc = '<div>TEST</div>';
element.addEventListener('load', () => {
resolve(element.contentDocument?.documentElement.innerHTML);
});
document.body.appendChild(element);
});
expect(actualHTML).toBe('<head></head><body><div>TEST</div></body>');
});

it('Takes priority, when the src attribute and the srcdoc attribute are both specified together', async () => {
const browser = new Browser();
const page = browser.newPage();
const window = page.mainFrame.window;
const document = window.document;
const element = <HTMLIFrameElement>document.createElement('iframe');
const url = await new Promise((resolve) => {
element.srcdoc = '<html><head></head><body>TEST</body></html>';
element.src = 'https://localhost:8080/iframe.html';
element.addEventListener('load', () => {
resolve(page.mainFrame.childFrames[0].url);
});
document.appendChild(element);
});
expect(url).toBe('about:srcdoc');
});

it('Resolve the value of the src attribute when the srcdoc attribute has been removed', async () => {
const browser = new Browser();
const page = browser.newPage();
const window = page.mainFrame.window;
const document = window.document;
const element = <HTMLIFrameElement>document.createElement('iframe');
page.mainFrame.url = 'https://localhost:8080';
const responseHTML = '<html><head></head><body>Test</body></html>';

vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => {
return Promise.resolve(<Response>(<unknown>{
text: () => Promise.resolve(responseHTML),
ok: true,
headers: new Headers()
}));
});
const frameUrl = 'https://localhost:8080/iframe.html';
const actualFrameUrl = await new Promise((resolve) => {
element.srcdoc = responseHTML;
element.src = frameUrl;
const firstLoad = (): void => {
expect(page.mainFrame.childFrames[0].url).toBe('about:srcdoc');
element.removeEventListener('load', firstLoad);
element.addEventListener('load', () => {
resolve(page.mainFrame.childFrames[0].url);
});
element.removeAttribute('srcdoc');
};
element.addEventListener('load', firstLoad);
document.body.appendChild(element);
});
expect(actualFrameUrl).toBe(frameUrl);
});

it('Execute code in the script', async () => {
const message = await new Promise((resolve) => {
element.srcdoc = `<!doctype html><html><head>
<script>
function handleMessage(e) {
parent.postMessage({ msg: 'loaded' }, '*');
}
window.addEventListener('message', handleMessage, false);
</script>
</head><body></body></html>`;
element.addEventListener('load', () => {
element.contentWindow?.postMessage('MESSAGE', '*');
});
window.addEventListener(
'message',
(e) => {
const data = (<MessageEvent>e).data;
resolve(data);
},
false
);
document.body.appendChild(element);
expect(element.contentWindow?.parent === window).toBe(true);
});
expect(message).toMatchObject({ msg: 'loaded' });
});
});

describe('get contentWindow()', () => {
it('Returns content window for "about:blank".', () => {
element.src = 'about:blank';
Expand Down Expand Up @@ -421,13 +520,44 @@ describe('HTMLIFrameElement', () => {
document.body.appendChild(element);
});
});

it('Remain at the initial about:blank page when none of the srcdoc/src attributes are set', async () => {
const browser = new Browser();
const page = browser.newPage();
const window = page.mainFrame.window;
const document = window.document;
const element = <HTMLIFrameElement>document.createElement('iframe');
page.mainFrame.url = 'https://localhost:8080';

vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => {
return Promise.resolve(<Response>(<unknown>{
text: () => Promise.resolve('<html><head></head><body>Test</body></html>'),
ok: true,
headers: new Headers()
}));
});
const actualFrameUrl = await new Promise((resolve) => {
element.src = 'https://localhost:8080/iframe.html';
const firstLoad = (): void => {
element.removeEventListener('load', firstLoad);
element.addEventListener('load', () => {
resolve(page.mainFrame.childFrames[0].url);
});
element.removeAttribute('src');
};
element.addEventListener('load', firstLoad);
document.body.appendChild(element);
});
expect(actualFrameUrl).toBe('about:blank');
});
});

describe('get contentDocument()', () => {
it('Returns content document for "about:blank".', () => {
element.src = 'about:blank';
expect(element.contentDocument).toBe(null);
document.body.appendChild(element);
expect(element.contentWindow?.parent === window).toBe(true);
expect(element.contentDocument?.documentElement.innerHTML).toBe('<head></head><body></body>');
});
});
Expand Down

0 comments on commit a289408

Please sign in to comment.