From 4afdb155acdbb1962e4b8859053c29ea5e743e44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:23:43 +0000 Subject: [PATCH 1/2] feat: add Playwright smoke tests for test studio Add Playwright smoke tests that verify: - Studio loads and shows workspace selector - Kitchen-sink workspace loads successfully Includes: - playwright.config.ts with staging auth via localStorage - globalSetup.ts to wait for dev server readiness - GitHub Actions workflow running on PRs and main pushes - Playwright browser caching for faster CI runs Agent-Logs-Url: https://github.com/sanity-io/plugins/sessions/a4e0185a-97da-4d5f-9ebc-da3ec5d0bcee --- .github/workflows/playwright.yml | 57 +++++++++++++++++++++ .gitignore | 5 ++ dev/test-studio/e2e/globalSetup.ts | 25 ++++++++++ dev/test-studio/e2e/playwright.config.ts | 63 ++++++++++++++++++++++++ dev/test-studio/e2e/smoke.spec.ts | 17 +++++++ dev/test-studio/package.json | 1 + dev/test-studio/tsconfig.json | 2 +- pnpm-lock.yaml | 38 ++++++++++++++ 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 dev/test-studio/e2e/globalSetup.ts create mode 100644 dev/test-studio/e2e/playwright.config.ts create mode 100644 dev/test-studio/e2e/smoke.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..2982e6dba --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,57 @@ +name: Playwright Smoke Tests + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + smoke-test: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + SANITY_INTERNAL_ENV: staging + SANITY_E2E_SESSION_TOKEN: ${{ secrets.SANITY_E2E_SESSION_TOKEN }} + SANITY_E2E_PROJECT_ID: ${{ vars.SANITY_E2E_PROJECT_ID }} + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup + + - name: Build plugins + run: pnpm turbo run build --filter=test-studio^... + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('dev/test-studio/package.json') }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm --filter test-studio exec playwright install --with-deps chromium + + - name: Install Playwright system dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: pnpm --filter test-studio exec playwright install-deps chromium + + - name: Run smoke tests + run: pnpm --filter test-studio exec playwright test --config e2e/playwright.config.ts + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: dev/test-studio/e2e/results/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 71b4e6a93..e4d08a557 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,8 @@ vite.config.ts.timestamp-* # Sanity Studio .sanity + +# Playwright +**/e2e/results/ +**/playwright-report/ +**/blob-report/ diff --git a/dev/test-studio/e2e/globalSetup.ts b/dev/test-studio/e2e/globalSetup.ts new file mode 100644 index 000000000..7455ca1a9 --- /dev/null +++ b/dev/test-studio/e2e/globalSetup.ts @@ -0,0 +1,25 @@ +import {chromium, type FullConfig} from '@playwright/test' + +const INIT_TIMEOUT_MS = 120_000 + +/** + * Global setup for smoke tests. + * + * Because the development server can be ready to receive requests but has not + * precompiled javascript, we wait here until the initial bundle is ready. + * This prevents each test from having to deal with the very different timeouts + * for the first and subsequent requests. + */ +export default async function globalSetup(config: FullConfig): Promise { + const {baseURL = 'http://localhost:3333', contextOptions} = config.projects[0].use + const browser = await chromium.launch() + const context = await browser.newContext(contextOptions) + const page = await context.newPage() + + await Promise.all([ + page.waitForResponse('*/**/users/me*', {timeout: INIT_TIMEOUT_MS}), + page.goto(baseURL, {timeout: INIT_TIMEOUT_MS}), + ]) + + await browser.close() +} diff --git a/dev/test-studio/e2e/playwright.config.ts b/dev/test-studio/e2e/playwright.config.ts new file mode 100644 index 000000000..9781ff4cf --- /dev/null +++ b/dev/test-studio/e2e/playwright.config.ts @@ -0,0 +1,63 @@ +import {defineConfig, devices} from '@playwright/test' + +const CI = process.env.CI === 'true' +const BASE_URL = process.env.SANITY_E2E_BASE_URL || 'http://localhost:3333' +const PROJECT_ID = process.env.SANITY_E2E_PROJECT_ID || 'ppsg7ml5' +const TOKEN = process.env.SANITY_E2E_SESSION_TOKEN || '' + +export default defineConfig({ + globalSetup: './globalSetup', + testDir: '.', + testMatch: '**/*.spec.ts', + timeout: 60_000, + expect: {timeout: 30_000}, + fullyParallel: true, + retries: CI ? 2 : 0, + reporter: CI ? [['list'], ['blob']] : [['list'], ['html', {open: 'never'}]], + outputDir: './results', + + use: { + baseURL: BASE_URL, + actionTimeout: 10_000, + trace: 'on-first-retry', + video: 'retain-on-failure', + viewport: {width: 1728, height: 1000}, + headless: true, + contextOptions: {reducedMotion: 'reduce'}, + storageState: { + cookies: [], + origins: [ + { + origin: BASE_URL, + localStorage: [ + { + name: `__studio_auth_token_${PROJECT_ID}`, + value: JSON.stringify({ + token: TOKEN, + time: new Date().toISOString(), + }), + }, + ], + }, + ], + }, + }, + + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']}, + }, + ], + + webServer: { + command: CI ? 'pnpm sanity start' : 'pnpm sanity dev', + port: 3333, + reuseExistingServer: !CI, + stdout: 'pipe', + timeout: 120_000, + env: { + SANITY_INTERNAL_ENV: process.env.SANITY_INTERNAL_ENV || 'staging', + }, + }, +}) diff --git a/dev/test-studio/e2e/smoke.spec.ts b/dev/test-studio/e2e/smoke.spec.ts new file mode 100644 index 000000000..9d26e0427 --- /dev/null +++ b/dev/test-studio/e2e/smoke.spec.ts @@ -0,0 +1,17 @@ +import {expect, test} from '@playwright/test' + +test.describe('Studio smoke test', () => { + test('studio loads the workspace selector', async ({page}) => { + await page.goto('/') + // Wait for the studio to fully render – workspace selector shows workspace links + await expect(page.getByRole('link', {name: /kitchen-sink/i})).toBeVisible() + }) + + test('kitchen-sink workspace loads', async ({page}) => { + await page.goto('/kitchen-sink') + // Wait for the studio navbar to appear, which indicates the workspace has loaded + await expect(page.getByRole('navigation')).toBeVisible() + // The studio should show the project's main UI (structure tool is the default) + await expect(page.getByRole('link', {name: /kitchen-sink/i})).toBeVisible() + }) +}) diff --git a/dev/test-studio/package.json b/dev/test-studio/package.json index 1a4af81f0..8f5fb7fa8 100644 --- a/dev/test-studio/package.json +++ b/dev/test-studio/package.json @@ -42,6 +42,7 @@ "styled-components": "catalog:" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@tsconfig/vite-react": "^7.0.2", "@types/node": "catalog:", "@types/react": "catalog:", diff --git a/dev/test-studio/tsconfig.json b/dev/test-studio/tsconfig.json index ed6babf96..52a979731 100644 --- a/dev/test-studio/tsconfig.json +++ b/dev/test-studio/tsconfig.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/vite-react/tsconfig.json", - "include": ["src/**/*", "migrations/**/*.ts", "sanity.cli.ts", "sanity.config.ts"], + "include": ["src/**/*", "migrations/**/*.ts", "sanity.cli.ts", "sanity.config.ts", "e2e/**/*"], "compilerOptions": { "customConditions": ["development"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 340f5881d..6ff6a1b49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: specifier: npm:@sanity/styled-components@latest version: '@sanity/styled-components@6.1.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)' devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 '@tsconfig/vite-react': specifier: ^7.0.2 version: 7.0.2 @@ -3290,6 +3293,11 @@ packages: cpu: [x64] os: [win32] + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -5729,6 +5737,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -7179,6 +7192,16 @@ packages: player.style@0.3.1: resolution: {integrity: sha512-z/T8hJGaTkHT9vdXgWdOgF37eB1FV7/j52VXQZ2lgEhpru9oT8TaUWIxp6GoxTnhPBM4X6nSbpkAHrT7UTjUKg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + pluralize-esm@9.0.5: resolution: {integrity: sha512-Kb2dcpMsIutFw2hYrN0EhsAXOUJTd6FVMIxvNAkZCMQLVt9NGZqQczvGpYDxNWCZeCWLHUPxQIBudWzt1h7VVA==} engines: {node: '>=14.0.0'} @@ -11198,6 +11221,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.56.0': optional: true + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -14440,6 +14467,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -15966,6 +15996,14 @@ snapshots: transitivePeerDependencies: - react + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize-esm@9.0.5: {} polished@4.3.1: From f33933de2ccf3174b05dd06bed4c4b167d6f327e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:10:00 +0000 Subject: [PATCH 2/2] fix: set webServer cwd and build test-studio for Playwright CI - Add `cwd: '..'` to webServer so `sanity start` runs from the studio root instead of the e2e/ subdirectory - Change build filter from `test-studio^...` to `test-studio...` so the studio itself is built (required for `sanity start`) Agent-Logs-Url: https://github.com/sanity-io/plugins/sessions/84fa7c37-c520-4dc6-af75-6fe8b2e5d9d5 --- .github/workflows/playwright.yml | 6 ++++-- dev/test-studio/e2e/playwright.config.ts | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 2982e6dba..2252d5b1c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -27,8 +27,10 @@ jobs: - uses: ./.github/actions/setup - - name: Build plugins - run: pnpm turbo run build --filter=test-studio^... + - name: Build plugins and test studio + # Uses test-studio... (not test-studio^...) to include test-studio itself in the build, + # which is required for `sanity start` to serve the pre-built studio. + run: pnpm turbo run build --filter=test-studio... - name: Cache Playwright browsers id: playwright-cache diff --git a/dev/test-studio/e2e/playwright.config.ts b/dev/test-studio/e2e/playwright.config.ts index 9781ff4cf..e2194f336 100644 --- a/dev/test-studio/e2e/playwright.config.ts +++ b/dev/test-studio/e2e/playwright.config.ts @@ -52,6 +52,8 @@ export default defineConfig({ webServer: { command: CI ? 'pnpm sanity start' : 'pnpm sanity dev', + // The config lives in e2e/ but sanity CLI must run from the studio root (dev/test-studio/) + cwd: '..', port: 3333, reuseExistingServer: !CI, stdout: 'pipe',