|
| 1 | +import { mkdir } from "node:fs/promises"; |
| 2 | +import path from "node:path"; |
| 3 | +import { chromium, type Browser, type Page } from "playwright"; |
| 4 | +import { afterAll, beforeAll, describe, expect, it } from "vitest"; |
| 5 | +import { |
| 6 | + canRunPlaywrightChromium, |
| 7 | + installMockGateway, |
| 8 | + resolvePlaywrightChromiumExecutablePath, |
| 9 | + startControlUiE2eServer, |
| 10 | + type ControlUiE2eServer, |
| 11 | +} from "../test-helpers/control-ui-e2e.ts"; |
| 12 | + |
| 13 | +const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath()); |
| 14 | +const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath); |
| 15 | +const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1"; |
| 16 | +const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip; |
| 17 | +const proofDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim(); |
| 18 | + |
| 19 | +let browser: Browser; |
| 20 | +let server: ControlUiE2eServer; |
| 21 | + |
| 22 | +function workspaceDoc() { |
| 23 | + return { |
| 24 | + doc: { |
| 25 | + schemaVersion: 1, |
| 26 | + workspaceVersion: 1, |
| 27 | + tabs: [ |
| 28 | + { |
| 29 | + slug: "previews", |
| 30 | + title: "Secure previews", |
| 31 | + hidden: false, |
| 32 | + createdBy: "agent:main", |
| 33 | + widgets: [ |
| 34 | + { |
| 35 | + id: "preview_allowed", |
| 36 | + kind: "builtin:preview", |
| 37 | + title: "Local app preview", |
| 38 | + grid: { x: 0, y: 0, w: 8, h: 5 }, |
| 39 | + collapsed: false, |
| 40 | + createdBy: "agent:main", |
| 41 | + props: { url: "/stale", defaultViewport: "desktop" }, |
| 42 | + bindings: { value: { source: "static", value: "/preview-proof" } }, |
| 43 | + }, |
| 44 | + { |
| 45 | + id: "preview_blocked", |
| 46 | + kind: "builtin:preview", |
| 47 | + title: "Blocked external preview", |
| 48 | + grid: { x: 8, y: 0, w: 4, h: 5 }, |
| 49 | + collapsed: false, |
| 50 | + createdBy: "agent:main", |
| 51 | + props: { url: "https://blocked.example" }, |
| 52 | + }, |
| 53 | + ], |
| 54 | + }, |
| 55 | + ], |
| 56 | + widgetsRegistry: {}, |
| 57 | + prefs: { tabOrder: ["previews"] }, |
| 58 | + }, |
| 59 | + workspaceVersion: 1, |
| 60 | + }; |
| 61 | +} |
| 62 | + |
| 63 | +async function capture(page: Page, name: string): Promise<void> { |
| 64 | + if (!proofDir) { |
| 65 | + return; |
| 66 | + } |
| 67 | + await mkdir(proofDir, { recursive: true }); |
| 68 | + await page.screenshot({ fullPage: true, path: path.join(proofDir, name) }); |
| 69 | +} |
| 70 | + |
| 71 | +describeControlUiE2e("Control UI secure preview widget", () => { |
| 72 | + beforeAll(async () => { |
| 73 | + if (!chromiumAvailable) { |
| 74 | + throw new Error(`Playwright Chromium is not available at ${chromiumExecutablePath}`); |
| 75 | + } |
| 76 | + server = await startControlUiE2eServer(); |
| 77 | + browser = await chromium.launch({ executablePath: chromiumExecutablePath }); |
| 78 | + }); |
| 79 | + |
| 80 | + afterAll(async () => { |
| 81 | + await browser?.close(); |
| 82 | + await server?.close(); |
| 83 | + }); |
| 84 | + |
| 85 | + it("renders an allowed bound origin, blocks an external origin, and preserves controls", async () => { |
| 86 | + const context = await browser.newContext({ |
| 87 | + locale: "en-US", |
| 88 | + serviceWorkers: "block", |
| 89 | + viewport: { height: 900, width: 1440 }, |
| 90 | + }); |
| 91 | + const page = await context.newPage(); |
| 92 | + let previewRequests = 0; |
| 93 | + await page.route("**/preview-proof", async (route) => { |
| 94 | + previewRequests += 1; |
| 95 | + await route.fulfill({ |
| 96 | + contentType: "text/html", |
| 97 | + body: `<!doctype html><style>body{margin:0;font:24px system-ui;background:#13293d;color:#fff;display:grid;place-items:center;height:100vh}strong{color:#62d9ff}</style><p><strong>Allowed</strong> same-origin preview</p>`, |
| 98 | + }); |
| 99 | + }); |
| 100 | + await installMockGateway(page, { |
| 101 | + controlUiTabs: [ |
| 102 | + { |
| 103 | + pluginId: "workspaces", |
| 104 | + id: "workspaces", |
| 105 | + label: "Workspaces", |
| 106 | + group: "control", |
| 107 | + }, |
| 108 | + ], |
| 109 | + featureMethods: ["workspaces.get"], |
| 110 | + methodResponses: { "workspaces.get": workspaceDoc() }, |
| 111 | + }); |
| 112 | + |
| 113 | + try { |
| 114 | + const response = await page.goto(`${server.baseUrl}plugin?plugin=workspaces&id=workspaces`); |
| 115 | + expect(response?.status()).toBe(200); |
| 116 | + const allowed = page.locator('[data-widget-id="preview_allowed"]'); |
| 117 | + const blocked = page.locator('[data-widget-id="preview_blocked"]'); |
| 118 | + const frame = allowed.locator('[data-test-id="workspace-preview-frame"]'); |
| 119 | + await frame.waitFor({ timeout: 10_000 }); |
| 120 | + await blocked.locator('[data-test-id="workspace-preview-blocked"]').waitFor(); |
| 121 | + expect(await frame.getAttribute("src")).toBe("/preview-proof"); |
| 122 | + expect(await frame.getAttribute("sandbox")).toBe("allow-scripts"); |
| 123 | + expect(await frame.getAttribute("sandbox")).not.toContain("allow-same-origin"); |
| 124 | + expect(await blocked.locator('[data-test-id="workspace-preview-frame"]').count()).toBe(0); |
| 125 | + await expect.poll(() => previewRequests).toBeGreaterThan(0); |
| 126 | + await capture(page, "workspace-preview-allowed-and-blocked.png"); |
| 127 | + |
| 128 | + await allowed.locator('[data-test-id="workspace-preview-viewport-mobile"]').click(); |
| 129 | + await expect |
| 130 | + .poll(() => allowed.locator(".workspace-preview__frame-wrap--mobile").count()) |
| 131 | + .toBe(1); |
| 132 | + expect( |
| 133 | + await allowed |
| 134 | + .locator('[data-test-id="workspace-preview-viewport-mobile"]') |
| 135 | + .getAttribute("aria-pressed"), |
| 136 | + ).toBe("true"); |
| 137 | + await capture(page, "workspace-preview-mobile.png"); |
| 138 | + |
| 139 | + const requestsBeforeReload = previewRequests; |
| 140 | + await allowed.locator('[data-test-id="workspace-preview-reload"]').click(); |
| 141 | + await expect.poll(() => previewRequests).toBeGreaterThan(requestsBeforeReload); |
| 142 | + expect(await allowed.locator(".workspace-preview__frame-wrap--mobile").count()).toBe(1); |
| 143 | + } finally { |
| 144 | + await context.close(); |
| 145 | + } |
| 146 | + }); |
| 147 | +}); |
0 commit comments