|
| 1 | +// End-to-end browser proof that an embedder can take over the engine's MAIN |
| 2 | +// content pane through the shadow boundary via the `ss:main` slot — the seam the |
| 3 | +// sideshow cloud uses to render its full-page "Settings" view in the main area |
| 4 | +// while the engine's sidebar (session list + account footer) stays put. |
| 5 | +// |
| 6 | +// Same harness as embed-slots.spec.ts. We mount in the default "full" layout with |
| 7 | +// a real session in view, and project a light-DOM `<div slot="ss:main">` child of |
| 8 | +// the mount element. The engine's <slot name="ss:main"> projects it in place of |
| 9 | +// the board, so the host pane shows and the engine's own #stream does not. |
| 10 | +import { readFileSync } from "node:fs"; |
| 11 | +import { fileURLToPath } from "node:url"; |
| 12 | +import { expect, publish, test } from "./fixtures.ts"; |
| 13 | + |
| 14 | +const embedDir = fileURLToPath(new URL("../viewer/dist-embed", import.meta.url)); |
| 15 | + |
| 16 | +function contentType(path: string): string { |
| 17 | + if (path.endsWith(".js") || path.endsWith(".mjs")) return "text/javascript"; |
| 18 | + if (path.endsWith(".wasm")) return "application/wasm"; |
| 19 | + if (path.endsWith(".css")) return "text/css"; |
| 20 | + return "application/octet-stream"; |
| 21 | +} |
| 22 | + |
| 23 | +const embedHtml = (sessionId: string) => `<!doctype html> |
| 24 | +<html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%}#m{position:fixed;inset:0}</style></head> |
| 25 | +<body><div id="m"><div slot="ss:main" id="hostMain"><h2>Host settings pane</h2></div></div> |
| 26 | +<script type="module"> |
| 27 | + import { mountViewer } from "/__embed/engine.js"; |
| 28 | + mountViewer(document.getElementById("m"), { |
| 29 | + basePath: "", |
| 30 | + router: { |
| 31 | + get: () => ({ sessionId: ${JSON.stringify(sessionId)} }), |
| 32 | + navigate() {}, |
| 33 | + subscribe() { return () => {}; }, |
| 34 | + }, |
| 35 | + }); |
| 36 | +</script></body></html>`; |
| 37 | + |
| 38 | +test("embedded engine: ss:main slot takes over the main pane while the sidebar stays", async ({ |
| 39 | + page, |
| 40 | + server, |
| 41 | +}) => { |
| 42 | + const surface = await publish( |
| 43 | + server.url, |
| 44 | + { html: "<p>board card</p>", title: "Board card", agent: "e2e" }, |
| 45 | + "", |
| 46 | + ); |
| 47 | + |
| 48 | + page.on("pageerror", (e) => console.error("[pageerror]", e.message)); |
| 49 | + page.on("console", (m) => m.type() === "error" && console.error("[console]", m.text())); |
| 50 | + |
| 51 | + await page.route("**/__embedtest", (route) => |
| 52 | + route.fulfill({ contentType: "text/html", body: embedHtml(surface.sessionId) }), |
| 53 | + ); |
| 54 | + await page.route("**/__embed/**", (route) => { |
| 55 | + const name = new URL(route.request().url()).pathname.replace("/__embed/", ""); |
| 56 | + route.fulfill({ contentType: contentType(name), body: readFileSync(`${embedDir}/${name}`) }); |
| 57 | + }); |
| 58 | + |
| 59 | + await page.goto(`${server.url}/__embedtest`); |
| 60 | + |
| 61 | + // The host's light-DOM pane projects into the main slot and is visible. |
| 62 | + const hostMain = page.locator("#hostMain"); |
| 63 | + await expect(hostMain).toBeVisible(); |
| 64 | + await expect(page.locator("main slot[name='ss:main']")).toHaveCount(1); |
| 65 | + |
| 66 | + // The sidebar (full layout) stays — the override is the main pane only, not the |
| 67 | + // whole viewport. |
| 68 | + await expect(page.locator("aside")).toBeVisible(); |
| 69 | + |
| 70 | + // The engine's own board content is replaced: with a child assigned to ss:main, |
| 71 | + // the slot's fallback (#sessionView stream) stays in the DOM — native <slot> |
| 72 | + // mechanics — but is not displayed, so the host pane is what the user sees. |
| 73 | + await expect(page.locator("#stream")).toBeHidden(); |
| 74 | +}); |
0 commit comments