From 5cbb1f97454faa2287e7c32e002a96e016f7d1df Mon Sep 17 00:00:00 2001 From: Andrew Golovanov Date: Tue, 15 Sep 2026 19:58:39 +0200 Subject: [PATCH] test(pulse): monitor critical public journeys with SiteOS --- .gitignore | 1 + e2e/README.md | 52 ++++++++++++++++++++++++++++++ e2e/package.json | 2 +- e2e/pulse/AGENTS.md | 41 ++++++++++++++++++++++++ e2e/pulse/agent-service.spec.ts | 35 +++++++++++++++++++++ e2e/pulse/authentication.spec.ts | 52 ++++++++++++++++++++++++++++++ e2e/pulse/legal.spec.ts | 18 +++++++++++ e2e/pulse/mobile.spec.ts | 20 ++++++++++++ e2e/pulse/onboarding.spec.ts | 25 +++++++++++++++ e2e/pulse/tsconfig.json | 5 +++ e2e/tsconfig.json | 3 +- package.json | 1 + pnpm-lock.yaml | 54 +++++++++++++++++++++++++------- siteos.config.json | 54 ++++++++++++++++++++++++++++++++ siteos.playwright.config.ts | 28 +++++++++++++++++ 15 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 e2e/pulse/AGENTS.md create mode 100644 e2e/pulse/agent-service.spec.ts create mode 100644 e2e/pulse/authentication.spec.ts create mode 100644 e2e/pulse/legal.spec.ts create mode 100644 e2e/pulse/mobile.spec.ts create mode 100644 e2e/pulse/onboarding.spec.ts create mode 100644 e2e/pulse/tsconfig.json create mode 100644 siteos.config.json create mode 100644 siteos.playwright.config.ts diff --git a/.gitignore b/.gitignore index efdf713..9a0a1d3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ pnpm-debug.log* *.sqlite-journal *.tsbuildinfo *.swp +.siteos/pulse/ diff --git a/e2e/README.md b/e2e/README.md index 9762852..1ec9b7e 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -131,3 +131,55 @@ overlapping runs and alert on a non-zero exit code. Each runner writes one JSON one safe error message to stderr; credentials and login tokens are never emitted. Website confirmation links require `DENSIO_SYNTHETIC_WEBSITE_URL` alongside `DENSIO_SYNTHETIC_API_URL` for opt-in deployed synthetics. The harness validates the email origin and submits confirmation JSON to the configured API; it never follows an arbitrary email destination. + +## SiteOS Pulse monitoring + +The `pulse/*.spec.ts` checks monitor public journeys on `https://densio.sh` and the agent API at +`https://api.densio.sh`. The root `siteos.config.json` owns their schedules and +`siteos.playwright.config.ts` owns browser settings. These production checks run only through +explicit Pulse or Playwright commands; Vitest continues to discover `*.test.ts` only. +The existing `pnpm typecheck:e2e` and root `pnpm check` include their static checks. + +| Check | Schedule | Coverage | +| --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Agent onboarding | Every 15 minutes | Homepage, copied installation prompt, Get Started, and return home. | +| Sign-in and account entry | Every 15 minutes | Invalid-email validation without writes, anonymous account redirects, safe return destinations, and canceled-checkout navigation. | +| Agent API and runtime skill | Every 15 minutes | Media-tool readiness and anonymous runtime-skill retrieval with version and checksum metadata. | +| Mobile sign-in journey | Hourly | Homepage viewport fit, menu navigation, and usable sign-in form at 390 × 844. | +| Legal page navigation | Daily | Footer links reach readable privacy and terms pages. | + +The five checks contain 11 tests and total 313 scheduled check runs per day when active. This is +a run count, not a billing estimate. They require no credentials or stateful production actions; +email delivery, authenticated processing, storage, and payments remain outside this coverage. +Legal-page checks verify navigation and rendering; the current content contains placeholders. +Authoring and production rules live in [pulse/AGENTS.md](pulse/AGENTS.md). + +Run these commands from the repository root, using Node.js 22.18+ and pnpm 11.7.0 through Corepack: + +```sh +corepack pnpm install --frozen-lockfile +corepack pnpm exec playwright install chromium +corepack pnpm typecheck:e2e +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli project use PROJECT_ID --organization ORGANIZATION_ID --environment production --json +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli project status --json +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse validate --json +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse sync --check --json +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse test +SITEOS_AUTH_BASE_URL=https://app.siteos.sh npx @siteoshq/cli pulse deploy --dry-run --json +``` + +Use the exact IDs from the approved task or SiteOS. Confirm the selected Densio production URL +and Pulse attachment in the safe status output. To run one spec through the pinned local runner: + +```sh +PLAYWRIGHT_BASE_URL=https://densio.sh corepack pnpm exec playwright test --config siteos.playwright.config.ts e2e/pulse/onboarding.spec.ts +``` + +The API spec always targets `https://api.densio.sh`, including when the website base URL is +overridden. Run only the intended browser specs for local website checks. + +After explicit publication approval, use `pulse deploy --json`. CLI 2.13.0+ supports +`pulse run --check CHECK_ID --request-id REQUEST_ID --json`; take Check IDs from the deployment +result and read terminal Run results through SiteOS MCP. Scheduling and remote health remain +unverified until deployment and subsequent scheduled results; a local pass or dry run does not +establish either. Local failure artifacts and deployment archives are ignored under `.siteos/pulse/`. diff --git a/e2e/package.json b/e2e/package.json index 734d2f1..b18edb8 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -7,7 +7,7 @@ "synthetic:production": "tsx deployed/production.ts", "synthetic:staging": "tsx deployed/staging.ts", "test": "vitest run --config vitest.config.ts", - "typecheck": "tsgo --noEmit --project tsconfig.json --pretty false", + "typecheck": "tsgo --noEmit --project tsconfig.json --pretty false && tsgo --noEmit --project pulse/tsconfig.json --pretty false", "test:fast": "vitest run --config vitest.config.ts --exclude golden-path.test.ts", "test:media": "vitest run --config vitest.config.ts golden-path.test.ts" }, diff --git a/e2e/pulse/AGENTS.md b/e2e/pulse/AGENTS.md new file mode 100644 index 0000000..3ad0851 --- /dev/null +++ b/e2e/pulse/AGENTS.md @@ -0,0 +1,41 @@ +# Pulse monitoring checks + +## Ownership and execution + +- Keep public website and agent-service monitors here as `*.spec.ts`. The root + `siteos.config.json` selects checks and schedules; `siteos.playwright.config.ts` owns Chromium + configuration. Update their paths together when moving a spec. +- Preserve the existing `e2e` Vitest `**/*.test.ts` discovery. Production Pulse requests must not + run through `pnpm test` or `pnpm check`. The existing `e2e` typecheck includes these specs and + the root Playwright configuration. +- Keep browser DOM types in this directory's `tsconfig.json`, separate from the parent Node-only + harness. The `e2e` package's `typecheck` command must run both configurations; combining their + global types causes incompatible Node and browser stream definitions in the API harness. +- Keep coverage descriptions and operator commands in `../README.md#siteos-pulse-monitoring`; + keep durable agent rules here. Do not duplicate the SiteOS plugin's general workflow. + +## Production boundaries + +- Use fresh anonymous browser contexts and relative website paths. The selected SiteOS environment + supplies `PLAYWRIGHT_BASE_URL`. Keep a single worker, no retries, bounded timeouts, and failure + artifacts under the Git-ignored `.siteos/pulse/` directory. +- `agent-service.spec.ts` deliberately targets the public CLI's production default, + `https://api.densio.sh`. A website base-URL override does not retarget it. Exclude that spec when + checking only a local website; do not infer another API origin from a website hostname. +- Do not send login emails, create or change accounts, upload media, start processing jobs, or + perform purchases. Preserve the invalid-email check's write-request blocking and its assertion + that no write was attempted. New stateful scenarios need an explicitly authorized isolated + test-data contract; see the deployed synthetics in the parent README. +- Keep Project IDs and credentials in private CLI state. Never put tokens, auth state, signed + artifact URLs, or private data in the bundle or tracked reports. + +## Verification and publication + +- For changes to these checks, run the existing `pnpm typecheck:e2e`, then the Pulse validation, + sync preview, test, and deployment dry-run commands documented in the parent README. Apply the + repository's completion checks as well. Inspect the manifest for only intended runtime files. +- Preserve meaningful user-observable assertions. API readiness does not prove a completed + compression job; legal-page rendering does not establish the quality of legal content. +- Publication requires the user's explicit approval. After an approved deployment, inspect + terminal remote results for each deployed check. Report local tests, bundle construction, + deployment, manual runs, and subsequent scheduled results separately. diff --git a/e2e/pulse/agent-service.spec.ts b/e2e/pulse/agent-service.spec.ts new file mode 100644 index 0000000..a31f595 --- /dev/null +++ b/e2e/pulse/agent-service.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; + +// This check intentionally targets the production API used by the public Densio CLI. +const apiUrl = "https://api.densio.sh"; + +test("The agent API and media tools are ready", async ({ request }) => { + const response = await request.get(`${apiUrl}/ready`, { timeout: 15_000 }); + expect(response.status()).toBe(200); + const payload: unknown = await response.json(); + expect(payload).toMatchObject({ + status: "ready", + ffmpegVersion: expect.stringMatching(/\S+/), + ffprobeVersion: expect.stringMatching(/\S+/), + }); +}); + +test("An anonymous agent can retrieve the current runtime skill", async ({ request }) => { + const response = await request.get(`${apiUrl}/v1/skill`, { timeout: 15_000 }); + expect(response.status()).toBe(200); + const payload: unknown = await response.json(); + expect(payload).toMatchObject({ + ok: true, + data: { + entrypoint: "SKILL.md", + skillVersion: expect.stringMatching(/^sha256:[a-f0-9]{64}$/), + files: expect.arrayContaining([ + expect.objectContaining({ + path: "SKILL.md", + content: expect.stringContaining("densio"), + sha256: expect.stringMatching(/^[a-f0-9]{64}$/), + }), + ]), + }, + }); +}); diff --git a/e2e/pulse/authentication.spec.ts b/e2e/pulse/authentication.spec.ts new file mode 100644 index 0000000..de9eaa1 --- /dev/null +++ b/e2e/pulse/authentication.spec.ts @@ -0,0 +1,52 @@ +import { expect, test } from "@playwright/test"; + +test("Email sign-in rejects empty and malformed input without sending a request", async ({ + page, +}) => { + const blockedWrites: string[] = []; + // Keep this production monitor safe even if native form validation regresses. + await page.route("**/*", async (route) => { + const method = route.request().method(); + if (["GET", "HEAD", "OPTIONS"].includes(method)) return route.continue(); + blockedWrites.push(method); + await route.abort("blockedbyclient"); + }); + const response = await page.goto("/auth/login"); + expect(response?.status()).toBe(200); + const email = page.getByRole("textbox", { name: "Email", exact: true }); + const submit = page.getByRole("button", { name: "Continue with email" }); + await expect(email).toHaveAttribute("type", "email"); + await expect(email).toHaveAttribute("required", ""); + await submit.click(); + await expect(email).toBeFocused(); + expect(await email.evaluate((input: HTMLInputElement) => input.validity.valueMissing)).toBe(true); + await email.fill("invalid-email"); + await submit.click(); + await expect(email).toBeFocused(); + expect(await email.evaluate((input: HTMLInputElement) => input.validity.typeMismatch)).toBe(true); + await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Check your email" })).toHaveCount(0); + expect(blockedWrites).toEqual([]); +}); + +test("An anonymous account visit preserves the sign-in return destination", async ({ page }) => { + await page.goto("/app"); + await expect(page).toHaveURL(/\/auth\/login\?returnTo=%2Fapp$/); + await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible(); + await expect(page.locator('input[name="returnTo"]')).toHaveValue("/app"); +}); + +test("Login ignores an external return destination", async ({ page }) => { + await page.goto("/auth/login?returnTo=https%3A%2F%2Fexample.com"); + await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible(); + await expect(page.locator('input[name="returnTo"]')).toHaveValue("/app"); +}); + +test("A canceled checkout offers a working route back to the account", async ({ page }) => { + const response = await page.goto("/checkout/canceled"); + expect(response?.status()).toBe(200); + await expect(page.getByRole("heading", { name: "Checkout canceled" })).toBeVisible(); + await page.getByRole("link", { name: "Go to billing", exact: true }).click(); + await expect(page).toHaveURL(/\/auth\/login\?returnTo=%2Fapp$/); + await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible(); +}); diff --git a/e2e/pulse/legal.spec.ts b/e2e/pulse/legal.spec.ts new file mode 100644 index 0000000..3f67d1b --- /dev/null +++ b/e2e/pulse/legal.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from "@playwright/test"; + +const pages = [ + { name: "Privacy Policy", path: "/privacy-policy" }, + { name: "Terms & Conditions", path: "/terms" }, +]; + +for (const { name, path } of pages) { + test(`A visitor can read ${name} from the homepage footer`, async ({ page }) => { + await page.goto("/"); + const link = page.getByRole("contentinfo").getByRole("link", { name, exact: true }); + await expect(link).toHaveAttribute("href", path); + await link.click(); + await expect(page).toHaveURL(new RegExp(`${path}$`)); + await expect(page.getByRole("heading", { name, exact: true, level: 1 })).toBeVisible(); + await expect(page.getByRole("main").getByRole("heading", { level: 2 }).first()).toBeVisible(); + }); +} diff --git a/e2e/pulse/mobile.spec.ts b/e2e/pulse/mobile.spec.ts new file mode 100644 index 0000000..54117c1 --- /dev/null +++ b/e2e/pulse/mobile.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from "@playwright/test"; + +test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true }); + +test("Mobile visitors can open the menu and reach email sign-in", async ({ page }) => { + const response = await page.goto("/"); + expect(response?.status()).toBe(200); + await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe( + true, + ); + await page.getByRole("button", { name: "Open menu", exact: true }).click(); + const menu = page.getByRole("dialog", { name: "Menu", exact: true }); + await expect(menu).toBeVisible(); + await menu.getByRole("link", { name: "Sign in to Densio", exact: true }).click(); + await expect(page).toHaveURL(/\/auth\/login$/); + await expect(menu).toHaveCount(0); + await expect(page.getByRole("textbox", { name: "Email", exact: true })).toBeEditable(); + await expect(page.getByRole("button", { name: "Continue with email" })).toBeInViewport(); +}); diff --git a/e2e/pulse/onboarding.spec.ts b/e2e/pulse/onboarding.spec.ts new file mode 100644 index 0000000..b554e06 --- /dev/null +++ b/e2e/pulse/onboarding.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from "@playwright/test"; + +test("A visitor can copy the agent onboarding prompt", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const response = await page.goto("/"); + expect(response?.status()).toBe(200); + await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible(); + await page.getByRole("button", { name: "Copy prompt", exact: true }).click(); + await expect(page.getByRole("button", { name: "Copied!", exact: true })).toBeVisible(); + const prompt = await page.evaluate(() => navigator.clipboard.readText()); + expect(prompt).toContain("npx skills add pixel-point/densio --skill densio"); + expect(prompt).toContain("ask me which video to optimize"); + expect(prompt).toContain("save the compressed files in my project"); +}); + +test("Get Started takes a visitor to email sign-in", async ({ page }) => { + await page.goto("/"); + await page.getByRole("link", { name: "Get Started", exact: true }).click(); + await expect(page).toHaveURL(/\/auth\/login$/); + await expect(page.getByRole("heading", { name: "Log in to Densio" })).toBeVisible(); + await expect(page.getByRole("textbox", { name: "Email", exact: true })).toBeEditable(); + await expect(page.getByRole("button", { name: "Continue with email" })).toBeEnabled(); + await page.getByRole("link", { name: "Densio home", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Agent first video optimization" })).toBeVisible(); +}); diff --git a/e2e/pulse/tsconfig.json b/e2e/pulse/tsconfig.json new file mode 100644 index 0000000..b4eca55 --- /dev/null +++ b/e2e/pulse/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "lib": ["ES2024", "DOM"], "types": ["node"] }, + "include": ["*.ts", "../../siteos.playwright.config.ts"] +} diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index f3e823b..3510e68 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "types": ["node", "vitest/globals"] }, - "include": ["**/*.ts", "../apps/api/src/markdown.d.ts"] + "include": ["**/*.ts", "../apps/api/src/markdown.d.ts"], + "exclude": ["pulse"] } diff --git a/package.json b/package.json index 8e2115e..9af9066 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:media": "turbo run test:media" }, "devDependencies": { + "@playwright/test": "1.63.0", "@types/archiver": "^8.0.0", "@types/node": "^22.18.0", "@typescript/native-preview": "^7.0.0-dev.20260703.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dabe1c..e684097 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@playwright/test': + specifier: 1.63.0 + version: 1.63.0 '@types/archiver': specifier: ^8.0.0 version: 8.0.0 @@ -190,7 +193,7 @@ importers: version: 8.6.0(react@19.2.4) geist: specifier: 1.7.2 - version: 1.7.2(next@16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 1.7.2(next@16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) github-slugger: specifier: ^2.0.0 version: 2.0.0 @@ -211,10 +214,10 @@ importers: version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: 16.3.4 - version: 16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 4.2.3(next@16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: specifier: 19.2.4 version: 19.2.4 @@ -387,7 +390,7 @@ importers: devDependencies: '@react-email/ui': specifier: 6.9.3 - version: 6.9.3(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 6.9.3(@playwright/test@1.63.0)(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -1662,6 +1665,11 @@ packages: cpu: [x64] os: [win32] + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + '@radix-ui/number@1.1.3': resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} @@ -4081,6 +4089,16 @@ packages: resolution: {integrity: sha512-0Z++uU8mvB0EvCPAEUq9BGiPcSravzJ+RPdXfjYlzXffqsogisiKRQqI6ctrSSJ6n985vxrgKtQ5n4sRINcJZg==} engines: {node: '>=18.0.0'} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + postal-mime@2.7.4: resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} @@ -5726,6 +5744,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.72.0': optional: true + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + '@radix-ui/number@1.1.3': {} '@radix-ui/primitive@1.1.7': {} @@ -6125,10 +6147,10 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@react-email/ui@6.9.3(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@react-email/ui@6.9.3(@playwright/test@1.63.0)(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: esbuild: 0.28.1 - next: 16.3.0(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.0(@playwright/test@1.63.0)(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@babel/core' - '@opentelemetry/api' @@ -7204,9 +7226,9 @@ snapshots: fsevents@2.3.3: optional: true - geist@1.7.2(next@16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + geist@1.7.2(next@16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: - next: 16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) get-nonce@1.0.1: {} @@ -8024,15 +8046,15 @@ snapshots: negotiator@0.6.3: {} - next-sitemap@4.2.3(next@16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + next-sitemap@4.2.3(next@16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - next@16.3.0(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.0(@playwright/test@1.63.0)(@types/node@22.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@next/env': 16.3.0 '@swc/helpers': 0.5.15 @@ -8051,13 +8073,14 @@ snapshots: '@next/swc-linux-x64-musl': 16.3.0 '@next/swc-win32-arm64-msvc': 16.3.0 '@next/swc-win32-x64-msvc': 16.3.0 + '@playwright/test': 1.63.0 sharp: 0.35.4(@types/node@22.18.0) transitivePeerDependencies: - '@babel/core' - '@types/node' - babel-plugin-macros - next@16.3.4(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.3.4(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.3.4 '@swc/helpers': 0.5.23 @@ -8076,6 +8099,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.3.4 '@next/swc-win32-arm64-msvc': 16.3.4 '@next/swc-win32-x64-msvc': 16.3.4 + '@playwright/test': 1.63.0 sharp: 0.35.4(@types/node@20.19.43) transitivePeerDependencies: - '@babel/core' @@ -8221,6 +8245,12 @@ snapshots: picospinner@3.1.2: {} + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + postal-mime@2.7.4: {} postcss-nesting@13.0.2(postcss@8.5.16): diff --git a/siteos.config.json b/siteos.config.json new file mode 100644 index 0000000..42daad6 --- /dev/null +++ b/siteos.config.json @@ -0,0 +1,54 @@ +{ + "version": 2, + "project": { + "slug": "densio", + "name": "Densio", + "baseUrl": "https://densio.sh", + "monitoringEnabled": true + }, + "testsDir": "e2e/pulse", + "playwrightConfig": "siteos.playwright.config.ts", + "checks": [ + { + "slug": "onboarding", + "name": "Agent onboarding", + "include": ["e2e/pulse/onboarding.spec.ts"], + "active": true, + "schedule": { + "mode": "scheduled", + "cadenceSeconds": 900 + } + }, + { + "slug": "authentication", + "name": "Sign-in and account entry", + "include": ["e2e/pulse/authentication.spec.ts"], + "active": true, + "schedule": { "mode": "scheduled", "cadenceSeconds": 900 } + }, + { + "slug": "agent-service", + "name": "Agent API and runtime skill", + "include": ["e2e/pulse/agent-service.spec.ts"], + "active": true, + "schedule": { "mode": "scheduled", "cadenceSeconds": 900 } + }, + { + "slug": "mobile", + "name": "Mobile sign-in journey", + "include": ["e2e/pulse/mobile.spec.ts"], + "active": true, + "schedule": { "mode": "scheduled", "cadenceSeconds": 3600 } + }, + { + "slug": "legal", + "name": "Legal page navigation", + "include": ["e2e/pulse/legal.spec.ts"], + "active": true, + "schedule": { + "mode": "scheduled", + "cadenceSeconds": 86400 + } + } + ] +} diff --git a/siteos.playwright.config.ts b/siteos.playwright.config.ts new file mode 100644 index 0000000..45a8349 --- /dev/null +++ b/siteos.playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e/pulse", + fullyParallel: false, + forbidOnly: true, + workers: 1, + retries: 0, + timeout: 30_000, + expect: { timeout: 10_000 }, + outputDir: ".siteos/pulse/test-results", + reporter: "list", + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "https://densio.sh", + actionTimeout: 10_000, + navigationTimeout: 20_000, + serviceWorkers: "block", + screenshot: "only-on-failure", + trace: "retain-on-failure", + video: "retain-on-failure", + }, +});