diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js index 1249d8bc8..0bf3090b3 100644 --- a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js @@ -13,8 +13,10 @@ import { MESSAGE_IN_APP } from "../../Personalization/constants/schema"; import { TEXT_HTML } from "../../Personalization/constants/contentType"; export default (id, type, detail) => { + // TODO: add webParameters when available from the authoring UI in detail const { html, mobileParameters } = detail; + // TODO: Remove it once we have webParameters const webParameters = { info: "this is a placeholder" }; return { diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index 39fcacf9b..4582c1df6 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -11,73 +11,19 @@ governing permissions and limitations under the License. */ import { getNonce } from "../../dom-actions/dom"; -import { parseAnchor, removeElementById } from "../utils"; +import { createElement, parseAnchor, removeElementById } from "../utils"; import { TEXT_HTML } from "../../constants/contentType"; +import { assign } from "../../../../utils"; import { getEventType } from "../../constants/propositionEventType"; -const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; -const ELEMENT_TAG_ID = "alloy-messaging-container"; - -const OVERLAY_TAG_CLASSNAME = "alloy-overlay-container"; -const OVERLAY_TAG_ID = "alloy-overlay-container"; +const ALLOY_MESSAGING_CONTAINER_ID = "alloy-messaging-container"; +const ALLOY_OVERLAY_CONTAINER_ID = "alloy-overlay-container"; const ALLOY_IFRAME_ID = "alloy-content-iframe"; const dismissMessage = () => - [ELEMENT_TAG_ID, OVERLAY_TAG_ID].forEach(removeElementById); - -// eslint-disable-next-line no-unused-vars -export const buildStyleFromParameters = (mobileParameters, webParameters) => { - const { - verticalAlign, - width, - horizontalAlign, - backdropColor, - height, - cornerRadius, - horizontalInset, - verticalInset, - uiTakeover = false - } = mobileParameters; - - const style = { - width: width ? `${width}%` : "100%", - backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", - borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", - border: "none", - position: uiTakeover ? "fixed" : "relative", - overflow: "hidden" - }; - if (horizontalAlign === "left") { - style.left = horizontalInset ? `${horizontalInset}%` : "0"; - } else if (horizontalAlign === "right") { - style.right = horizontalInset ? `${horizontalInset}%` : "0"; - } else if (horizontalAlign === "center") { - style.left = "50%"; - style.transform = "translateX(-50%)"; - } - - if (verticalAlign === "top") { - style.top = verticalInset ? `${verticalInset}%` : "0"; - } else if (verticalAlign === "bottom") { - style.position = "fixed"; - style.bottom = verticalInset ? `${verticalInset}%` : "0"; - } else if (verticalAlign === "center") { - style.top = "50%"; - style.transform = `${ - horizontalAlign === "center" ? `${style.transform} ` : "" - }translateY(-50%)`; - style.display = "flex"; - style.alignItems = "center"; - style.justifyContent = "center"; - } - - if (height) { - style.height = `${height}vh`; - } else { - style.height = "100%"; - } - return style; -}; + [ALLOY_MESSAGING_CONTAINER_ID, ALLOY_OVERLAY_CONTAINER_ID].forEach( + removeElementById + ); const setWindowLocationHref = link => { window.location.assign(link); @@ -132,13 +78,6 @@ export const createIframe = (htmlContent, clickHandler) => { new Blob([htmlDocument.documentElement.outerHTML], { type: TEXT_HTML }) ); element.id = ALLOY_IFRAME_ID; - - Object.assign(element.style, { - border: "none", - width: "100%", - height: "100%" - }); - element.addEventListener("load", () => { const { addEventListener } = element.contentDocument || element.contentWindow.document; @@ -148,60 +87,203 @@ export const createIframe = (htmlContent, clickHandler) => { return element; }; -export const createContainerElement = settings => { - const { mobileParameters = {}, webParameters = {} } = settings; - const element = document.createElement("div"); - element.id = ELEMENT_TAG_ID; - element.className = `${ELEMENT_TAG_CLASSNAME}`; - Object.assign( - element.style, - buildStyleFromParameters(mobileParameters, webParameters) - ); - - return element; +const renderMessage = (iframe, webParameters, container, overlay) => { + [ + { id: ALLOY_OVERLAY_CONTAINER_ID, element: overlay }, + { id: ALLOY_MESSAGING_CONTAINER_ID, element: container }, + { id: ALLOY_IFRAME_ID, element: iframe } + ].forEach(({ id, element }) => { + const { style = {}, params = {} } = webParameters[id]; + + assign(element.style, style); + + const { + parentElement = "body", + insertionMethod = "appendChild", + enabled = true + } = params; + + const parent = document.querySelector(parentElement); + if (enabled && parent && typeof parent[insertionMethod] === "function") { + parent[insertionMethod](element); + } + }); }; -export const createOverlayElement = parameter => { - const element = document.createElement("div"); - const backdropOpacity = parameter.backdropOpacity || 0.5; - const backdropColor = parameter.backdropColor || "#FFFFFF"; - element.id = OVERLAY_TAG_ID; - element.className = `${OVERLAY_TAG_CLASSNAME}`; +export const buildStyleFromMobileParameters = mobileParameters => { + const { + verticalAlign, + width, + horizontalAlign, + backdropColor, + height, + cornerRadius, + horizontalInset, + verticalInset, + uiTakeover = false + } = mobileParameters; - Object.assign(element.style, { + const style = { + width: width ? `${width}%` : "100%", + backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", + borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", + border: "none", + position: uiTakeover ? "fixed" : "relative", + overflow: "hidden" + }; + if (horizontalAlign === "left") { + style.left = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "right") { + style.right = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "center") { + style.left = "50%"; + style.transform = "translateX(-50%)"; + } + + if (verticalAlign === "top") { + style.top = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "bottom") { + style.position = "fixed"; + style.bottom = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "center") { + style.top = "50%"; + style.transform = `${ + horizontalAlign === "center" ? `${style.transform} ` : "" + }translateY(-50%)`; + style.display = "flex"; + style.alignItems = "center"; + style.justifyContent = "center"; + } + + if (height) { + style.height = `${height}vh`; + } else { + style.height = "100%"; + } + return style; +}; + +export const mobileOverlay = mobileParameters => { + const { backdropOpacity, backdropColor } = mobileParameters; + const opacity = backdropOpacity || 0.5; + const color = backdropColor || "#FFFFFF"; + const style = { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", background: "transparent", - opacity: backdropOpacity, - backgroundColor: backdropColor - }); + opacity, + backgroundColor: color + }; + return style; +}; - return element; +const REQUIRED_PARAMS = ["enabled", "parentElement", "insertionMethod"]; + +const isValidWebParameters = webParameters => { + if (!webParameters) { + return false; + } + + const ids = Object.keys(webParameters); + + if (!ids.includes(ALLOY_MESSAGING_CONTAINER_ID)) { + return false; + } + + if (!ids.includes(ALLOY_OVERLAY_CONTAINER_ID)) { + return false; + } + + const values = Object.values(webParameters); + + for (let i = 0; i < values.length; i += 1) { + if (!Object.prototype.hasOwnProperty.call(values[i], "style")) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(values[i], "params")) { + return false; + } + + for (let j = 0; j < REQUIRED_PARAMS.length; j += 1) { + if ( + !Object.prototype.hasOwnProperty.call( + values[i].params, + REQUIRED_PARAMS[j] + ) + ) { + return false; + } + } + } + + return true; +}; + +const generateWebParameters = mobileParameters => { + if (!mobileParameters) { + return undefined; + } + + const { uiTakeover = false } = mobileParameters; + + return { + [ALLOY_IFRAME_ID]: { + style: { + border: "none", + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + }, + [ALLOY_MESSAGING_CONTAINER_ID]: { + style: buildStyleFromMobileParameters(mobileParameters), + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + [ALLOY_OVERLAY_CONTAINER_ID]: { + style: mobileOverlay(mobileParameters), + params: { + enabled: uiTakeover === true, + parentElement: "body", + insertionMethod: "appendChild" + } + } + }; }; export const displayHTMLContentInIframe = (settings, interact) => { dismissMessage(); const { content, contentType, mobileParameters } = settings; + let { webParameters } = settings; if (contentType !== TEXT_HTML) { return; } - const container = createContainerElement(settings); - + const container = createElement(ALLOY_MESSAGING_CONTAINER_ID); const iframe = createIframe(content, createIframeClickHandler(interact)); + const overlay = createElement(ALLOY_OVERLAY_CONTAINER_ID); - container.appendChild(iframe); + if (!isValidWebParameters(webParameters)) { + webParameters = generateWebParameters(mobileParameters); + } - if (mobileParameters.uiTakeover) { - const overlay = createOverlayElement(mobileParameters); - document.body.appendChild(overlay); - document.body.style.overflow = "hidden"; + if (!webParameters) { + return; } - document.body.appendChild(container); + + renderMessage(iframe, webParameters, container, overlay); }; export default (settings, collect) => { diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js index b8f82bc49..aa4f42567 100644 --- a/src/components/Personalization/in-app-message-actions/utils.js +++ b/src/components/Personalization/in-app-message-actions/utils.js @@ -74,3 +74,8 @@ export const parseAnchor = anchor => { uuid }; }; +export const createElement = elementTagId => { + const element = document.createElement("div"); + element.id = elementTagId; + return element; +}; diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js deleted file mode 100644 index 9b85d9f29..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2023 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import { createNode } from "../../../../../../../src/utils/dom"; -import { DIV } from "../../../../../../../src/constants/tagName"; -import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; -import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; - -describe("Personalization::IAM:banner", () => { - it("inserts banner into dom", async () => { - const something = createNode( - DIV, - { className: "something" }, - { - innerHTML: - "

Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

" - } - ); - - document.body.append(something); - - await displayIframeContent({ - mobileParameters: { - verticalAlign: "center", - dismissAnimation: "top", - verticalInset: 0, - backdropOpacity: 0.2, - cornerRadius: 15, - horizontalInset: 0, - uiTakeover: false, - horizontalAlign: "center", - width: 80, - displayAnimation: "top", - backdropColor: "#000000", - height: 60 - }, - content: `
banner
Alf Says`, - contentType: TEXT_HTML - }); - - const overlayContainer = document.querySelector( - "div#alloy-overlay-container" - ); - const messagingContainer = document.querySelector( - "div#alloy-messaging-container" - ); - - expect(overlayContainer).toBeNull(); - expect(messagingContainer).not.toBeNull(); - - expect(messagingContainer.parentNode).toEqual(document.body); - expect(messagingContainer.nextElementSibling).toBeNull(); - - const iframe = document.querySelector( - ".alloy-messaging-container > iframe" - ); - - expect(iframe).not.toBeNull(); - - await new Promise(resolve => { - iframe.addEventListener("load", () => { - resolve(); - }); - }); - - expect( - (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML - ).toContain("Alf Says"); - }); -}); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js index 3e836449c..e03314eaa 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js @@ -10,8 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import { - buildStyleFromParameters, - createOverlayElement, + buildStyleFromMobileParameters, createIframe, createIframeClickHandler, displayHTMLContentInIframe @@ -46,11 +45,7 @@ describe("DOM Actions on Iframe", () => { verticalInset: 10, uiTakeover: true }; - - const webParameters = {}; - - const style = buildStyleFromParameters(mobileParameters, webParameters); - + const style = buildStyleFromMobileParameters(mobileParameters); expect(style.width).toBe("80%"); expect(style.backgroundColor).toBe("rgba(0, 0, 0, 0.7)"); expect(style.borderRadius).toBe("10px"); @@ -62,26 +57,6 @@ describe("DOM Actions on Iframe", () => { }); }); - describe("createOverlayElement", () => { - it("should create overlay element with correct styles", () => { - const parameter = { - backdropOpacity: 0.8, - backdropColor: "#000000" - }; - - const overlayElement = createOverlayElement(parameter); - - expect(overlayElement.id).toBe("alloy-overlay-container"); - expect(overlayElement.style.position).toBe("fixed"); - expect(overlayElement.style.top).toBe("0px"); - expect(overlayElement.style.left).toBe("0px"); - expect(overlayElement.style.width).toBe("100%"); - expect(overlayElement.style.height).toBe("100%"); - expect(overlayElement.style.background).toBe("rgb(0, 0, 0)"); - expect(overlayElement.style.opacity).toBe("0.8"); - expect(overlayElement.style.backgroundColor).toBe("rgb(0, 0, 0)"); - }); - }); describe("createIframe function", () => { it("should create an iframe element with specified properties", () => { const mockHtmlContent = @@ -89,13 +64,9 @@ describe("DOM Actions on Iframe", () => { const mockClickHandler = jasmine.createSpy("clickHandler"); const iframe = createIframe(mockHtmlContent, mockClickHandler); - expect(iframe).toBeDefined(); expect(iframe instanceof HTMLIFrameElement).toBe(true); expect(iframe.src).toContain("blob:"); - expect(iframe.style.border).toBe("none"); - expect(iframe.style.width).toBe("100%"); - expect(iframe.style.height).toBe("100%"); }); it("should set 'nonce' attribute on script tag if it exists", async () => { @@ -285,9 +256,7 @@ describe("DOM Actions on Iframe", () => { let originalAppendChild; let originalBodyStyle; let mockCollect; - let originalCreateContainerElement; let originalCreateIframe; - let originalCreateOverlayElement; beforeEach(() => { mockCollect = jasmine.createSpy("collect"); @@ -295,17 +264,8 @@ describe("DOM Actions on Iframe", () => { document.body.appendChild = jasmine.createSpy("appendChild"); originalBodyStyle = document.body.style; document.body.style = {}; - - originalCreateContainerElement = window.createContainerElement; - window.createContainerElement = jasmine - .createSpy("createContainerElement") - .and.callFake(() => { - const element = document.createElement("div"); - element.id = "alloy-messaging-container"; - return element; - }); - originalCreateIframe = window.createIframe; + window.createIframe = jasmine .createSpy("createIframe") .and.callFake(() => { @@ -313,29 +273,19 @@ describe("DOM Actions on Iframe", () => { element.id = "alloy-content-iframe"; return element; }); - - originalCreateOverlayElement = window.createOverlayElement; - window.createOverlayElement = jasmine - .createSpy("createOverlayElement") - .and.callFake(() => { - const element = document.createElement("div"); - element.id = "alloy-overlay-container"; - return element; - }); }); afterEach(() => { document.body.appendChild = originalAppendChild; document.body.style = originalBodyStyle; document.body.innerHTML = ""; - window.createContainerElement = originalCreateContainerElement; - window.createOverlayElement = originalCreateOverlayElement; window.createIframe = originalCreateIframe; }); - it("should display HTML content in iframe with overlay", () => { + it("should display HTML content in iframe with overlay using mobile parameters", () => { const settings = { type: "custom", + webParameters: { info: "this is a placeholder" }, mobileParameters: { verticalAlign: "center", dismissAnimation: "bottom", @@ -351,9 +301,6 @@ describe("DOM Actions on Iframe", () => { backdropColor: "#4CA206", height: 63 }, - webParameters: { - info: "this is a placeholder" - }, content: '\n\n\n Bumper Sale!\n \n\n\n
\n \n

Black Friday Sale!

\n Technology Image\n

Don\'t miss out on our incredible discounts and deals at our gadgets!

\n
\n Shop\n Dismiss\n
\n
\n\n\n\n', contentType: TEXT_HTML, @@ -379,7 +326,133 @@ describe("DOM Actions on Iframe", () => { displayHTMLContentInIframe(settings, mockCollect); expect(document.body.appendChild).toHaveBeenCalledTimes(2); - expect(document.body.style.overflow).toBe("hidden"); + }); + + it("should display HTML content in iframe with overlay using web parameters", () => { + const settings = { + webParameters: { + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.5, + backgroundColor: "#FFFFFF" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-messaging-container": { + style: { + width: "72%", + backgroundColor: "orange", + borderRadius: "20px", + border: "none", + position: "fixed", + overflow: "hidden", + left: "50%", + transform: "translateX(-50%) translateY(-50%)", + top: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "63vh" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-content-iframe": { + style: { + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + } + }, + content: + '\n\n\n Bumper Sale!\n \n\n\n
\n \n

Black Friday Sale!

\n Technology Image\n

Don\'t miss out on our incredible discounts and deals at our gadgets!

\n
\n Shop\n Dismiss\n
\n
\n\n\n\n', + contentType: TEXT_HTML, + schema: "https://ns.adobe.com/personalization/message/in-app" + }; + + displayHTMLContentInIframe(settings, mockCollect); + expect(document.body.appendChild).toHaveBeenCalledTimes(2); + }); + it("should display HTML content in iframe with no overlay using web parameters", () => { + const settings = { + webParameters: { + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.5, + backgroundColor: "#FFFFFF" + }, + params: { + enabled: false, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-messaging-container": { + style: { + width: "72%", + backgroundColor: "orange", + borderRadius: "20px", + border: "none", + position: "fixed", + overflow: "hidden", + left: "50%", + transform: "translateX(-50%) translateY(-50%)", + top: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "63vh" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-content-iframe": { + style: { + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + } + }, + content: + '\n\n\n Bumper Sale!\n \n\n\n
\n \n

Black Friday Sale!

\n Technology Image\n

Don\'t miss out on our incredible discounts and deals at our gadgets!

\n
\n Shop\n Dismiss\n
\n
\n\n\n\n', + contentType: TEXT_HTML, + schema: "https://ns.adobe.com/personalization/message/in-app" + }; + + displayHTMLContentInIframe(settings, mockCollect); + expect(document.body.appendChild).toHaveBeenCalledTimes(1); }); }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js deleted file mode 100644 index 1edb52313..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2023 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import { createNode } from "../../../../../../../src/utils/dom"; -import { DIV } from "../../../../../../../src/constants/tagName"; -import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; -import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; - -describe("Personalization::IAM:modal", () => { - it("inserts modal into dom", async () => { - const something = createNode( - DIV, - { className: "something" }, - { - innerHTML: - "

Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

" - } - ); - - document.body.append(something); - - await displayIframeContent({ - mobileParameters: { - verticalAlign: "center", - dismissAnimation: "top", - verticalInset: 0, - backdropOpacity: 0.2, - cornerRadius: 15, - horizontalInset: 0, - uiTakeover: true, - horizontalAlign: "center", - width: 80, - displayAnimation: "top", - backdropColor: "#000000", - height: 60 - }, - content: `
modal
Alf Says`, - contentType: TEXT_HTML - }); - document.querySelector("div#alloy-overlay-container"); - const messagingContainer = document.querySelector( - "div#alloy-messaging-container" - ); - - expect(messagingContainer).not.toBeNull(); - - expect(messagingContainer.parentNode).toEqual(document.body); - expect(messagingContainer.nextElementSibling).toBeNull(); - - const iframe = document.querySelector( - ".alloy-messaging-container > iframe" - ); - - expect(iframe).not.toBeNull(); - - await new Promise(resolve => { - iframe.addEventListener("load", () => { - resolve(); - }); - }); - - expect( - (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML - ).toContain("Alf Says"); - }); -});