diff --git a/cloudflare/artifact-routes.ts b/cloudflare/artifact-routes.ts index 1b1debe..5605b2c 100644 --- a/cloudflare/artifact-routes.ts +++ b/cloudflare/artifact-routes.ts @@ -128,7 +128,7 @@ export async function artifactRoute(request: Request, env: Env): Promise
`; + const frame = `
`; return html(`Artifact`); + return c.html(`Sidequery Artifacts preview
`); }); app.get("/", c => c.env.ASSETS.fetch(new Request(new URL("/index.html", c.req.url)))); app.get("/gallery", c => c.env.ASSETS.fetch(new Request(new URL("/index.html", c.req.url)))); diff --git a/e2e/artifact-layout.test.ts b/e2e/artifact-layout.test.ts new file mode 100644 index 0000000..bbc5471 --- /dev/null +++ b/e2e/artifact-layout.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "bun:test"; +import { rmSync } from "node:fs"; +import { join } from "node:path"; +import { chromium } from "playwright"; +import { createArtifactServer } from "../src/serve"; +import { VIEWPORT_ARTIFACT, checkArtifactLayout } from "../src/test/artifact-layout"; +import { tempDir, writeArtifact } from "../src/test/fixtures"; + +test("local artifacts and gallery previews fill the viewport without host padding", async () => { + const directory = tempDir("artifact-layout-"); + writeArtifact(directory, "presentation", VIEWPORT_ARTIFACT); + const server = await createArtifactServer({ artifactsDir: directory, historyPath: join(directory, "history.sqlite"), gallery: true }); + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + for (const viewport of [{ width: 1440, height: 960 }, { width: 390, height: 844 }]) { + await page.setViewportSize(viewport); + for (const path of ["/c/presentation", "/gallery/preview?name=presentation"]) { + expect((await page.goto(server.url + path))!.status()).toBe(200); + await checkArtifactLayout(page); + } + } + } finally { + await browser.close(); + server.stop(); + rmSync(directory, { recursive: true, force: true }); + } +}, 30_000); diff --git a/package.json b/package.json index b3d0e18..2b1c6eb 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "test:native-worker": "tsc -p scripts/native-worker/tsconfig.json --noEmit && bun test scripts/native-worker --timeout 30000", "native-worker:operator": "bun run scripts/native-worker/operator.ts", "test:e2e": "bun test e2e/herdr.e2e.test.ts", - "test:mcp-ui": "bun test e2e/gallery-transfer.test.ts e2e/gallery-creation.test.ts e2e/gallery-onboarding.test.ts e2e/gallery-drafts.test.ts e2e/gallery-move.test.ts e2e/project-editor.test.ts e2e/mcp-app.test.ts e2e/mcp-file-editor.test.ts e2e/gallery-scripts.test.ts e2e/gallery-secrets.test.ts e2e/gallery-native-apps.test.ts e2e/gallery-navigation.test.ts e2e/gallery-live.test.ts e2e/gallery-connection.test.ts e2e/execution-controls.test.ts e2e/browser-plugins.test.ts e2e/plugin-bridge.test.ts e2e/routing.test.ts e2e/runner-status.test.ts --timeout 30000", + "test:mcp-ui": "bun test e2e/gallery-transfer.test.ts e2e/gallery-creation.test.ts e2e/gallery-onboarding.test.ts e2e/gallery-drafts.test.ts e2e/gallery-move.test.ts e2e/project-editor.test.ts e2e/mcp-app.test.ts e2e/mcp-file-editor.test.ts e2e/gallery-scripts.test.ts e2e/gallery-secrets.test.ts e2e/gallery-native-apps.test.ts e2e/gallery-navigation.test.ts e2e/gallery-live.test.ts e2e/gallery-connection.test.ts e2e/execution-controls.test.ts e2e/browser-plugins.test.ts e2e/plugin-bridge.test.ts e2e/routing.test.ts e2e/artifact-layout.test.ts e2e/runner-status.test.ts --timeout 30000", "typecheck": "tsc --noEmit && tsc -p examples/runner-status/tsconfig.json --noEmit && tsc -p examples/runner-status/tsconfig.local.json --noEmit", "dev:cloudflare": "wrangler dev --local --var ENVIRONMENT:local", "prepare:celld": "bun run scripts/prepare-celld.ts", diff --git a/src/html.ts b/src/html.ts index 6cb3357..a0b446f 100644 --- a/src/html.ts +++ b/src/html.ts @@ -34,7 +34,7 @@ export function artifactHtml(opts: { color: #f0f0f0; font-family: ui-sans-serif, system-ui, sans-serif; } - #root { padding: 24px; box-sizing: border-box; } + #root { box-sizing: border-box; } diff --git a/src/test/artifact-layout.ts b/src/test/artifact-layout.ts new file mode 100644 index 0000000..3970bb2 --- /dev/null +++ b/src/test/artifact-layout.ts @@ -0,0 +1,44 @@ +import { expect } from "bun:test"; +import type { FrameLocator, Page } from "playwright"; + +export const VIEWPORT_ARTIFACT = `import { useState } from "sidequery/artifacts"; +export default function Presentation() { + const [expanded, setExpanded] = useState(false); + return
+
Presentation
+ +
; +}`; + +export async function checkArtifactLayout(view: Page | FrameLocator) { + const presentation = view.getByRole("main"); + await presentation.waitFor(); + const layout = await presentation.evaluate(element => { + const bounds = element.getBoundingClientRect(); + const header = element.querySelector("header")!.getBoundingClientRect(); + const footer = element.querySelector("footer")!.getBoundingClientRect(); + return { + x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, + viewportWidth: innerWidth, viewportHeight: innerHeight, + scrollWidth: document.documentElement.scrollWidth, + scrollHeight: document.documentElement.scrollHeight, + headerX: header.x, headerY: header.y, footerBottom: footer.bottom, + }; + }); + expect(layout.x).toBe(0); + expect(layout.y).toBe(0); + expect(layout.width).toBe(layout.viewportWidth); + expect(layout.height).toBe(layout.viewportHeight); + expect(layout.scrollWidth).toBe(layout.viewportWidth); + expect(layout.scrollHeight).toBe(layout.viewportHeight); + expect(layout.headerX).toBe(32); + expect(layout.headerY).toBe(32); + expect(layout.footerBottom).toBe(layout.viewportHeight - 32); + + await view.getByRole("button", { name: "Show more", exact: true }).click(); + await view.getByRole("button", { name: "Show less", exact: true }).waitFor(); + expect(await presentation.evaluate(() => document.documentElement.scrollHeight > innerHeight)).toBe(true); + // The footer remains reachable when the artifact intentionally exceeds the viewport. + await view.getByRole("button", { name: "Show less", exact: true }).click(); + await view.getByRole("button", { name: "Show more", exact: true }).waitFor(); +}