From a3b0df602a28034c1f7f68f77f691a433ec88b89 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 10:18:45 +0200 Subject: [PATCH 1/9] added playwright fixture to reset db Signed-off-by: Benjamin Strasser --- e2e/specs/fixtures.ts | 16 ++++++++++++++++ e2e/specs/new-user-flow.spec.ts | 3 ++- package.json | 1 + .../src/kysely/migrations/2024-04-28T09_init.ts | 14 ++++++++------ services/src/kysely/migrator.ts | 12 ++++++++++++ 5 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 e2e/specs/fixtures.ts diff --git a/e2e/specs/fixtures.ts b/e2e/specs/fixtures.ts new file mode 100644 index 00000000..2cd61045 --- /dev/null +++ b/e2e/specs/fixtures.ts @@ -0,0 +1,16 @@ +import { test as base } from '@playwright/test' +import { execSync } from 'child_process' + +const test = base.extend({ + page: async ({ page }, use) => { + // Run the migrate:reset script to clean up the database + execSync('pnpm run migrate:reset', { stdio: 'inherit' }) + + // Run the migrate:latest script to set up the database + execSync('pnpm run migrate:latest', { stdio: 'inherit' }) + + await use(page) + } +}) + +export default test diff --git a/e2e/specs/new-user-flow.spec.ts b/e2e/specs/new-user-flow.spec.ts index 6880b810..acb05d51 100644 --- a/e2e/specs/new-user-flow.spec.ts +++ b/e2e/specs/new-user-flow.spec.ts @@ -1,4 +1,5 @@ -import test, { expect } from '@playwright/test' +import { expect } from '@playwright/test' +import test from './fixtures' test.describe('registration process', { tag: ['@foo-bar'] }, () => { const testEmail = 'foo@bar.com' diff --git a/package.json b/package.json index 376bd0a5..7a0ef0da 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "migrate:latest": "pnpm run migrate -- latest", "migrate:up": "pnpm run migrate -- up", "migrate:down": "pnpm run migrate -- down", + "migrate:reset": "pnpm run migrate -- reset", "sync:db": "cross-env DATABASE_URL=db.sqlite kysely-codegen", "---- DOCS ----------------------------------------------------------": "", "docs:dev": "vitepress dev docs", diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index a8b623d2..a03f5d57 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -53,10 +53,12 @@ export async function up(db: Kysely): Promise { } export async function down(db: Kysely): Promise { - await db.schema.dropTable('users').execute() - await db.schema.dropTable('projects_users').execute() - await db.schema.dropTable('translations').execute() - await db.schema.dropTable('keys').execute() - await db.schema.dropTable('languages').execute() - await db.schema.dropTable('projects').execute() + await db.transaction().execute(async (tx) => { + await tx.schema.dropTable('users').execute() + await tx.schema.dropTable('projects_users').execute() + await tx.schema.dropTable('translations').execute() + await tx.schema.dropTable('keys').execute() + await tx.schema.dropTable('languages').execute() + await tx.schema.dropTable('projects').execute() + }) } diff --git a/services/src/kysely/migrator.ts b/services/src/kysely/migrator.ts index 735e65f2..ef29b037 100644 --- a/services/src/kysely/migrator.ts +++ b/services/src/kysely/migrator.ts @@ -2,6 +2,7 @@ import { MIGRATION_PROVIDER, getMigrator } from './migrator.util' import * as process from 'node:process' import minimist from 'minimist' import { pick } from 'typesafe-utils' +import { NO_MIGRATIONS } from 'kysely' function highlight(text: string) { return `\x1b[32m${text}\x1b[0m` @@ -73,6 +74,17 @@ async function main() { break } + case 'reset': { + const { results, error } = await migrator.migrateTo(NO_MIGRATIONS) + + if (error) return console.error(error) + if (!results || !results.length) return console.info(highlight('Database is on base version')) + + console.info(`Undid all migrations`) + + break + } + default: { return console.error('Please supply a command') } From b6764697344371197605c3895052b21f201ca3ba Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 13:11:44 +0200 Subject: [PATCH 2/9] added transactional context to up migrations Signed-off-by: Benjamin Strasser --- .../kysely/migrations/2024-04-28T09_init.ts | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index a03f5d57..e8160db9 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -2,54 +2,56 @@ import { Kysely, sql } from 'kysely' import { createTableMigration } from '../migration.util' export async function up(db: Kysely): Promise { - await createTableMigration(db, 'users') - .addColumn('email', 'text', (col) => col.unique().notNull()) - .addColumn('first_name', 'text', (col) => col.notNull()) - .addColumn('last_name', 'text', (col) => col.notNull()) - .addColumn('role', 'text', (col) => col.defaultTo('user').notNull()) - .addColumn('password_hash', 'text', (col) => col.notNull()) - .execute() + await db.transaction().execute(async (tx) => { + await createTableMigration(tx, 'users') + .addColumn('email', 'text', (col) => col.unique().notNull()) + .addColumn('first_name', 'text', (col) => col.notNull()) + .addColumn('last_name', 'text', (col) => col.notNull()) + .addColumn('role', 'text', (col) => col.defaultTo('user').notNull()) + .addColumn('password_hash', 'text', (col) => col.notNull()) + .execute() - await createTableMigration(db, 'projects') - .addColumn('name', 'text', (col) => col.unique().notNull()) - .addColumn('base_language', 'integer', (col) => - col.references('languages.id').onDelete('restrict').notNull() - ) - .execute() + await createTableMigration(tx, 'projects') + .addColumn('name', 'text', (col) => col.unique().notNull()) + .addColumn('base_language', 'integer', (col) => + col.references('languages.id').onDelete('restrict').notNull() + ) + .execute() - await createTableMigration(db, 'languages') - .addColumn('code', 'text', (col) => col.unique().notNull()) - .addColumn('fallback_language', 'integer', (col) => col.references('languages.id')) - .addColumn('project_id', 'integer', (col) => - col.references('project.id').onDelete('cascade').notNull() - ) - .execute() + await createTableMigration(tx, 'languages') + .addColumn('code', 'text', (col) => col.unique().notNull()) + .addColumn('fallback_language', 'integer', (col) => col.references('languages.id')) + .addColumn('project_id', 'integer', (col) => + col.references('project.id').onDelete('cascade').notNull() + ) + .execute() - await createTableMigration(db, 'keys') - .addColumn('project_id', 'integer', (col) => - col.references('projects.id').onDelete('cascade').notNull() - ) - .addColumn('name', 'text', (col) => col.unique().notNull()) - .execute() + await createTableMigration(tx, 'keys') + .addColumn('project_id', 'integer', (col) => + col.references('projects.id').onDelete('cascade').notNull() + ) + .addColumn('name', 'text', (col) => col.unique().notNull()) + .execute() - await createTableMigration(db, 'translations') - .addColumn('key_id', 'integer', (col) => - col.references('keys.id').onDelete('cascade').notNull() - ) - .addColumn('language_id', 'integer', (col) => - col.references('languages.id').onDelete('cascade').notNull() - ) - .addColumn('value', 'text') - .execute() + await createTableMigration(tx, 'translations') + .addColumn('key_id', 'integer', (col) => + col.references('keys.id').onDelete('cascade').notNull() + ) + .addColumn('language_id', 'integer', (col) => + col.references('languages.id').onDelete('cascade').notNull() + ) + .addColumn('value', 'text') + .execute() - await createTableMigration(db, 'projects_users', false, false) - .addColumn('project_id', 'integer', (col) => col.references('projects.id').notNull()) - .addColumn('user_id', 'integer', (col) => col.references('user.id').notNull()) - .addColumn('permission', 'text', (col) => - col.check(sql`permission in ('READONLY', 'WRITE', 'ADMIN')`) - ) - .addPrimaryKeyConstraint('projects_users_pk', ['project_id', 'user_id']) - .execute() + await createTableMigration(tx, 'projects_users', false, false) + .addColumn('project_id', 'integer', (col) => col.references('projects.id').notNull()) + .addColumn('user_id', 'integer', (col) => col.references('user.id').notNull()) + .addColumn('permission', 'text', (col) => + col.check(sql`permission in ('READONLY', 'WRITE', 'ADMIN')`) + ) + .addPrimaryKeyConstraint('projects_users_pk', ['project_id', 'user_id']) + .execute() + }) } export async function down(db: Kysely): Promise { From bf0f4fb4835467c891c962e09eae1e15cc3df5f4 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 14:44:58 +0200 Subject: [PATCH 3/9] added local testing to playwright setup Signed-off-by: Benjamin Strasser --- e2e/specs/new-user-flow.spec.ts | 3 +++ e2e/specs/util.ts | 5 +++++ package.json | 1 + playwright.config.local.ts | 27 +++++++++++++++++++++++++++ src/routes/+layout.svelte | 3 ++- 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 e2e/specs/util.ts create mode 100644 playwright.config.local.ts diff --git a/e2e/specs/new-user-flow.spec.ts b/e2e/specs/new-user-flow.spec.ts index acb05d51..19d99330 100644 --- a/e2e/specs/new-user-flow.spec.ts +++ b/e2e/specs/new-user-flow.spec.ts @@ -1,5 +1,6 @@ import { expect } from '@playwright/test' import test from './fixtures' +import { waitForHydration } from './util' test.describe('registration process', { tag: ['@foo-bar'] }, () => { const testEmail = 'foo@bar.com' @@ -7,6 +8,7 @@ test.describe('registration process', { tag: ['@foo-bar'] }, () => { test('registers foo bar and logs into the app', async ({ page, baseURL }) => { await page.goto(`${baseURL!}/signup`) + await waitForHydration(page) await test.step('sign up a new user', async () => { const firstname = page.getByTestId('signup-firstname-input') @@ -36,6 +38,7 @@ test.describe('registration process', { tag: ['@foo-bar'] }, () => { const termsCheckbox = page.getByTestId('signup-terms-checkbox') await expect(termsCheckbox).toBeVisible() + await termsCheckbox.focus() await termsCheckbox.check() const signUpCta = page.getByTestId('signup-cta') diff --git a/e2e/specs/util.ts b/e2e/specs/util.ts new file mode 100644 index 00000000..d74327ec --- /dev/null +++ b/e2e/specs/util.ts @@ -0,0 +1,5 @@ +import type { Page } from '@playwright/test' + +export async function waitForHydration(page: Page) { + await page.locator('.hydrated').waitFor({ state: 'visible' }) +} diff --git a/package.json b/package.json index 7a0ef0da..a7411baf 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:integration": "cross-env DATABASE_LOCATION=:memory: vitest run --project integration", "test:e2e": "pnpm run build && playwright test", "test:e2e:only": "playwright test", + "test:e2e:local": "playwright test --headed --ui --config ./playwright.config.local.ts", "---- DB ------------------------------------------------------------": "", "setup:db": "pnpm migrate:latest && pnpm sync:db", "migrate": "tsx ./services/src/kysely/migrator.ts", diff --git a/playwright.config.local.ts b/playwright.config.local.ts new file mode 100644 index 00000000..32d4baf8 --- /dev/null +++ b/playwright.config.local.ts @@ -0,0 +1,27 @@ +import { type PlaywrightTestConfig, devices } from '@playwright/test' + +const WEB_SERVER_PORT = 3000 +const config: PlaywrightTestConfig = { + testDir: './e2e/specs', + outputDir: './e2e/results', + projects: [ + { + name: 'Chrome', + testMatch: /.*\.spec\.ts/, + use: { + ...devices['Desktop Chrome'] + } + } + ], + use: { + baseURL: `http://localhost:${WEB_SERVER_PORT}`, + bypassCSP: true + }, + webServer: { + command: 'pnpm run dev', + port: WEB_SERVER_PORT + }, + reporter: [['list']] +} + +export default config diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ef0555e3..dc052a3a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,10 @@ -
+
From 3d64c38620481774ce489862cfc963a157b35a17 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 18:42:08 +0200 Subject: [PATCH 4/9] sped up migrations inside e2e tests Signed-off-by: Benjamin Strasser --- e2e/specs/fixtures.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/specs/fixtures.ts b/e2e/specs/fixtures.ts index 2cd61045..4f55faa4 100644 --- a/e2e/specs/fixtures.ts +++ b/e2e/specs/fixtures.ts @@ -1,13 +1,13 @@ import { test as base } from '@playwright/test' -import { execSync } from 'child_process' +import { migrate, undoMigration } from 'services/kysely/migrator.util' const test = base.extend({ page: async ({ page }, use) => { - // Run the migrate:reset script to clean up the database - execSync('pnpm run migrate:reset', { stdio: 'inherit' }) + // clean up the database + await undoMigration() - // Run the migrate:latest script to set up the database - execSync('pnpm run migrate:latest', { stdio: 'inherit' }) + // set up the database + await migrate() await use(page) } From 7b28bb61b913271ed42f32cc991bd7596337f73b Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 21:04:55 +0200 Subject: [PATCH 5/9] minor changes Signed-off-by: Benjamin Strasser --- playwright.config.local.ts | 3 ++- services/src/kysely/migrator.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/playwright.config.local.ts b/playwright.config.local.ts index 32d4baf8..29a9c722 100644 --- a/playwright.config.local.ts +++ b/playwright.config.local.ts @@ -19,7 +19,8 @@ const config: PlaywrightTestConfig = { }, webServer: { command: 'pnpm run dev', - port: WEB_SERVER_PORT + port: WEB_SERVER_PORT, + reuseExistingServer: !process.env.CI }, reporter: [['list']] } diff --git a/services/src/kysely/migrator.ts b/services/src/kysely/migrator.ts index ef29b037..143beccd 100644 --- a/services/src/kysely/migrator.ts +++ b/services/src/kysely/migrator.ts @@ -8,6 +8,10 @@ function highlight(text: string) { return `\x1b[32m${text}\x1b[0m` } +function highlightRed(text: string) { + return `\x1b[31m${text}\x1b[0m` +} + const consoleWithPrefix = (prefix: string): Console => new Proxy(global.console, { get(target: Target, prop: T) { @@ -68,7 +72,7 @@ async function main() { if (!results || !results.length) return console.info(highlight('Database is on base version')) console.info( - `Migrated to the previous version (DOWN)\n${highlight(results.map(pick('migrationName')).join('\n'))}` + `Migrated to the previous version (DOWN)\n${highlightRed(results.map(pick('migrationName')).join('\n'))}` ) break @@ -81,6 +85,9 @@ async function main() { if (!results || !results.length) return console.info(highlight('Database is on base version')) console.info(`Undid all migrations`) + for (const result of results) { + console.info(`${highlightRed(result.migrationName)}`) + } break } From 49c4b340322fad81b4f7132bf117796d5b92c1b0 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 17 Jun 2024 23:10:09 +0200 Subject: [PATCH 6/9] added programatic way to setup user and barebone projects e2e test Signed-off-by: Benjamin Strasser --- e2e/specs/create-project-flow.spec.ts | 7 ++++++ e2e/specs/fixtures.ts | 11 ++++++-- e2e/specs/new-user-flow.spec.ts | 2 +- e2e/specs/util.ts | 36 ++++++++++++++++++++++++++- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 e2e/specs/create-project-flow.spec.ts diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts new file mode 100644 index 00000000..3c037e63 --- /dev/null +++ b/e2e/specs/create-project-flow.spec.ts @@ -0,0 +1,7 @@ +import { testWithUser as test } from './fixtures' + +test.describe('registration process', { tag: ['@foo-bar'] }, () => { + test('projects', async ({ page }) => { + await page.goto('/projects') + }) +}) diff --git a/e2e/specs/fixtures.ts b/e2e/specs/fixtures.ts index 4f55faa4..2608726e 100644 --- a/e2e/specs/fixtures.ts +++ b/e2e/specs/fixtures.ts @@ -1,7 +1,8 @@ import { test as base } from '@playwright/test' import { migrate, undoMigration } from 'services/kysely/migrator.util' +import { setupUser } from './util' -const test = base.extend({ +export const test = base.extend({ page: async ({ page }, use) => { // clean up the database await undoMigration() @@ -13,4 +14,10 @@ const test = base.extend({ } }) -export default test +export const testWithUser = test.extend({ + page: async ({ page }, use) => { + await setupUser(page.request) + + await use(page) + } +}) diff --git a/e2e/specs/new-user-flow.spec.ts b/e2e/specs/new-user-flow.spec.ts index 19d99330..7ca9b85a 100644 --- a/e2e/specs/new-user-flow.spec.ts +++ b/e2e/specs/new-user-flow.spec.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test' -import test from './fixtures' +import { test } from './fixtures' import { waitForHydration } from './util' test.describe('registration process', { tag: ['@foo-bar'] }, () => { diff --git a/e2e/specs/util.ts b/e2e/specs/util.ts index d74327ec..0bd63ff0 100644 --- a/e2e/specs/util.ts +++ b/e2e/specs/util.ts @@ -1,5 +1,39 @@ -import type { Page } from '@playwright/test' +import { type APIRequestContext, type Page, expect } from '@playwright/test' export async function waitForHydration(page: Page) { await page.locator('.hydrated').waitFor({ state: 'visible' }) } + +export async function setupUser(request: APIRequestContext) { + await register(request) + await login(request) +} + +export async function register(request: APIRequestContext) { + const signup = await request.post('/signup', { + headers: { + origin: 'http://localhost:3000' + }, + form: { + first_name: 'test', + last_name: 'test', + email: 'test@test.com', + password: 'password', + confirmPassword: 'password', + termsOfService: 'true' + } + }) + + expect(signup.ok()).toBeTruthy() +} + +export async function login(request: APIRequestContext) { + const login = await request.post('/login', { + headers: { + origin: 'http://localhost:3000' + }, + form: { email: 'test@test.com', password: 'password' } + }) + + expect(login.ok()).toBeTruthy() +} From 25850d06cae98a28305b01b9a1888eace179b99b Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Tue, 18 Jun 2024 21:35:46 +0200 Subject: [PATCH 7/9] renamed projects test Signed-off-by: Benjamin Strasser --- e2e/specs/create-project-flow.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts index 3c037e63..e3790e81 100644 --- a/e2e/specs/create-project-flow.spec.ts +++ b/e2e/specs/create-project-flow.spec.ts @@ -1,6 +1,6 @@ import { testWithUser as test } from './fixtures' -test.describe('registration process', { tag: ['@foo-bar'] }, () => { +test.describe('create project', { tag: ['@foo-bar'] }, () => { test('projects', async ({ page }) => { await page.goto('/projects') }) From 9dd43127b2ce91f7584d10a2eac10e579e3e441b Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Thu, 27 Jun 2024 23:26:03 +0200 Subject: [PATCH 8/9] added support for e2e database and added feedback for minor stuff Signed-off-by: Benjamin Strasser --- .gitignore | 2 +- e2e/specs/create-project-flow.spec.ts | 1 + package.json | 3 ++- playwright.config.local.ts | 5 ++--- src/routes/+layout.svelte | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1d0c62d4..e209d47e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ npm-debug.log* /playwright/.cache/ /playwright-report -db.sqlite +db*.sqlite /build /.svelte-kit diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts index e3790e81..4978a86e 100644 --- a/e2e/specs/create-project-flow.spec.ts +++ b/e2e/specs/create-project-flow.spec.ts @@ -3,5 +3,6 @@ import { testWithUser as test } from './fixtures' test.describe('create project', { tag: ['@foo-bar'] }, () => { test('projects', async ({ page }) => { await page.goto('/projects') + // TODO add test }) }) diff --git a/package.json b/package.json index a7411baf..9b96a171 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "pnpm run setup:db && vite dev", + "dev:e2e": "cross-env-shell DATABASE_LOCATION=dbe2e.sqlite \"pnpm run setup:db && vite dev\"", "build": "vite build", "preview": "pnpm run setup:db && vite preview", "sync:svelte": "svelte-kit sync", @@ -17,7 +18,7 @@ "test:integration": "cross-env DATABASE_LOCATION=:memory: vitest run --project integration", "test:e2e": "pnpm run build && playwright test", "test:e2e:only": "playwright test", - "test:e2e:local": "playwright test --headed --ui --config ./playwright.config.local.ts", + "test:e2e:local": "cross-env DATABASE_LOCATION=dbe2e.sqlite playwright test --headed --ui --config ./playwright.config.local.ts", "---- DB ------------------------------------------------------------": "", "setup:db": "pnpm migrate:latest && pnpm sync:db", "migrate": "tsx ./services/src/kysely/migrator.ts", diff --git a/playwright.config.local.ts b/playwright.config.local.ts index 29a9c722..423c30cf 100644 --- a/playwright.config.local.ts +++ b/playwright.config.local.ts @@ -18,9 +18,8 @@ const config: PlaywrightTestConfig = { bypassCSP: true }, webServer: { - command: 'pnpm run dev', - port: WEB_SERVER_PORT, - reuseExistingServer: !process.env.CI + command: 'pnpm run dev:e2e', + port: WEB_SERVER_PORT }, reporter: [['list']] } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dc052a3a..b3b38ee2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,7 +4,7 @@ import { browser } from '$app/environment' -
+
From 06882c0d995fa8650d97cb2df8d89017b4680ea9 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Thu, 27 Jun 2024 23:49:14 +0200 Subject: [PATCH 9/9] synced versions of playwright inside github actions Signed-off-by: Benjamin Strasser --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2ab003fb..8fe322e1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,7 +57,7 @@ jobs: run: pnpm test - name: Install Playwright Browsers - run: pnpx playwright install --with-deps chromium + run: pnpm exec playwright install --with-deps chromium - name: Run Playwright tests run: pnpm test:e2e:only