diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49accfc403..7f8d542370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,6 +120,12 @@ jobs: - name: Typecheck all packages run: npm run typecheck + - name: Install website test browsers + run: npx playwright install --with-deps chromium webkit + + - name: Verify website mockups + run: npm run test:e2e --workspace=@getpaseo/website + - name: Verify public package contents run: | npm pack --dry-run --ignore-scripts --workspace=@getpaseo/protocol diff --git a/packages/website/e2e/mockups.spec.ts b/packages/website/e2e/mockups.spec.ts new file mode 100644 index 0000000000..8b0ba65857 --- /dev/null +++ b/packages/website/e2e/mockups.spec.ts @@ -0,0 +1,61 @@ +import { expect, test, type Locator, type Page } from "playwright/test"; + +const desktopName = + "Paseo desktop app with coding agents, a conversation, and a code diff open side by side"; + +async function openHomepage(page: Page) { + await page.setViewportSize({ width: 1512, height: 930 }); + await page.goto("/"); + await expect(page.getByRole("button", { name: "Build", exact: true })).toBeVisible(); +} + +async function viewPhones(page: Page) { + await page.getByRole("img", { name: "Paseo agent chat", exact: true }).scrollIntoViewIfNeeded(); +} + +async function expectScreenContentInsidePhone(page: Page, name: string, time: string) { + const phone = page.getByRole("img", { name, exact: true }); + const clock = phone.getByText(time, { exact: true }); + await expect(clock).toBeVisible(); + await expect(async () => { + const frame = await phone.boundingBox(); + const content = await clock.boundingBox(); + expect(frame).not.toBeNull(); + expect(content).not.toBeNull(); + if (!frame || !content) throw new Error("Phone screen is not rendered"); + expect(content.x).toBeGreaterThanOrEqual(frame.x); + expect(content.y).toBeGreaterThanOrEqual(frame.y); + expect(content.x + content.width).toBeLessThanOrEqual(frame.x + frame.width); + expect(content.y + content.height).toBeLessThanOrEqual(frame.y + frame.height); + }).toPass({ timeout: 5000 }); +} + +async function cornerProportion(mockup: Locator) { + return mockup.evaluate((element) => { + const radius = getComputedStyle(element).borderTopLeftRadius.split(" ")[0]; + if (radius.endsWith("%")) return Number.parseFloat(radius) / 100; + return Number.parseFloat(radius) / element.getBoundingClientRect().width; + }); +} + +async function resizeToPhone(page: Page) { + await page.setViewportSize({ width: 390, height: 844 }); + await page.getByRole("img", { name: desktopName, exact: true }).scrollIntoViewIfNeeded(); +} + +test("the three phone mockups show their screen content", async ({ page }) => { + await openHomepage(page); + await viewPhones(page); + await expectScreenContentInsidePhone(page, "Paseo workspace drawer", "18:54"); + await expectScreenContentInsidePhone(page, "Paseo agent chat", "18:53"); + await expectScreenContentInsidePhone(page, "Paseo diff view", "18:55"); +}); + +test("desktop mockup corners shrink in proportion on a phone", async ({ page }) => { + await openHomepage(page); + const mockup = page.getByRole("img", { name: desktopName, exact: true }); + const desktopCorner = await cornerProportion(mockup); + expect(desktopCorner).toBeGreaterThan(0); + await resizeToPhone(page); + await expect.poll(() => cornerProportion(mockup)).toBeCloseTo(desktopCorner, 3); +}); diff --git a/packages/website/package.json b/packages/website/package.json index 0545ce6850..514e9fb2f4 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -8,7 +8,8 @@ "build": "vite build", "preview": "vite preview", "typecheck": "tsgo --noEmit", - "deploy": "npm run build && wrangler deploy" + "deploy": "npm run build && wrangler deploy", + "test:e2e": "playwright test" }, "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", diff --git a/packages/website/playwright.config.ts b/packages/website/playwright.config.ts new file mode 100644 index 0000000000..011a34e6ad --- /dev/null +++ b/packages/website/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from "playwright/test"; + +const baseURL = process.env.WEBSITE_TEST_URL ?? "http://127.0.0.1:8187"; + +export default defineConfig({ + testDir: "./e2e", + workers: 1, + use: { baseURL, screenshot: "only-on-failure", trace: "retain-on-failure" }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + ], + webServer: process.env.WEBSITE_TEST_URL + ? undefined + : { + command: "npm run dev -- --host 127.0.0.1 --port 8187 --strictPort", + url: baseURL, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index 4204a8d036..eeffc41d79 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -7,22 +7,12 @@ import { MOCKUP_STATES, type MockupStateId, MockupWindow, + ScaledMockup, } from "~/components/mockup"; const ALT = "Paseo desktop app with coding agents, a conversation, and a code diff open side by side"; -const ASPECT_STYLE = { aspectRatio: `${DESIGN_WIDTH} / ${DESIGN_HEIGHT}` }; - -// `tan(atan2(a, b))` is the CSS way to divide one length by another, so the scale -// factor tracks the container with no JS, no layout thrash, and a correct -// server-rendered first paint. -const SCALE_STYLE = { - width: DESIGN_WIDTH, - height: DESIGN_HEIGHT, - transform: `scale(tan(atan2(100cqw, ${DESIGN_WIDTH}px)))`, -}; - const PILL_TRANSITION = { duration: 0.34, ease: [0.22, 0.61, 0.36, 1] as const }; export function HeroMockup() { @@ -42,21 +32,15 @@ export function HeroMockup() { ))} -