import * as React from "react"; import ReactDOMServer from "react-dom/server"; import { test, expect, describe, beforeEach } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; import { __testing__, HtmlEmbed } from "./html-embed"; import { cartesian } from "./test-utils/cartesian"; const { scriptTestIdPrefix } = __testing__; const SCRIPT_TEST_ID = "script-a"; const SCRIPT_PROCESSED_TEST_ID = `${scriptTestIdPrefix}${SCRIPT_TEST_ID}`; const FRAGMENT_DIV_ID = "div"; const App = (props: { clientOnly?: boolean; renderer?: "canvas" | "preview"; executeScriptOnCanvas?: boolean; $ws$executeScripts?: boolean; assetUrlsByPath?: Record; code?: string; }) => { const [page, switchPage] = React.useReducer((n) => (n + 1) % 2, 0); const [refresh, setRefresh] = React.useReducer((n) => n + 1, 0); const code = props.code ?? `
hello
`; return ( "", renderer: props.renderer, resources: {}, breakpoints: [], onError: console.error, }} >
); }; beforeEach(() => { // clear body for (const child of document.body.children) { child.remove(); } }); describe("Published site", () => { test("resolves root-relative asset paths during server rendering", () => { const html = ReactDOMServer.renderToString( `} assetUrlsByPath={{ "/test.js": "/assets/test_hash.js", "/styles/site.css": "/assets/site_hash.css", "/Hero%20image.png": "https://assets.example/hero_hash.png?format=raw", }} /> ); expect(html).toContain('src="/assets/test_hash.js?version=1#ready"'); expect(html).toContain('href="/assets/site_hash.css"'); expect(html).toContain( 'src="https://assets.example/hero_hash.png?format=raw"' ); expect(html).toContain('src="/not-an-asset.png"'); }); /** * Tests the behavior of script tags in an SSR context for a published site with `clientOnly` set to false: * - SSR: Renders script tags in HTML embeds directly, without modification, on the server side, on hydration and on refresh. * - Page Navigation: Executes scripts using additional processing described here https://ghinda.net/article/script-tags/. */ test.each([false, undefined])("clientOnly === false", async (clientOnly) => { const ui = ; const container = document.createElement("div"); // Server rendering document.body.appendChild(container); container.innerHTML = ReactDOMServer.renderToString(ui); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); // Hydration render(ui, { container, hydrate: true }); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); // Force page rerender fireEvent.click(screen.getByText("refresh:0")); expect(screen.getByText("refresh:1")).toBeTruthy(); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_TEST_ID)?.dataset.page).toEqual("0"); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); // Force page change fireEvent.click(screen.getByText("page:0")); expect(screen.getByText("page:1")).toBeTruthy(); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); // Script has changed expect( screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page ).toEqual("1"); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); }); /** * Tests the behavior of script tags in an SSR context for a published site with `clientOnly` set to true: * - SSR: Do not render html embed at all on server side. * - Render HTMLEmbed atfer hydration, then executes scripts using additional processing described here https://ghinda.net/article/script-tags/. * - Page Navigation: Executes scripts using additional processing described here https://ghinda.net/article/script-tags/. */ test("clientOnly === true", async () => { const ui = ; const container = document.createElement("div"); // Server rendering document.body.appendChild(container); container.innerHTML = ReactDOMServer.renderToString(ui); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).not.toBeTruthy(); // Hydration render(ui, { container, hydrate: true }); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); // Force page rerender fireEvent.click(screen.getByText("refresh:0")); expect(screen.getByText("refresh:1")).toBeTruthy(); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); expect( screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page ).toEqual("0"); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); // Force page change fireEvent.click(screen.getByText("page:0")); expect(screen.getByText("page:1")).toBeTruthy; // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); expect( screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page ).toEqual("1"); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); }); }); /** * On canvas renderer, scripts are not executed on the server side. */ describe("Builder renderer= canvas | preview", () => { test("resolves root-relative asset paths for canvas markup", () => { const { container } = render( `} assetUrlsByPath={{ "/test.js": "/cgi/asset/test_hash.js", "/site.css": "/cgi/asset/site_hash.css", "/hero.png": "/cgi/asset/hero_hash.png", }} /> ); expect(container.querySelector("script")?.getAttribute("src")).toBe( "/cgi/asset/test_hash.js" ); expect(container.querySelector("link")?.getAttribute("href")).toBe( "/cgi/asset/site_hash.css" ); expect(container.querySelector("img")?.getAttribute("src")).toBe( "/cgi/asset/hero_hash.png" ); }); test.each(["canvas", "preview"] as const)( "waits for resources to settle before executing scripts in %s", async (renderer) => { const props = { renderer, executeScriptOnCanvas: true, $ws$executeScripts: false, }; const { rerender } = render(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); rerender(); await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); } ); /** * On canvas if renderer is canvas, and executeScriptOnCanvas=false * scripts postprocessing are not applied independently of clientOnly value. */ test.each( cartesian( [true, false, undefined], //clientOnly [false, undefined] //executeScriptOnCanvas ) )( "script processing are not applied when clientOnly=%p executeScriptOnCanvas=%p and renderer=canvas", async (clientOnly, executeScriptOnCanvas) => { const ui = ( ); const container = document.createElement("div"); // Server rendering document.body.appendChild(container); // Hydration render(ui, { container }); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); fireEvent.click(screen.getByText("refresh:0")); expect(screen.getByText("refresh:1")).toBeTruthy(); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); } ); /** * Script postprocessing is always applied for preview mode, independently of other props * Script postprocessing is always applied for canvas renderer if executeScriptOnCanvas = true or undefined */ test.each([ ...cartesian( [true, false, undefined], // clientOnly ["preview" as const], // renderer [true, false, undefined] // executeScriptOnCanvas ), ...cartesian( [true, false, undefined], // clientOnly ["canvas" as const], // renderer [true] // executeScriptOnCanvas ), ])( "In preview mode, script processing are applied when clientOnly=%p and renderer=%p and executeScriptOnCanvas=%p", async (clientOnly, renderer, executeScriptOnCanvas) => { const ui = ( ); const container = document.createElement("div"); // Server rendering document.body.appendChild(container); // Hydration render(ui, { container }); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); fireEvent.click(screen.getByText("refresh:0")); expect(screen.getByText("refresh:1")).toBeTruthy(); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); document.body.removeChild(container); } ); test("Code change cause scripts to be updated", async () => { const SCRIPT_TEST_ID_2 = "script-b"; const SCRIPT_PROCESSED_TEST_ID_2 = `${scriptTestIdPrefix}${SCRIPT_TEST_ID_2}`; const codes = [ ``, ``, ]; const AppWithCode = () => { const [codeIndex, switchCodeIndex] = React.useReducer( (n) => (n + 1) % 2, 0 ); const code = codes[codeIndex]; return ( "", renderer: "canvas", resources: {}, breakpoints: [], onError: console.error, }} > ); }; const ui = ; const container = document.createElement("div"); // Server rendering document.body.appendChild(container); // Hydration render(ui, { container }); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy(); // Force page rerender fireEvent.click(screen.getByText("code:0")); // Wait execute await task(); to be executed (if exists) await Promise.resolve(); expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); // Code has changed, new script processed expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID_2)).toBeTruthy(); }); test.each(["", " "])("Placeholder is shown if code is %p", async (code) => { const AppWithCode = () => { return ; }; const ui = ; const container = document.createElement("div"); // Server rendering document.body.appendChild(container); // Hydration render(ui, { container }); expect( screen.queryByText(`Open the "Settings" panel to insert HTML code.`) ).toBeTruthy(); }); /** * Disabled script execution takes precedence over canvas settings and renderer mode. */ test.each( cartesian( [true, false, undefined], //clientOnly [true, false, undefined], //executeScriptOnCanvas ["canvas", "preview"] //renderer ) )( "Safe mode prevents script execution when clientOnly=%p executeScriptOnCanvas=%p and renderer=%p", async (clientOnly, executeScriptOnCanvas, renderer) => { const AppWithSafeMode = () => { const code = `
hello
`; return ( "", renderer: renderer as "canvas" | "preview", resources: {}, breakpoints: [], onError: console.error, }} > ); }; const ui = ; const container = document.createElement("div"); document.body.appendChild(container); render(ui, { container }); await Promise.resolve(); // Scripts should not be processed (no SCRIPT_PROCESSED_TEST_ID) expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy(); expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy(); expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy(); } ); });