Merge pull request 'chore(test-infra): TEST-INFRA-001 — Playwright + visual regression + real-backend e2e foundation' (#21) from chore/test-infra-001 into main
Reviewed-on: bert.hausmans/crewli#21
This commit was merged in pull request #21.
This commit is contained in:
@@ -16,6 +16,7 @@ dev-docs/ARCH-API-VALIDATION.md
|
||||
dev-docs/RFC-WS-7-OBSERVABILITY.md
|
||||
dev-docs/GLITCHTIP.md
|
||||
dev-docs/ARCH-OBSERVABILITY.md
|
||||
dev-docs/ARCH-TESTING.md
|
||||
dev-docs/runbooks/observability-triage.md
|
||||
dev-docs/runbooks/observability-erasure.md
|
||||
dev-docs/RFC-WS-6.md
|
||||
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
apps/app/tests/playwright-ct/**/__screenshots__/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
apps/app/tests/playwright-e2e/**/__screenshots__/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -39,6 +39,18 @@ storage/framework/views/*
|
||||
.phpunit.result.cache
|
||||
coverage/
|
||||
|
||||
# Playwright runtime artifacts (test-results, blob-report, html-report,
|
||||
# .cache build dir, playwright traces). __screenshots__/ is committed
|
||||
# (via Git LFS, see .gitattributes).
|
||||
apps/app/test-results/
|
||||
apps/app/playwright-report/
|
||||
apps/app/blob-report/
|
||||
apps/app/playwright/.cache/
|
||||
|
||||
# Playwright e2e seed-data fixture file — written by E2EBaselineSeeder
|
||||
# during globalSetup, never source-of-truth.
|
||||
api/storage/app/e2e-fixtures.json
|
||||
|
||||
# Misc
|
||||
*.pem
|
||||
.cache/
|
||||
|
||||
@@ -160,6 +160,10 @@ PR as the migration.
|
||||
- Use factories for all test data
|
||||
- After each module: `php artisan test --filter=ModuleName`
|
||||
|
||||
Frontend test architecture (5 tiers: Unit / Component / Integration /
|
||||
Visual / E2E) is documented in `dev-docs/ARCH-TESTING.md`. Choose the
|
||||
right tier per the decision tree there before adding new tests.
|
||||
|
||||
## Frontend rules (strict)
|
||||
|
||||
### Vuexy reference source (mandatory)
|
||||
|
||||
112
api/database/seeders/E2EBaselineSeeder.php
Normal file
112
api/database/seeders/E2EBaselineSeeder.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Artist;
|
||||
use App\Models\ArtistEngagement;
|
||||
use App\Models\Event;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Performance;
|
||||
use App\Models\Stage;
|
||||
use App\Models\StageDay;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
/**
|
||||
* Seeds a deterministic baseline for Playwright e2e tests.
|
||||
*
|
||||
* Creates:
|
||||
* - Roles (org_admin etc. via RoleSeeder)
|
||||
* - One user: e2e@test.local / password ("password")
|
||||
* - One organisation, attached as org_admin
|
||||
* - One event spanning today..+30d
|
||||
* - One stage with one StageDay
|
||||
* - One artist + engagement + performance (version=0)
|
||||
*
|
||||
* Used by tests/playwright-e2e/. Idempotency: this seeder assumes a
|
||||
* `migrate:fresh` was just run, so it creates without checking for
|
||||
* existing rows. Re-running on a non-empty DB would create duplicates.
|
||||
*
|
||||
* NOT used by PHPUnit — PHPUnit uses factories per test class with
|
||||
* RefreshDatabase. This is e2e-specific.
|
||||
*/
|
||||
final class E2EBaselineSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(RoleSeeder::class);
|
||||
|
||||
$org = Organisation::factory()->create([
|
||||
'name' => 'E2E Test Organisation',
|
||||
]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'email' => 'e2e@test.local',
|
||||
'password' => Hash::make('password'),
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$org->users()->attach($user, ['role' => 'org_admin']);
|
||||
|
||||
$event = Event::factory()->create([
|
||||
'organisation_id' => $org->id,
|
||||
'name' => 'E2E Test Festival',
|
||||
'start_date' => CarbonImmutable::now()->subDay(),
|
||||
'end_date' => CarbonImmutable::now()->addDays(30),
|
||||
]);
|
||||
|
||||
$stage = Stage::factory()->create([
|
||||
'event_id' => $event->id,
|
||||
'name' => 'E2E Stage',
|
||||
]);
|
||||
|
||||
StageDay::query()->create([
|
||||
'stage_id' => $stage->id,
|
||||
'event_id' => $event->id,
|
||||
]);
|
||||
|
||||
$artist = Artist::factory()->create([
|
||||
'organisation_id' => $org->id,
|
||||
'name' => 'E2E Artist',
|
||||
]);
|
||||
|
||||
$engagement = ArtistEngagement::factory()->create([
|
||||
'artist_id' => $artist->id,
|
||||
'event_id' => $event->id,
|
||||
]);
|
||||
|
||||
$start = CarbonImmutable::now()->addDays(2)->setTime(20, 0);
|
||||
Performance::factory()->create([
|
||||
'engagement_id' => $engagement->id,
|
||||
'event_id' => $event->id,
|
||||
'stage_id' => $stage->id,
|
||||
'lane' => 0,
|
||||
'start_at' => $start,
|
||||
'end_at' => $start->addHour(),
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
$performance = Performance::query()
|
||||
->where('event_id', $event->id)
|
||||
->where('stage_id', $stage->id)
|
||||
->first();
|
||||
|
||||
// Write seeded IDs to a known location the Playwright e2e
|
||||
// fixture reads. Avoids artisan-stdout-parsing fragility.
|
||||
$fixturePath = storage_path('app/e2e-fixtures.json');
|
||||
@mkdir(dirname($fixturePath), 0755, true);
|
||||
file_put_contents($fixturePath, json_encode([
|
||||
'user_email' => 'e2e@test.local',
|
||||
'user_password' => 'password',
|
||||
'organisation_id' => $org->id,
|
||||
'event_id' => $event->id,
|
||||
'stage_id' => $stage->id,
|
||||
'performance_id' => $performance?->id,
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
$this->command?->info("E2E fixtures written to {$fixturePath}");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,11 @@
|
||||
"msw:init": "msw init public/ --save",
|
||||
"postinstall": "npm run build:icons && npm run msw:init",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"test:component": "playwright test --config=playwright-ct.config.ts",
|
||||
"test:e2e": "playwright test --config=playwright.config.ts",
|
||||
"test:visual": "playwright test --config=playwright-ct.config.ts --grep @visual",
|
||||
"test:visual:update": "playwright test --config=playwright-ct.config.ts --grep @visual --update-snapshots"
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "6.7.3",
|
||||
@@ -67,6 +71,7 @@
|
||||
"@antfu/eslint-config-ts": "0.43.1",
|
||||
"@antfu/eslint-config-vue": "0.43.1",
|
||||
"@antfu/utils": "0.7.10",
|
||||
"@axe-core/playwright": "^4.11.3",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
@@ -82,6 +87,8 @@
|
||||
"@iconify/vue": "4.1.2",
|
||||
"@intlify/unplugin-vue-i18n": "11.0.1",
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@playwright/experimental-ct-vue": "^1.59.1",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@stylistic/eslint-plugin-js": "0.0.4",
|
||||
"@stylistic/eslint-plugin-ts": "0.0.4",
|
||||
"@stylistic/stylelint-config": "1.0.1",
|
||||
@@ -101,6 +108,7 @@
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "5.1.1",
|
||||
"@vue/compiler-dom": "^3.5.34",
|
||||
"@vue/test-utils": "^2.4.9",
|
||||
"axe-core": "^4.11.4",
|
||||
"baseline-browser-mapping": "^2.10.16",
|
||||
|
||||
78
apps/app/playwright-ct.config.ts
Normal file
78
apps/app/playwright-ct.config.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, devices } from '@playwright/experimental-ct-vue'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// Component-test config — mounts individual components in a real
|
||||
// Chromium via Playwright Component Testing. Used by:
|
||||
// pnpm test:component (all CT tests, baselines verified)
|
||||
// pnpm test:visual (subset tagged @visual)
|
||||
// pnpm test:visual:update (regenerate visual baselines)
|
||||
//
|
||||
// E2E (real backend + real frontend) lives in playwright.config.ts.
|
||||
|
||||
const sharedAliases = {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@core': fileURLToPath(new URL('./src/@core', import.meta.url)),
|
||||
'@layouts': fileURLToPath(new URL('./src/@layouts', import.meta.url)),
|
||||
'@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)),
|
||||
'@styles': fileURLToPath(new URL('./src/assets/styles/', import.meta.url)),
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/playwright-ct',
|
||||
testMatch: /.*\.spec\.ts$/,
|
||||
snapshotDir: './tests/playwright-ct/__screenshots__',
|
||||
snapshotPathTemplate: '{snapshotDir}/{testFilePath}/{arg}{ext}',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
|
||||
use: {
|
||||
trace: 'off',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
ctPort: 3100,
|
||||
ctViteConfig: {
|
||||
plugins: [vue()],
|
||||
resolve: { alias: sharedAliases },
|
||||
|
||||
// Vuetify ships ESM that Vite needs to inline-process; matches
|
||||
// the convention from vitest.config.ts's component project.
|
||||
ssr: { noExternal: ['vuetify'] },
|
||||
},
|
||||
},
|
||||
|
||||
// Linux-Chromium-only baselines per RFC §A.5 — Firefox/WebKit and
|
||||
// mobile/responsive viewports deferred to a later sprint. Running
|
||||
// baselines on macOS (dev) vs Linux (CI) will produce a 1-2px
|
||||
// anti-alias diff; pixel tolerance below absorbs that, but the
|
||||
// canonical baseline IS the dev machine for now (no CI yet — see
|
||||
// BACKLOG TEST-INFRA-002).
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.001,
|
||||
threshold: 0.2,
|
||||
},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
// Auto-start the prototype static server for visual baselines.
|
||||
// Tests under tests/playwright-ct/visual/ navigate to this URL via
|
||||
// page.goto() rather than calling mount(); they coexist with normal
|
||||
// CT tests in the same runner.
|
||||
webServer: {
|
||||
command: 'node tests/playwright-ct/visual/static-server.mjs',
|
||||
url: `http://127.0.0.1:${process.env.PROTOTYPE_PORT ?? 5179}/crewli-timetable.html`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 30_000,
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
})
|
||||
69
apps/app/playwright.config.ts
Normal file
69
apps/app/playwright.config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
// E2E config — drives a real Laravel test server. Used by `pnpm test:e2e`.
|
||||
// Component tests live in playwright-ct.config.ts (different runner).
|
||||
//
|
||||
// Architecture (B4 / TEST-CONTRACT-001):
|
||||
// - globalSetup runs `php artisan migrate:fresh --force --seed` against
|
||||
// crewli_test (the existing PHPUnit MySQL test DB) using the
|
||||
// E2EBaselineSeeder. Writes seeded IDs to api/storage/app/e2e-fixtures.json.
|
||||
// - webServer auto-starts `php artisan serve --port=8001` against the
|
||||
// same DB so the test can hit a live HTTP endpoint.
|
||||
// - Tests use page.context().request to call /api/v1/* with cookie auth
|
||||
// established via /api/v1/auth/login (Bearer-via-cookie, not stateful
|
||||
// Sanctum SPA flow — see api/.../SetAuthCookie.php).
|
||||
//
|
||||
// SCOPE: contract tests only (request shape verification). UI-driven e2e
|
||||
// tests would additionally need the Vite dev server (port 5174). Add that
|
||||
// to webServer when first UI-driven e2e test lands.
|
||||
//
|
||||
// CI integration: deferred to TEST-INFRA-002. This config currently
|
||||
// targets local-machine execution.
|
||||
|
||||
const E2E_API_PORT = Number(process.env.E2E_API_PORT ?? 8001)
|
||||
|
||||
// Use `localhost` (not 127.0.0.1) so the auth cookie set with
|
||||
// `domain=localhost` (per api/.env's SESSION_DOMAIN) is matched on
|
||||
// subsequent requests. Playwright's APIRequestContext respects
|
||||
// cookie scope strictly.
|
||||
const E2E_API_URL = `http://localhost:${E2E_API_PORT}`
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/playwright-e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
globalSetup: './tests/playwright-e2e/global-setup.ts',
|
||||
|
||||
use: {
|
||||
baseURL: E2E_API_URL,
|
||||
trace: 'off',
|
||||
video: 'off',
|
||||
screenshot: 'off',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
extraHTTPHeaders: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
// Auto-start the Laravel test server. globalSetup runs first
|
||||
// (migrate:fresh + seed), then this command spawns artisan serve
|
||||
// against the now-seeded crewli_test DB.
|
||||
webServer: {
|
||||
command: 'cd ../../api && DB_DATABASE=crewli_test php artisan --env=testing serve --port=8001',
|
||||
url: `${E2E_API_URL}/up`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 60_000,
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
})
|
||||
12
apps/app/playwright/index.html
Normal file
12
apps/app/playwright/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Playwright CT — Crewli</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
125
apps/app/playwright/index.ts
Normal file
125
apps/app/playwright/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { beforeMount } from '@playwright/experimental-ct-vue/hooks'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
||||
import { type RouteRecordRaw, createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { type ThemeDefinition, createVuetify } from 'vuetify'
|
||||
|
||||
// vuetify/components namespace import: required to register the full
|
||||
// component set on a freshly-created Vuetify instance per test, mirroring
|
||||
// tests/utils/mountWithVuexy.ts. Test infra only.
|
||||
import * as components from 'vuetify/components' // eslint-disable-line no-restricted-imports
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
// Vuetify base styles — required for v-btn / v-chip / v-card etc. to
|
||||
// render with their actual visual appearance (not a default browser
|
||||
// look). Without this, Playwright visual baselines would capture
|
||||
// unstyled components. Removed in F3 alongside the Vuetify plugin.
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Plain-CSS token sheet — getComputedStyle(el).getPropertyValue('--tt-status-…')
|
||||
// resolves during component tests. Path resolved by the alias map in
|
||||
// playwright-ct.config.ts.
|
||||
import '@/styles/tokens/_timetable.css'
|
||||
|
||||
// =============================================================================
|
||||
// HOOKS CONFIG (per-test, opt-in)
|
||||
// =============================================================================
|
||||
//
|
||||
// Tests pass `hooksConfig` to mount() to override defaults. Shape:
|
||||
// {
|
||||
// initialRoute?: string,
|
||||
// initialQuery?: Record<string, string>,
|
||||
// routes?: RouteRecordRaw[],
|
||||
// piniaInitialState?: Record<string, Record<string, unknown>>,
|
||||
// // injected by mountWithProviders.ts wrapper:
|
||||
// notificationMockKey?: string,
|
||||
// }
|
||||
//
|
||||
// Defaults below render every component with the full Vuexy/Vuetify
|
||||
// stack. F3 (PrimeVue foundation) replaces the Vuetify plugin line
|
||||
// here with PrimeVue and updates the sanity test — that is a ~2-hour
|
||||
// swap, not a rewrite. Vuetify is INTENTIONAL TEMPORARY STATE in this
|
||||
// file; do not abstract behind a "UI framework provider" indirection
|
||||
// because the abstraction would itself need to be removed in F3.
|
||||
// See dev-docs/ARCH-TESTING.md §6 for the migration timeline.
|
||||
// =============================================================================
|
||||
|
||||
export interface HooksConfig {
|
||||
initialRoute?: string
|
||||
initialQuery?: Record<string, string>
|
||||
routes?: RouteRecordRaw[]
|
||||
piniaInitialState?: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
const defaultTheme: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: '#1f7ad1',
|
||||
error: '#d63d4b',
|
||||
success: '#2fa66a',
|
||||
warning: '#e0992c',
|
||||
info: '#1f7ad1',
|
||||
},
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
|
||||
// ---- Vuetify (TEMPORARY: replaced by PrimeVue in F3) -----------------
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: { defaultTheme: 'crewliLight', themes: { crewliLight: defaultTheme } },
|
||||
})
|
||||
|
||||
app.use(vuetify)
|
||||
|
||||
// ---- Pinia ----------------------------------------------------------
|
||||
// Fresh instance per test. We do NOT use @pinia/testing's
|
||||
// createTestingPinia here because it depends on Vitest's `vi.fn`,
|
||||
// which doesn't exist in Playwright's Node runtime. Tests that need
|
||||
// to assert on store actions should snapshot store state via
|
||||
// page.evaluate() against window.__pinia (exposed below).
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
setActivePinia(pinia)
|
||||
|
||||
if (hooksConfig?.piniaInitialState) {
|
||||
// Hydrate store state directly. Stores are created lazily on first
|
||||
// use(); pre-hydration via pinia.state.value is safe.
|
||||
pinia.state.value = {
|
||||
...pinia.state.value,
|
||||
...hooksConfig.piniaInitialState,
|
||||
}
|
||||
}
|
||||
|
||||
// Expose pinia on window for cross-frame state assertions.
|
||||
;(globalThis as { __pinia?: typeof pinia }).__pinia = pinia
|
||||
|
||||
// ---- TanStack Vue Query ---------------------------------------------
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, refetchOnWindowFocus: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
app.use(VueQueryPlugin, { queryClient })
|
||||
|
||||
// ---- Router (memory history; no auth guards) ------------------------
|
||||
const routes: RouteRecordRaw[] = hooksConfig?.routes ?? [
|
||||
{ path: '/', component: { template: '<div data-test="ct-route-root" />' } },
|
||||
{ path: '/:pathMatch(.*)*', component: { template: '<div data-test="ct-route-catchall" />' } },
|
||||
]
|
||||
|
||||
const router = createRouter({ history: createMemoryHistory(), routes })
|
||||
|
||||
app.use(router)
|
||||
|
||||
if (hooksConfig?.initialRoute) {
|
||||
await router.push({
|
||||
path: hooksConfig.initialRoute,
|
||||
query: hooksConfig.initialQuery,
|
||||
})
|
||||
}
|
||||
await router.isReady()
|
||||
})
|
||||
244
apps/app/pnpm-lock.yaml
generated
244
apps/app/pnpm-lock.yaml
generated
@@ -157,6 +157,9 @@ importers:
|
||||
'@antfu/utils':
|
||||
specifier: 0.7.10
|
||||
version: 0.7.10
|
||||
'@axe-core/playwright':
|
||||
specifier: ^4.11.3
|
||||
version: 4.11.3(playwright-core@1.59.1)
|
||||
'@fullcalendar/core':
|
||||
specifier: 6.1.19
|
||||
version: 6.1.19
|
||||
@@ -198,10 +201,16 @@ importers:
|
||||
version: 4.1.2(vue@3.5.22(typescript@5.9.3))
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: 11.0.1
|
||||
version: 11.0.1(@vue/compiler-dom@3.5.22)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
version: 11.0.1(@vue/compiler-dom@3.5.34)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
'@pinia/testing':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))
|
||||
'@playwright/experimental-ct-vue':
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)
|
||||
'@playwright/test':
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
'@stylistic/eslint-plugin-js':
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
@@ -216,7 +225,7 @@ importers:
|
||||
version: 2.1.3(stylelint@16.8.0(typescript@5.9.3))
|
||||
'@testing-library/vue':
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0(@vue/compiler-dom@3.5.22)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
version: 8.1.0(@vue/compiler-dom@3.5.34)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
'@tiptap/extension-character-count':
|
||||
specifier: ^2.27.1
|
||||
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
|
||||
@@ -259,9 +268,12 @@ importers:
|
||||
'@vitejs/plugin-vue-jsx':
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
'@vue/compiler-dom':
|
||||
specifier: ^3.5.34
|
||||
version: 3.5.34
|
||||
'@vue/test-utils':
|
||||
specifier: ^2.4.9
|
||||
version: 2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
version: 2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
axe-core:
|
||||
specifier: ^4.11.4
|
||||
version: 4.11.4
|
||||
@@ -387,7 +399,7 @@ importers:
|
||||
version: 0.18.6(@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3)))(rollup@4.52.5)
|
||||
unplugin-vue-components:
|
||||
specifier: 0.27.5
|
||||
version: 0.27.5(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3))
|
||||
version: 0.27.5(@babel/parser@7.29.3)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3))
|
||||
unplugin-vue-router:
|
||||
specifier: 0.8.8
|
||||
version: 0.8.8(rollup@4.52.5)(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
@@ -467,6 +479,11 @@ packages:
|
||||
'@asamuzakjp/nwsapi@2.3.9':
|
||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||
|
||||
'@axe-core/playwright@4.11.3':
|
||||
resolution: {integrity: sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==}
|
||||
peerDependencies:
|
||||
playwright-core: '>= 1.0.0'
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -554,6 +571,11 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/parser@7.29.3':
|
||||
resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/plugin-proposal-decorators@7.28.0':
|
||||
resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -611,6 +633,10 @@ packages:
|
||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@boundaries/elements@2.0.1':
|
||||
resolution: {integrity: sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==}
|
||||
engines: {node: '>=18.18'}
|
||||
@@ -1157,6 +1183,20 @@ packages:
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/experimental-ct-core@1.59.1':
|
||||
resolution: {integrity: sha512-U7+jNROBJxfwjM/G7011+UNEyLiI5zIT1HWAn1k89WZIWl5RUWaCGWlYkYdAZwBSVfGstjF9AgkzmS0RsF8Ulw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@playwright/experimental-ct-vue@1.59.1':
|
||||
resolution: {integrity: sha512-RygXcwXQwRHzcdaQAXpKiHEl8XDPepZKmfHDNPCSnCN1g9ylaAvtNF6s7DgpsxlHqLlwgc2DmMr1n3D/OOVKyQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@playwright/test@1.59.1':
|
||||
resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -2013,6 +2053,13 @@ packages:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
vue: ^3.0.0
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4':
|
||||
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@vitejs/plugin-vue@6.0.1':
|
||||
resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -2086,9 +2133,15 @@ packages:
|
||||
'@vue/compiler-core@3.5.22':
|
||||
resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
|
||||
|
||||
'@vue/compiler-core@3.5.34':
|
||||
resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==}
|
||||
|
||||
'@vue/compiler-dom@3.5.22':
|
||||
resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==}
|
||||
|
||||
'@vue/compiler-dom@3.5.34':
|
||||
resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.22':
|
||||
resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==}
|
||||
|
||||
@@ -2143,6 +2196,9 @@ packages:
|
||||
'@vue/shared@3.5.22':
|
||||
resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
|
||||
|
||||
'@vue/shared@3.5.34':
|
||||
resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==}
|
||||
|
||||
'@vue/test-utils@2.4.9':
|
||||
resolution: {integrity: sha512-YwgowiO1mPleZqpgAGfxvWu/A5A8nkLrbyH2SqiQRkyzCIaDzzo27/2uS/F1g7fRLvl8BUY0+Sr1eC+6+IHfrw==}
|
||||
peerDependencies:
|
||||
@@ -3219,6 +3275,11 @@ packages:
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
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}
|
||||
@@ -4207,6 +4268,16 @@ packages:
|
||||
pkg-types@2.3.0:
|
||||
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
|
||||
|
||||
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@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5212,6 +5283,46 @@ packages:
|
||||
peerDependencies:
|
||||
vue: '>=3.2.13'
|
||||
|
||||
vite@6.4.2:
|
||||
resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
jiti: '>=1.21.0'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vite@7.1.12:
|
||||
resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -5703,6 +5814,11 @@ snapshots:
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
'@axe-core/playwright@4.11.3(playwright-core@1.59.1)':
|
||||
dependencies:
|
||||
axe-core: 4.11.4
|
||||
playwright-core: 1.59.1
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -5826,6 +5942,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/parser@7.29.3':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -5896,6 +6016,11 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@boundaries/elements@2.0.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
@@ -6254,12 +6379,12 @@ snapshots:
|
||||
|
||||
'@intlify/shared@11.1.12': {}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.22)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
'@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.34)(eslint@8.57.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
|
||||
'@intlify/bundle-utils': 11.0.1(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.1.12
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.22)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.34)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
'@typescript-eslint/scope-manager': 8.46.2
|
||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||
@@ -6278,12 +6403,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.22)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.34)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
optionalDependencies:
|
||||
'@intlify/shared': 11.1.12
|
||||
'@vue/compiler-dom': 3.5.22
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
vue-i18n: 11.1.12(vue@3.5.22(typescript@5.9.3))
|
||||
|
||||
@@ -6385,6 +6510,47 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@playwright/experimental-ct-core@1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)':
|
||||
dependencies:
|
||||
playwright: 1.59.1
|
||||
playwright-core: 1.59.1
|
||||
vite: 6.4.2(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@playwright/experimental-ct-vue@1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)':
|
||||
dependencies:
|
||||
'@playwright/experimental-ct-core': 1.59.1(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
'@vitejs/plugin-vue': 5.2.4(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- vite
|
||||
- vue
|
||||
- yaml
|
||||
|
||||
'@playwright/test@1.59.1':
|
||||
dependencies:
|
||||
playwright: 1.59.1
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
@@ -6614,11 +6780,11 @@ snapshots:
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.22)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
'@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.34)(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@testing-library/dom': 9.3.4
|
||||
'@vue/test-utils': 2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
'@vue/test-utils': 2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.22
|
||||
@@ -7257,6 +7423,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.29
|
||||
@@ -7365,11 +7536,24 @@ snapshots:
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-core@3.5.34':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.3
|
||||
'@vue/shared': 3.5.34
|
||||
entities: 7.0.1
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-dom@3.5.22':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.22
|
||||
'@vue/shared': 3.5.22
|
||||
|
||||
'@vue/compiler-dom@3.5.34':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.34
|
||||
'@vue/shared': 3.5.34
|
||||
|
||||
'@vue/compiler-sfc@3.5.22':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
@@ -7436,7 +7620,7 @@ snapshots:
|
||||
'@vue/language-core@3.1.2(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.23
|
||||
'@vue/compiler-dom': 3.5.22
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
'@vue/shared': 3.5.22
|
||||
alien-signals: 3.0.3
|
||||
muggle-string: 0.4.1
|
||||
@@ -7469,9 +7653,11 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.22': {}
|
||||
|
||||
'@vue/test-utils@2.4.9(@vue/compiler-dom@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
'@vue/shared@3.5.34': {}
|
||||
|
||||
'@vue/test-utils@2.4.9(@vue/compiler-dom@3.5.34)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.22
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
js-beautify: 1.15.4
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.2.7
|
||||
@@ -8853,6 +9039,9 @@ snapshots:
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -9901,6 +10090,14 @@ snapshots:
|
||||
exsolve: 1.0.7
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.59.1: {}
|
||||
|
||||
playwright@1.59.1:
|
||||
dependencies:
|
||||
playwright-core: 1.59.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
@@ -10933,7 +11130,7 @@ snapshots:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
|
||||
unplugin-vue-components@0.27.5(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)):
|
||||
unplugin-vue-components@0.27.5(@babel/parser@7.29.3)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
@@ -10947,7 +11144,7 @@ snapshots:
|
||||
unplugin: 1.16.1
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/parser': 7.29.3
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -11099,7 +11296,7 @@ snapshots:
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
|
||||
'@vue/compiler-dom': 3.5.22
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
@@ -11128,6 +11325,21 @@ snapshots:
|
||||
svgo: 3.3.2
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
|
||||
vite@6.4.2(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.11
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.52.5
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 24.9.2
|
||||
fsevents: 2.3.3
|
||||
sass: 1.76.0
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.1
|
||||
|
||||
vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.11
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const count = ref(0)
|
||||
function onClick() {
|
||||
count.value += 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test="harness">
|
||||
<VBtn
|
||||
color="primary"
|
||||
data-test="btn"
|
||||
@click="onClick"
|
||||
>
|
||||
Clicks: {{ count }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from '@playwright/experimental-ct-vue'
|
||||
import SanityButtonHarness from './SanityButtonHarness.vue'
|
||||
|
||||
// B2 sanity — proves the full provider stack from playwright/index.ts
|
||||
// is wired:
|
||||
// - Vuetify renders v-btn with theme tokens applied
|
||||
// - Click events propagate via Vue's reactivity (counter ref updates)
|
||||
// - Vuetify CSS variables resolve in computed style
|
||||
//
|
||||
// TEMPORARY VUETIFY: this test is replaced by a PrimeVue equivalent
|
||||
// in F3. Do not extend or generalise — F3 rewrites it. See
|
||||
// dev-docs/ARCH-TESTING.md §6.
|
||||
//
|
||||
// Why a .vue harness file: Playwright CT runs the test orchestrator
|
||||
// in Node and the component in a Vite-bundled browser context. Vue
|
||||
// components that pull in CSS-side-effect imports (Vuetify) cannot be
|
||||
// loaded directly into the test's Node module graph; they must live
|
||||
// in a .vue / Vite-compilable file. This is a structural divergence
|
||||
// from Vitest, which uses jsdom and one module graph for both.
|
||||
|
||||
test.describe('B2 sanity: provider stack', () => {
|
||||
test('mounts a Vuetify v-btn and propagates clicks', async ({ mount }) => {
|
||||
const component = await mount(SanityButtonHarness)
|
||||
const btn = component.locator('[data-test="btn"]')
|
||||
|
||||
await expect(btn).toBeVisible()
|
||||
await expect(btn).toContainText('Clicks: 0')
|
||||
|
||||
await btn.click()
|
||||
await expect(btn).toContainText('Clicks: 1')
|
||||
|
||||
await btn.click()
|
||||
await expect(btn).toContainText('Clicks: 2')
|
||||
})
|
||||
|
||||
test('Vuetify primary theme color resolves on rendered button', async ({ mount, page }) => {
|
||||
await mount(SanityButtonHarness)
|
||||
|
||||
// Vuetify exposes theme primary as "R, G, B" decimals on
|
||||
// --v-theme-primary (e.g. "31, 122, 209" for #1f7ad1).
|
||||
const themePrimary = await page.evaluate(() => {
|
||||
const root = document.documentElement
|
||||
|
||||
return getComputedStyle(root).getPropertyValue('--v-theme-primary').trim()
|
||||
})
|
||||
|
||||
expect(themePrimary).not.toBe('')
|
||||
expect(themePrimary.split(',')).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
8
apps/app/tests/playwright-ct/smoke.spec.ts
Normal file
8
apps/app/tests/playwright-ct/smoke.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { expect, test } from '@playwright/experimental-ct-vue'
|
||||
|
||||
// B1 smoke: proves Playwright Component Testing is wired and Chromium
|
||||
// boots. Replaces no functionality; deleted once a real component
|
||||
// test (B2 sanity) supersedes it.
|
||||
test('chromium boots in CT runner', async ({ page }) => {
|
||||
expect(page).toBeTruthy()
|
||||
})
|
||||
123
apps/app/tests/playwright-ct/utils/mountWithProviders.ts
Normal file
123
apps/app/tests/playwright-ct/utils/mountWithProviders.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import type { HooksConfig } from '../../../playwright'
|
||||
|
||||
/**
|
||||
* mountWithProviders — Playwright Component Testing analogue of
|
||||
* tests/utils/mountWithVuexy.ts.
|
||||
*
|
||||
* Why two helpers?
|
||||
* ----------------
|
||||
* Playwright CT's `mount()` API is structurally different from
|
||||
* @vue/test-utils' `mount()`: provider plugins (Vuetify, Pinia,
|
||||
* Router, VueQuery) must be registered in `playwright/index.ts`'s
|
||||
* `beforeMount` hook rather than passed at call time. This helper
|
||||
* is a thin ergonomic wrapper that:
|
||||
*
|
||||
* - Forwards `hooksConfig` to the beforeMount hook (typed via
|
||||
* HooksConfig from playwright/index.ts)
|
||||
* - Returns the standard Playwright Locator from CT's mount, so
|
||||
* downstream test code uses normal Playwright assertions
|
||||
* (component.click(), expect(component).toBeVisible(), etc.)
|
||||
* - Serves as a single, discoverable surface for "how do I mount
|
||||
* a component in this codebase" questions
|
||||
*
|
||||
* Lifecycle note (TEMPORARY VUETIFY):
|
||||
* -----------------------------------
|
||||
* The Vuetify plugin line in `playwright/index.ts`'s `beforeMount`
|
||||
* hook is INTENTIONALLY temporary state. F3 (PrimeVue foundation,
|
||||
* RFC-WS-FRONTEND-PRIMEVUE §6) replaces it with PrimeVue. We do NOT
|
||||
* abstract behind a "pluggable UI framework" indirection because:
|
||||
*
|
||||
* 1. We are NOT retaining Vuetify; the abstraction would itself
|
||||
* need to be removed in F3.
|
||||
* 2. The swap is mechanical (~2-hour) and atomic; abstraction adds
|
||||
* cognitive cost without paying back.
|
||||
* 3. Reviewers seeing "Vuetify in test infra in a PrimeVue migration
|
||||
* sprint" should read this JSDoc and dev-docs/ARCH-TESTING.md §6
|
||||
* for context.
|
||||
*
|
||||
* Equivalence to mountWithVuexy.ts:
|
||||
* ---------------------------------
|
||||
* | Capability | Vitest (mountWithVuexy) | Playwright CT (this) |
|
||||
* |----------------------------------|-------------------------------|----------------------|
|
||||
* | Vuetify w/ tokens | createVuetify({components,…}) | beforeMount hook |
|
||||
* | Pinia (actions execute) | createTestingPinia | createPinia |
|
||||
* | TanStack Query (fresh client) | per-call new QueryClient | per-test in hook |
|
||||
* | Memory-history router | per-call createRouter | per-test in hook |
|
||||
* | initialPath / initialQuery | options.initialPath | hooksConfig.… |
|
||||
* | Initial Pinia state | options.initialState | hooksConfig.piniaInitialState |
|
||||
* | Notification mock | createNotificationMock + plug | (see assertNotification below) |
|
||||
*
|
||||
* Notification assertions:
|
||||
* ------------------------
|
||||
* Playwright runs in a separate Node process from the browser and
|
||||
* cannot use `vi.fn()` spies on store actions. Instead, tests assert
|
||||
* on the rendered UI (e.g. `await expect(page.getByRole('alert'))
|
||||
* .toContainText('Saved')`) or read pinia state via page.evaluate.
|
||||
* This is a deliberate divergence from the Vitest pattern — UI
|
||||
* assertions are stronger than spy assertions for a real-browser
|
||||
* runner.
|
||||
*
|
||||
* Type signature constraint:
|
||||
* --------------------------
|
||||
* Playwright CT's MountResult uses internal types. We accept the
|
||||
* native MountOptions shape and wrap; tests should import the
|
||||
* Playwright-CT `expect`/`test` and call `mountWithProviders` to
|
||||
* get the locator back.
|
||||
*/
|
||||
export interface MountOptions {
|
||||
props?: Record<string, unknown>
|
||||
slots?: Record<string, unknown>
|
||||
hooksConfig?: HooksConfig
|
||||
}
|
||||
|
||||
// Re-export so callers have one import surface.
|
||||
export type { HooksConfig } from '../../../playwright'
|
||||
|
||||
// The actual mount call must be done by the test using Playwright CT's
|
||||
// `mount` fixture (`test('…', async ({ mount }) => …)`). We expose a
|
||||
// helper that builds the options object correctly. This avoids the
|
||||
// "two ways to mount" footgun: there's the Playwright fixture, and
|
||||
// there's our wrapper that produces its arguments.
|
||||
export function buildMountArgs<C extends Component>(
|
||||
component: C,
|
||||
opts: MountOptions = {},
|
||||
): {
|
||||
component: C
|
||||
options: { props?: Record<string, unknown>; slots?: Record<string, unknown>; hooksConfig?: HooksConfig }
|
||||
} {
|
||||
return {
|
||||
component,
|
||||
options: {
|
||||
props: opts.props,
|
||||
slots: opts.slots,
|
||||
hooksConfig: opts.hooksConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: assert on the notification store via the browser's
|
||||
* window.__pinia exposed in the beforeMount hook. Returns the current
|
||||
* notification state as serialised JSON.
|
||||
*/
|
||||
export async function readNotificationState(
|
||||
componentLocator: Locator,
|
||||
): Promise<{ visible: boolean; message: string; type: string }> {
|
||||
const page = componentLocator.page()
|
||||
|
||||
return page.evaluate(() => {
|
||||
interface Win { __pinia?: { state: { value: Record<string, unknown> } } }
|
||||
const w = window as unknown as Win
|
||||
|
||||
const state = w.__pinia?.state?.value?.notification as
|
||||
| { visible: boolean; message: string; type: string }
|
||||
| undefined
|
||||
|
||||
return state
|
||||
? { visible: state.visible, message: state.message, type: state.type }
|
||||
: { visible: false, message: '', type: 'info' }
|
||||
})
|
||||
}
|
||||
11
apps/app/tests/playwright-ct/visual/prototype-smoke.spec.ts
Normal file
11
apps/app/tests/playwright-ct/visual/prototype-smoke.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { expect, test } from '@playwright/experimental-ct-vue'
|
||||
|
||||
const PROTOTYPE_URL = `http://127.0.0.1:${process.env.PROTOTYPE_PORT ?? 5179}/crewli-timetable.html`
|
||||
|
||||
test('prototype loads and renders', async ({ page }) => {
|
||||
await page.goto(PROTOTYPE_URL)
|
||||
|
||||
// Wait for the React app to render. The first stage row appears
|
||||
// once data + babel-transformed JSX are loaded.
|
||||
await expect(page.locator('.cw-block').first()).toBeVisible({ timeout: 15_000 })
|
||||
})
|
||||
203
apps/app/tests/playwright-ct/visual/prototype.spec.ts
Normal file
203
apps/app/tests/playwright-ct/visual/prototype.spec.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { expect, test } from '@playwright/experimental-ct-vue'
|
||||
|
||||
// =============================================================================
|
||||
// VISUAL BASELINES — Artist Management surfaces (RFC §A.3 baseline scope)
|
||||
// =============================================================================
|
||||
//
|
||||
// SOURCE OF TRUTH: the Crewli prototype HTML at
|
||||
// resources/Crewli - Artist Timetable Management/crewli-timetable.html
|
||||
// This is the canonical visual reference Artist Management is measured
|
||||
// against during F4. When the prototype changes, baselines update with
|
||||
// `pnpm test:visual:update` and the diff PNG is reviewed in the PR.
|
||||
//
|
||||
// Tagged @visual so `pnpm test:visual` runs only this suite.
|
||||
//
|
||||
// Baseline strategy — composite over isolated:
|
||||
// --------------------------------------------
|
||||
// The prototype's DOM does not expose status, perf-id, or stage-id as
|
||||
// data-* attributes. Block status is encoded in inline `style.background`
|
||||
// only. Locating an individual block by status would require either
|
||||
// pixel-color sampling (brittle) or hardcoding artist names (locks the
|
||||
// test to specific data values, which would silently rot if the
|
||||
// prototype's seed data changes).
|
||||
//
|
||||
// Instead we capture COMPOSITE surfaces that contain the full visual
|
||||
// vocabulary side-by-side:
|
||||
// - Full Vrijdag canvas → all status colors, b2b, multi-lane
|
||||
// - Full Zaterdag canvas → conflict ring, capacity warning
|
||||
// - Wachtrij sidebar → list rendering, status badges, counts
|
||||
// - Popover (clicked from canvas) → popover layout
|
||||
// One screenshot covers many "surfaces" the prompt's §A.3 list
|
||||
// enumerates separately. F4 component-level Vue tests will provide
|
||||
// per-state isolated baselines using the live SPA's data-test-id
|
||||
// attributes (added during component migration).
|
||||
//
|
||||
// Surfaces NOT captured here are documented as test.skip() at the
|
||||
// bottom with the reason and what would be required to enable them.
|
||||
// =============================================================================
|
||||
|
||||
const PROTOTYPE_URL = `http://127.0.0.1:${process.env.PROTOTYPE_PORT ?? 5179}/crewli-timetable.html`
|
||||
|
||||
async function navigateAndStabilize(page: import('@playwright/test').Page) {
|
||||
await page.goto(PROTOTYPE_URL)
|
||||
await expect(page.locator('.cw-block').first()).toBeVisible({ timeout: 15_000 })
|
||||
|
||||
// Hide elements that vary visually between machines or are dev-only.
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
*:focus { outline: none !important; }
|
||||
.cw-tweaks-panel, .cw-tweak-btn { visibility: hidden !important; }
|
||||
`,
|
||||
})
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => resolve(undefined))))
|
||||
}
|
||||
|
||||
test.describe('@visual Artist Management — prototype baselines', () => {
|
||||
test.use({ viewport: { width: 1440, height: 900 } })
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CANVAS — full-page baselines per day
|
||||
//
|
||||
// Vrijdag (d_fr) covers status colors (option, requested, confirmed,
|
||||
// contracted, concept), B2B indicators (auto-derived for adjacent
|
||||
// perfs on hardstyle: p_1→p_2→p_3), and multi-lane stacking
|
||||
// (p_4 / p_4b / p_4c overlap).
|
||||
//
|
||||
// Zaterdag (d_sa) covers conflict ring (p_18 vs p_19 overlap on
|
||||
// s_urban) and capacity warning (p_14 draw 4800/cap 5000, p_15
|
||||
// draw 5200/cap 5000 on s_hollandse).
|
||||
// ---------------------------------------------------------------------------
|
||||
test('canvas — Vrijdag (statuses + b2b + multi-lane)', async ({ page }) => {
|
||||
await navigateAndStabilize(page)
|
||||
await expect(page).toHaveScreenshot('canvas-friday.png', { fullPage: true })
|
||||
})
|
||||
|
||||
test('canvas — Zaterdag (conflict + capacity warning)', async ({ page }) => {
|
||||
await navigateAndStabilize(page)
|
||||
|
||||
// Day tab uses role="tab", not "button".
|
||||
await page.getByRole('tab', { name: /Zaterdag/ }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page).toHaveScreenshot('canvas-saturday.png', { fullPage: true })
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// STAGE ROW — first row in isolation, captures multi-lane stacking
|
||||
// detail. The prototype renders rows as .cw-tt-stage (label column)
|
||||
// + a corresponding lane-band in the canvas. We capture the canvas
|
||||
// element which spans 1440×row-height.
|
||||
// ---------------------------------------------------------------------------
|
||||
test('stage row — first row (Hardstyle, multi-lane on Vrijdag)', async ({ page }) => {
|
||||
await navigateAndStabilize(page)
|
||||
|
||||
// The first .cw-tt-stage is hardstyle on Vrijdag. We capture the
|
||||
// full row width by screenshotting the .cw-tt container's first
|
||||
// visible row using a clip rectangle derived from the element.
|
||||
const firstStage = page.locator('.cw-tt-stage').first()
|
||||
|
||||
await expect(firstStage).toBeVisible()
|
||||
|
||||
const stageBox = await firstStage.boundingBox()
|
||||
if (!stageBox)
|
||||
throw new Error('Could not measure first stage row')
|
||||
|
||||
await expect(page).toHaveScreenshot('stage-row-multilane.png', {
|
||||
clip: { x: 0, y: stageBox.y, width: 1440, height: stageBox.height },
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WACHTRIJ — populated state (default seed data has parked + pending)
|
||||
// ---------------------------------------------------------------------------
|
||||
test('wachtrij — populated (default seed)', async ({ page }) => {
|
||||
await navigateAndStabilize(page)
|
||||
|
||||
const queue = page.locator('aside.cw-parking').first()
|
||||
|
||||
await expect(queue).toBeVisible()
|
||||
await expect(queue).toHaveScreenshot('wachtrij-populated.png')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POPOVER — opens on block click
|
||||
// ---------------------------------------------------------------------------
|
||||
test('popover — opens on first-block click', async ({ page }) => {
|
||||
await navigateAndStabilize(page)
|
||||
|
||||
// Click any visible block; the popover renders into a portal
|
||||
// sibling of the block. We pick .cw-block first so we don't depend
|
||||
// on artist names.
|
||||
await page.locator('.cw-block').first().click()
|
||||
|
||||
const popover = page.locator('.cw-popover').first()
|
||||
|
||||
await expect(popover).toBeVisible({ timeout: 5_000 })
|
||||
await expect(popover).toHaveScreenshot('popover.png')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SKIPPED — surfaces not capturable from the canonical prototype.
|
||||
// Each documents the gap so F4 component-level baselines pick them up.
|
||||
// ---------------------------------------------------------------------------
|
||||
test.skip('block — cancelled status (isolated)', async () => {
|
||||
// Prototype data has no cancelled performance (data.js shows only
|
||||
// concept|requested|option|confirmed|contracted appear). Status
|
||||
// filter at timetable.jsx:227 also defaults cancelled OFF. To
|
||||
// capture this baseline we'd modify prototype data — that would
|
||||
// violate "we don't fake the prototype" (sprint prompt). Defer to
|
||||
// F4 where the live SPA renders cancelled blocks against real
|
||||
// data and we can baseline a dedicated cancelled fixture.
|
||||
})
|
||||
|
||||
test.skip('block — B2B indicator (isolated)', async () => {
|
||||
// Covered by canvas-friday.png composite (p_1→p_2→p_3 are
|
||||
// perfectly back-to-back on hardstyle). An isolated block-level
|
||||
// baseline would require artist-name-based locators that lock
|
||||
// the test to specific data values. Defer to F4 where Vue
|
||||
// components expose data-test-id="performance-block".
|
||||
})
|
||||
|
||||
test.skip('block — capacity warning (isolated)', async () => {
|
||||
// Covered by canvas-saturday.png composite. Same locator
|
||||
// brittleness rationale as B2B above.
|
||||
})
|
||||
|
||||
test.skip('block — conflict ring (isolated)', async () => {
|
||||
// Covered by canvas-saturday.png composite.
|
||||
})
|
||||
|
||||
test.skip('add-performance dialog — drag mode', async () => {
|
||||
// Drag-mode requires a real mousedown→mousemove→mouseup sequence
|
||||
// on the canvas. The prototype's drag detection uses raw mouse
|
||||
// event math that's flaky under Playwright's page.mouse simulated
|
||||
// events. F4 component test will use a stable drag-handle API on
|
||||
// the live Vue component.
|
||||
})
|
||||
|
||||
test.skip('add-performance dialog — button mode', async () => {
|
||||
// The prototype does not expose a "+ Voorstelling" button at
|
||||
// the canvas level — performances are only added via drag-from-
|
||||
// wachtrij or by clicking an empty time slot. F4 component
|
||||
// tests will baseline the live SPA's explicit "Voorstelling
|
||||
// toevoegen" button.
|
||||
})
|
||||
|
||||
test.skip('wachtrij — empty state', async () => {
|
||||
// Prototype seed data ships with PARKED + PENDING populated;
|
||||
// no UI flow drains the queue to empty. Defer to F4 component
|
||||
// test against an empty-state fixture.
|
||||
})
|
||||
|
||||
test.skip('wachtrij — grouped by status with counts', async () => {
|
||||
// Grouping toggle exists (cw-pq-msel-btn) but its visual delta
|
||||
// vs ungrouped is small and the toggle state is opaque to a
|
||||
// post-render observer. Defer to F4 where the Vue component
|
||||
// exposes state via data-test-state="grouped"|"ungrouped".
|
||||
})
|
||||
|
||||
test.skip('canvas — empty day', async () => {
|
||||
// Prototype only has d_fr and d_sa, both populated. No empty-day
|
||||
// baseline available from canonical data. Defer to F4.
|
||||
})
|
||||
})
|
||||
72
apps/app/tests/playwright-ct/visual/static-server.mjs
Normal file
72
apps/app/tests/playwright-ct/visual/static-server.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable no-console */
|
||||
import { createReadStream, statSync } from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
|
||||
// Tiny static-file server that serves the canonical Crewli prototype
|
||||
// HTML so Playwright visual baselines can render it in real Chromium.
|
||||
//
|
||||
// Why not http-server / serve / similar?
|
||||
// --------------------------------------
|
||||
// They'd add ~5 MB and another supply-chain hop for a 30-line problem.
|
||||
// The prototype directory has 9 files; we serve them with mime types
|
||||
// for .html, .css, .js, .jsx (text/babel handles via the HTML loader).
|
||||
// Node's built-in http + fs is sufficient.
|
||||
//
|
||||
// Started by playwright-ct.config.ts via webServer config.
|
||||
|
||||
const ROOT = path.resolve(
|
||||
process.cwd(),
|
||||
'..',
|
||||
'..',
|
||||
'resources',
|
||||
'Crewli - Artist Timetable Management',
|
||||
)
|
||||
const PORT = Number(process.env.PROTOTYPE_PORT ?? 5179)
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.jsx': 'text/babel; charset=utf-8',
|
||||
'.mjs': 'application/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.ico': 'image/x-icon',
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Strip query / hash before path resolution.
|
||||
const urlPath = (req.url ?? '/').split('?')[0].split('#')[0]
|
||||
const safe = path.normalize(urlPath).replace(/^(\.\.[\\/])+/, '')
|
||||
const filePath = path.join(ROOT, safe === '/' ? 'crewli-timetable.html' : safe)
|
||||
|
||||
// Reject path traversal.
|
||||
if (!filePath.startsWith(ROOT)) {
|
||||
res.statusCode = 403
|
||||
res.end('Forbidden')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = statSync(filePath)
|
||||
if (stat.isDirectory()) {
|
||||
res.statusCode = 404
|
||||
res.end('Not found')
|
||||
return
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream')
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
createReadStream(filePath).pipe(res)
|
||||
}
|
||||
catch {
|
||||
res.statusCode = 404
|
||||
res.end(`Not found: ${urlPath}`)
|
||||
}
|
||||
})
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`[prototype-server] http://127.0.0.1:${PORT}/ -> ${ROOT}`)
|
||||
})
|
||||
72
apps/app/tests/playwright-e2e/global-setup.ts
Normal file
72
apps/app/tests/playwright-e2e/global-setup.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Playwright globalSetup — runs once before all e2e tests.
|
||||
//
|
||||
// Responsibilities:
|
||||
// 1. Verify api/.env exists (Laravel needs APP_KEY etc.)
|
||||
// 2. Run `php artisan migrate:fresh --force --seed --seeder=E2EBaselineSeeder`
|
||||
// against crewli_test (override DB_DATABASE on the command line).
|
||||
// The seeder writes seeded IDs to api/storage/app/e2e-fixtures.json.
|
||||
// 3. Verify the fixtures file was produced.
|
||||
//
|
||||
// Failures here abort the test run before any spec runs — prevents
|
||||
// confusing per-test failures when the test DB is in an unexpected state.
|
||||
//
|
||||
// Why crewli_test (and not a dedicated crewli_e2e DB):
|
||||
// The PHPUnit suite already uses crewli_test with `RefreshDatabase`
|
||||
// (transaction-rollback per test). Running e2e and PHPUnit
|
||||
// concurrently would collide; the lifecycle here assumes serial
|
||||
// execution, which is the realistic local-dev flow. Documented in
|
||||
// dev-docs/ARCH-TESTING.md §5 (mock-vs-real-backend).
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..')
|
||||
const API_DIR = path.join(REPO_ROOT, 'api')
|
||||
const FIXTURES_PATH = path.join(API_DIR, 'storage', 'app', 'e2e-fixtures.json')
|
||||
|
||||
export default async function globalSetup(): Promise<void> {
|
||||
console.log('[e2e setup] Repo root:', REPO_ROOT)
|
||||
console.log('[e2e setup] API dir:', API_DIR)
|
||||
|
||||
if (!existsSync(path.join(API_DIR, '.env'))) {
|
||||
throw new Error(
|
||||
'[e2e setup] api/.env not found. Run `cp api/.env.example api/.env` and configure DB_* + APP_KEY.',
|
||||
)
|
||||
}
|
||||
|
||||
console.log('[e2e setup] Running migrate:fresh + E2EBaselineSeeder against crewli_test...')
|
||||
|
||||
// --env=testing avoids the dangerous-bash hook's migrate:fresh guard
|
||||
// (which requires --env=testing to permit the destructive op).
|
||||
// DB_DATABASE override forces crewli_test even though the .env may
|
||||
// point at the dev DB.
|
||||
try {
|
||||
execSync(
|
||||
'DB_DATABASE=crewli_test php artisan --env=testing migrate:fresh --force --seed --seeder="Database\\\\Seeders\\\\E2EBaselineSeeder"',
|
||||
{
|
||||
cwd: API_DIR,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, DB_DATABASE: 'crewli_test' },
|
||||
},
|
||||
)
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`[e2e setup] migrate:fresh + seed failed: ${(err as Error).message}`)
|
||||
}
|
||||
|
||||
if (!existsSync(FIXTURES_PATH)) {
|
||||
throw new Error(
|
||||
`[e2e setup] Expected fixtures file at ${FIXTURES_PATH} but it does not exist. `
|
||||
+ 'Did E2EBaselineSeeder run? Check the artisan output above.',
|
||||
)
|
||||
}
|
||||
|
||||
const fixtures = JSON.parse(readFileSync(FIXTURES_PATH, 'utf-8')) as Record<string, unknown>
|
||||
|
||||
console.log('[e2e setup] Fixtures ready:', Object.keys(fixtures).join(', '))
|
||||
}
|
||||
112
apps/app/tests/playwright-e2e/timetable/409-conflict.spec.ts
Normal file
112
apps/app/tests/playwright-e2e/timetable/409-conflict.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { loginAsBaselineUser } from '../utils/auth'
|
||||
import { readFixtures } from '../utils/fixtures'
|
||||
|
||||
// =============================================================================
|
||||
// TEST-CONTRACT-001 — 409 conflict shape on optimistic-locking failure
|
||||
// =============================================================================
|
||||
//
|
||||
// Why this test exists:
|
||||
// ---------------------
|
||||
// The timetable-stabilization sprint's B5 incident (commit ff056e3)
|
||||
// caught a schema-drift bug where the frontend's Zod schema for
|
||||
// MoveTimetableConflictResponse diverged from the backend's response
|
||||
// shape. Vitest+jsdom tests passed because they mocked both sides.
|
||||
// This test hits the REAL backend and validates the response shape
|
||||
// against what the SPA actually parses.
|
||||
//
|
||||
// Scenario:
|
||||
// ---------
|
||||
// 1. Login as e2e@test.local (seeded via E2EBaselineSeeder).
|
||||
// 2. POST /timetable/move with version=0 → expect 200 (succeeds; perf
|
||||
// version becomes 1).
|
||||
// 3. POST same move again with version=0 → expect 409 with shape:
|
||||
// { success: false, errors: { conflict: 'version_mismatch' }, ... }
|
||||
// 4. Assert structural properties of the 409 response so future
|
||||
// backend changes that drift this shape FAIL the test instead of
|
||||
// silently passing through mock-equipped Vitest assertions.
|
||||
//
|
||||
// What this test does NOT cover:
|
||||
// ------------------------------
|
||||
// - Multi-context race (two real browsers): single-context replay is
|
||||
// functionally equivalent for contract validation; the 409 is
|
||||
// returned by the server based on stored version, not on session
|
||||
// identity.
|
||||
// - UI rollback behaviour: that's a future UI-driven e2e that needs
|
||||
// the Vite webServer running. Out of B4 scope.
|
||||
// - Exhaustive 4xx/5xx variants: only the 409 shape is the documented
|
||||
// regression risk.
|
||||
// =============================================================================
|
||||
|
||||
test.describe('TEST-CONTRACT-001 — timetable move 409 conflict', () => {
|
||||
test('second move with stale version returns 409 with version_mismatch', async ({ browser }) => {
|
||||
const fixtures = readFixtures()
|
||||
const context = await browser.newContext({ baseURL: process.env.E2E_API_URL ?? 'http://localhost:8001' })
|
||||
const { request, organisationId } = await loginAsBaselineUser(context)
|
||||
|
||||
expect(organisationId).toBe(fixtures.organisation_id)
|
||||
|
||||
const moveUrl = `/api/v1/organisations/${fixtures.organisation_id}/events/${fixtures.event_id}/timetable/move`
|
||||
|
||||
// Use a stable target time independent of the original seed, so
|
||||
// we control where the perf lands. Take the seeded perf's
|
||||
// engagement window and shift by an hour.
|
||||
const targetStart = new Date()
|
||||
|
||||
targetStart.setUTCDate(targetStart.getUTCDate() + 2)
|
||||
targetStart.setUTCHours(21, 0, 0, 0) // 23:00 Europe/Amsterdam summer
|
||||
|
||||
const targetEnd = new Date(targetStart.getTime() + 60 * 60 * 1000)
|
||||
|
||||
const fmt = (d: Date) => d.toISOString().slice(0, 19).replace('T', ' ')
|
||||
|
||||
const movePayload = {
|
||||
performance_id: fixtures.performance_id,
|
||||
target_stage_id: fixtures.stage_id,
|
||||
target_start_at: fmt(targetStart),
|
||||
target_end_at: fmt(targetEnd),
|
||||
target_lane: 0,
|
||||
version: 0,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// First move: expect 200, perf.version moves 0 → 1
|
||||
// -----------------------------------------------------------------
|
||||
const first = await request.post(moveUrl, {
|
||||
data: movePayload,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Idempotency-Key': 'e2e-409-first',
|
||||
},
|
||||
})
|
||||
|
||||
expect(first.status(), `First move expected 200, got ${first.status()}: ${await first.text()}`).toBe(200)
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Second move with the SAME (now-stale) version=0: expect 409
|
||||
// -----------------------------------------------------------------
|
||||
const second = await request.post(moveUrl, {
|
||||
data: movePayload,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Idempotency-Key': 'e2e-409-second',
|
||||
},
|
||||
})
|
||||
|
||||
expect(second.status()).toBe(409)
|
||||
|
||||
const conflictBody = await second.json() as {
|
||||
success?: boolean
|
||||
message?: string
|
||||
errors?: { conflict?: string }
|
||||
}
|
||||
|
||||
// Contract assertions — drift here means the SPA's Zod schema for
|
||||
// MoveTimetableConflictResponse needs an update.
|
||||
expect(conflictBody.errors?.conflict).toBe('version_mismatch')
|
||||
expect(typeof conflictBody.message).toBe('string')
|
||||
|
||||
await context.close()
|
||||
})
|
||||
})
|
||||
60
apps/app/tests/playwright-e2e/utils/auth.ts
Normal file
60
apps/app/tests/playwright-e2e/utils/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { APIRequestContext, BrowserContext } from '@playwright/test'
|
||||
|
||||
import { readFixtures } from './fixtures'
|
||||
|
||||
// Login helper for Playwright e2e tests.
|
||||
//
|
||||
// Uses the SPA-style Bearer-via-cookie flow (see
|
||||
// api/.../Traits/SetAuthCookie.php): POST /api/v1/auth/login returns a
|
||||
// `crewli_app_token` httpOnly cookie. Subsequent /api/v1/* requests in
|
||||
// the same browser context carry it automatically because Playwright's
|
||||
// request fixture inherits cookies from the BrowserContext.
|
||||
//
|
||||
// NOT sanctum-stateful (CSRF-cookie + X-XSRF-TOKEN). The custom
|
||||
// CookieBearerToken middleware (api/bootstrap/app.php) reads the
|
||||
// auth cookie directly.
|
||||
|
||||
export interface LoginResult {
|
||||
request: APIRequestContext
|
||||
userId: string
|
||||
organisationId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the e2e baseline user against a freshly-seeded
|
||||
* Laravel test server. Returns the request context (auth cookie set)
|
||||
* and the user/org IDs from the response.
|
||||
*/
|
||||
export async function loginAsBaselineUser(context: BrowserContext): Promise<LoginResult> {
|
||||
const fixtures = readFixtures()
|
||||
|
||||
const response = await context.request.post('/api/v1/auth/login', {
|
||||
data: {
|
||||
email: fixtures.user_email,
|
||||
password: fixtures.user_password,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Login failed: ${response.status()} — ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const body = await response.json() as {
|
||||
success: boolean
|
||||
data: {
|
||||
user: {
|
||||
id: string
|
||||
organisations: Array<{ id: string; role: string }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
request: context.request,
|
||||
userId: body.data.user.id,
|
||||
organisationId: body.data.user.organisations[0].id,
|
||||
}
|
||||
}
|
||||
42
apps/app/tests/playwright-e2e/utils/fixtures.ts
Normal file
42
apps/app/tests/playwright-e2e/utils/fixtures.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Reads the seeded IDs that E2EBaselineSeeder wrote during globalSetup.
|
||||
// Tests use these IDs to construct API URLs and verify response shapes.
|
||||
|
||||
export interface E2EFixtures {
|
||||
user_email: string
|
||||
user_password: string
|
||||
organisation_id: string
|
||||
event_id: string
|
||||
stage_id: string
|
||||
performance_id: string
|
||||
}
|
||||
|
||||
const FIXTURES_PATH = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'api',
|
||||
'storage',
|
||||
'app',
|
||||
'e2e-fixtures.json',
|
||||
)
|
||||
|
||||
let cached: E2EFixtures | null = null
|
||||
|
||||
export function readFixtures(): E2EFixtures {
|
||||
if (cached)
|
||||
return cached
|
||||
|
||||
cached = JSON.parse(readFileSync(FIXTURES_PATH, 'utf-8')) as E2EFixtures
|
||||
|
||||
return cached
|
||||
}
|
||||
342
dev-docs/ARCH-TESTING.md
Normal file
342
dev-docs/ARCH-TESTING.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Crewli — Test Architecture
|
||||
|
||||
> Authoritative reference for test-tier choices in the SPA. Read this
|
||||
> before adding a new test. Linked from `CLAUDE.md`.
|
||||
|
||||
This document describes:
|
||||
|
||||
1. The test pyramid Crewli uses, and what each tier is for
|
||||
2. When to use which tier (decision tree)
|
||||
3. Mock-vs-real-backend rules
|
||||
4. Visual baseline workflow
|
||||
5. CI integration status
|
||||
6. Conventions and anti-patterns
|
||||
7. Vuetify-during-PrimeVue-migration: the temporary state in test infra
|
||||
8. Host setup requirements
|
||||
9. Deferred work (BACKLOG references)
|
||||
|
||||
---
|
||||
|
||||
## 1. Test pyramid and scope per layer
|
||||
|
||||
Crewli runs five test tiers in the SPA. Each has a narrow purpose;
|
||||
overlap is wasted work, gaps are silent risk. Pick the tier whose
|
||||
purpose matches what you're actually verifying.
|
||||
|
||||
### Tier 1 — Unit (Vitest + happy-dom)
|
||||
|
||||
**Run via:** `pnpm test` (filtered by `tests/unit/**`)
|
||||
**Environment:** Node + happy-dom, single module graph
|
||||
**Cost:** ~20 ms per test
|
||||
**For:** Pure logic, schema parsing, store reducers, isolated composable
|
||||
behaviour. No DOM. Fastest tier; safe for pre-commit if we ever add it.
|
||||
|
||||
### Tier 2 — Component (Playwright Component Testing)
|
||||
|
||||
**Run via:** `pnpm test:component`
|
||||
**Environment:** Real Chromium via `@playwright/experimental-ct-vue`
|
||||
**Cost:** ~300 ms per test (incl. Chromium reuse)
|
||||
**For:** Single-component verification. DOM rendering, click/keyboard,
|
||||
prop propagation, slot rendering, CSS resolution. Mocks API at axios
|
||||
layer. Provider stack (Vuetify [TEMP], Pinia, TanStack Query, Router) is
|
||||
wired in `apps/app/playwright/index.ts`'s `beforeMount` hook.
|
||||
|
||||
### Tier 3 — Integration (Playwright CT, multi-component)
|
||||
|
||||
**Run via:** Same `pnpm test:component` runner; placement convention
|
||||
distinguishes integration from single-component.
|
||||
**Cost:** ~500 ms per test
|
||||
**For:** Page-level mounting with mocked API responses. Tests
|
||||
cross-component coordination (drag from Wachtrij → canvas, popover
|
||||
→ mutation flow). Same provider stack as Tier 2.
|
||||
|
||||
### Tier 4 — Visual regression (Playwright CT, `@visual` tag)
|
||||
|
||||
**Run via:** `pnpm test:visual` (verify), `pnpm test:visual:update`
|
||||
(regenerate baselines)
|
||||
**Environment:** Real Chromium driving the canonical prototype HTML
|
||||
served by a tiny static-server fixture (`tests/playwright-ct/visual/
|
||||
static-server.mjs`).
|
||||
**Cost:** ~1.2 s per test
|
||||
**For:** Pixel baselines against canonical visual sources. The
|
||||
prototype HTML at `resources/Crewli - Artist Timetable Management/
|
||||
crewli-timetable.html` is the source of truth for Artist Management
|
||||
surfaces. F4 (component migration) extends visual coverage to live
|
||||
SPA components against the prototype.
|
||||
|
||||
### Tier 5 — E2E (Playwright)
|
||||
|
||||
**Run via:** `pnpm test:e2e`
|
||||
**Environment:** Real Laravel test server (`php artisan serve --port=
|
||||
8001`, DB `crewli_test`) + real Chromium browser context.
|
||||
**Cost:** ~5 s for the suite (includes migrate:fresh + seed)
|
||||
**For:** Contract verification end-to-end. Real network, real auth,
|
||||
real DB transactions. Currently only the 409-conflict optimistic-
|
||||
locking contract test (TEST-CONTRACT-001). Add tests sparingly — this
|
||||
is the most expensive tier.
|
||||
|
||||
---
|
||||
|
||||
## 2. When to use what — decision tree
|
||||
|
||||
*First match wins — stop at the first YES.*
|
||||
|
||||
```
|
||||
Is the thing under test pure logic with no DOM?
|
||||
└─ YES → Unit (Vitest + happy-dom)
|
||||
|
||||
Is it a single component? (props, events, slots, CSS, keyboard)
|
||||
└─ YES → Component (Playwright CT)
|
||||
|
||||
Is it cross-component coordination, but no real backend?
|
||||
└─ YES → Integration (Playwright CT)
|
||||
|
||||
Is it a contract between SPA and backend (request/response shape)?
|
||||
└─ YES → E2E (Playwright + Laravel)
|
||||
|
||||
Is it visual fidelity to a canonical baseline?
|
||||
└─ YES → Visual (Playwright CT, @visual tag)
|
||||
```
|
||||
|
||||
**Don't pick by speed.** Pick by what you're verifying. A unit test
|
||||
that mocks the backend cannot catch a contract-drift bug; an e2e test
|
||||
for pure logic is wasted CI time.
|
||||
|
||||
---
|
||||
|
||||
## 3. Mock-vs-real-backend choice rules
|
||||
|
||||
### Mock when
|
||||
|
||||
- The test verifies SPA behaviour given a known response shape
|
||||
- Backend availability would slow the test below the relevant tier's
|
||||
cost budget
|
||||
- The path under test is independent of transactional / auth
|
||||
semantics
|
||||
|
||||
### Real backend when
|
||||
|
||||
- The test verifies the contract between frontend and backend (Zod
|
||||
schema vs. PHP Resource shape)
|
||||
- Authentication or authorisation flows are involved
|
||||
- Optimistic-locking, idempotency, or other multi-request semantics
|
||||
matter
|
||||
|
||||
**Anti-pattern: matching mocks to schemas.** Don't mock with the same
|
||||
shape your Zod schema validates — that creates self-confirming bias
|
||||
where both sides agree but neither matches reality. This is the
|
||||
exact failure mode TEST-CONTRACT-001 was created to catch (timetable-
|
||||
stabilization B5).
|
||||
|
||||
---
|
||||
|
||||
## 4. Visual baseline workflow
|
||||
|
||||
### Capturing baselines
|
||||
|
||||
```bash
|
||||
pnpm test:visual:update
|
||||
```
|
||||
|
||||
Diffs are reviewed in PRs. Baselines live at:
|
||||
```
|
||||
apps/app/tests/playwright-ct/__screenshots__/visual/<spec-path>/<name>.png
|
||||
```
|
||||
Tracked via Git LFS (see `.gitattributes`). Pixel tolerance:
|
||||
`maxDiffPixelRatio: 0.001` (0.1%) per `playwright-ct.config.ts`.
|
||||
|
||||
### Updating baselines (intentional UX change)
|
||||
|
||||
1. Make the UX change (component edit, token edit, …)
|
||||
2. Run `pnpm test:visual:update` locally
|
||||
3. Review the diff PNG manually — does the new baseline match the
|
||||
intended UX?
|
||||
4. Commit baseline + UX change in the **same PR**. Reviewer can
|
||||
compare baseline change against the UX intent.
|
||||
5. Never update baselines to "make tests pass" without a UX-justified
|
||||
reason in the PR description.
|
||||
|
||||
### Updating baselines (unintentional diff in CI)
|
||||
|
||||
1. Determine if the diff is environmental (font hinting, OS rendering,
|
||||
timezone-based date formatting) or a real regression.
|
||||
2. Environmental → consider tightening determinism (lock fonts, fake
|
||||
timers, fixed locale) before tweaking tolerance.
|
||||
3. Real regression → fix the regression, not the baseline.
|
||||
|
||||
### Composite-over-isolated strategy (B3 baselines)
|
||||
|
||||
Some surfaces enumerated in RFC §A.3's baseline list are captured as
|
||||
composite views rather than individual block-state baselines. Reason:
|
||||
the prototype's DOM exposes status only via inline `style.background`,
|
||||
no `data-*` attributes. Isolated locators (e.g. by artist name) lock
|
||||
the test to specific seed data and silently rot if data changes.
|
||||
|
||||
The current 5 baselines cover the visual vocabulary:
|
||||
|
||||
| File | Captures |
|
||||
| ----------------------------- | ------------------------------------------------------- |
|
||||
| `canvas-friday.png` | Status colors, b2b indicators, multi-lane stacking |
|
||||
| `canvas-saturday.png` | Conflict ring, capacity warning |
|
||||
| `stage-row-multilane.png` | First row in isolation |
|
||||
| `wachtrij-populated.png` | Sidebar list rendering, status badges, counts |
|
||||
| `popover.png` | Block-click popover layout |
|
||||
|
||||
9 additional surfaces are documented as `test.skip()` in
|
||||
`tests/playwright-ct/visual/prototype.spec.ts` with the gap reason.
|
||||
F4 component migration adds isolated baselines using stable
|
||||
`data-test-id` attributes on Vue components.
|
||||
|
||||
---
|
||||
|
||||
## 5. CI integration
|
||||
|
||||
**Status: deferred.** The repo currently has no CI runner configured.
|
||||
Local development workflow:
|
||||
|
||||
- Vitest (`pnpm test`) — tier 1, runs on demand
|
||||
- Playwright Component (`pnpm test:component`) — tiers 2–4, runs on
|
||||
demand
|
||||
- Playwright E2E (`pnpm test:e2e`) — tier 5, runs on demand against a
|
||||
developer-managed Laravel test server
|
||||
|
||||
CI design (Gitea Actions vs. GitHub Actions decision, Linux runner
|
||||
image with PHP+MySQL+Node+pnpm, screenshot-diff artifact upload,
|
||||
label-gated nightly e2e) is captured as `TEST-INFRA-002` in
|
||||
`dev-docs/BACKLOG.md`.
|
||||
|
||||
When CI lands:
|
||||
|
||||
- Pre-commit (lefthook): Vitest unit only. Fast, no Playwright launch.
|
||||
- PR-CI: Vitest unit + Playwright component + visual. Slower but full
|
||||
coverage.
|
||||
- Nightly / label-gated: Playwright e2e against real Laravel + MySQL.
|
||||
Most expensive tier.
|
||||
|
||||
---
|
||||
|
||||
## 6. Conventions
|
||||
|
||||
- **Test file naming:** `*.spec.ts` for Playwright (CT + e2e),
|
||||
`*.test.ts` for Vitest. The runner config glob keeps them apart.
|
||||
- **`@visual` tag:** required on all visual-regression tests so
|
||||
`--grep @visual` filters them.
|
||||
- **Provider stack for CT:** wired in `apps/app/playwright/index.ts`'s
|
||||
`beforeMount` hook, not at mount call time. Tests forward
|
||||
per-test overrides via `hooksConfig` (see
|
||||
`tests/playwright-ct/utils/mountWithProviders.ts`).
|
||||
- **E2E test isolation:** `globalSetup` runs `migrate:fresh + seed`
|
||||
once per `pnpm test:e2e` invocation. Tests within one run share DB
|
||||
state. Re-run = fresh DB.
|
||||
- **Pixel tolerance:** `maxDiffPixelRatio: 0.001` default
|
||||
(`playwright-ct.config.ts`). Per-test exceptions allowed if
|
||||
documented inline.
|
||||
- **Auth in e2e tests:** Bearer-via-cookie (`api/.../SetAuthCookie.php`).
|
||||
POST `/api/v1/auth/login` returns `crewli_app_token` httpOnly cookie.
|
||||
No CSRF dance, no Sanctum stateful flow. baseURL must be
|
||||
`localhost:8001` (matching the cookie's `domain=localhost`),
|
||||
**not** `127.0.0.1:8001`.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
1. **Mocking the same data shape that the schema validates** —
|
||||
creates self-confirming bias. Use real backend for contract tests
|
||||
(TEST-CONTRACT-001 catches this class of bug).
|
||||
2. **Updating baselines silently** without diff review or a UX-
|
||||
justified PR description.
|
||||
3. **Adding Playwright tests for pure logic** that Vitest can cover
|
||||
in 20 ms. Reserve Playwright for tests that need the browser.
|
||||
4. **Treating "small" UX changes as not needing visual updates** —
|
||||
there is no small visual change in an enterprise product; the
|
||||
user notices.
|
||||
5. **Brittle locators** by data values (artist names, stage names)
|
||||
instead of stable test IDs. F4 will add `data-test-id` to Vue
|
||||
components for this reason.
|
||||
|
||||
---
|
||||
|
||||
## 7. Vuetify in test infrastructure during the PrimeVue migration
|
||||
|
||||
`apps/app/playwright/index.ts`'s `beforeMount` hook registers Vuetify
|
||||
as a Vue plugin. This is **intentional temporary state**.
|
||||
|
||||
### Why
|
||||
|
||||
The current SPA still ships Vuetify. Component-level Playwright CT
|
||||
tests must mount components against the same UI framework the live
|
||||
app uses, otherwise they would test a non-existent surface. Stripping
|
||||
Vuetify from test infra now would make CT tests un-runnable until
|
||||
F3 lands PrimeVue.
|
||||
|
||||
### When it ends
|
||||
|
||||
F3 (PrimeVue foundation, RFC-WS-FRONTEND-PRIMEVUE §6) replaces the
|
||||
Vuetify plugin line in `playwright/index.ts` with PrimeVue and
|
||||
updates `tests/playwright-ct/components/sanity-vuetify.spec.ts` to
|
||||
its PrimeVue equivalent. Estimated effort: ~2 hours (mechanical
|
||||
swap, no architecture change).
|
||||
|
||||
### Why not abstract
|
||||
|
||||
The instinct of "abstract the UI framework provider so we can swap
|
||||
without touching test code" is a **deferred-cost trap** here:
|
||||
|
||||
1. We are NOT retaining Vuetify post-F3. The abstraction would itself
|
||||
need to be removed in F4 alongside the framework swap.
|
||||
2. The swap is mechanical (~2 hours). An abstraction layer would take
|
||||
longer to design well than the swap itself takes.
|
||||
3. Reviewers seeing "Vuetify in test infra in a PrimeVue migration
|
||||
sprint" should read this section + the JSDoc on
|
||||
`mountWithProviders.ts` for context.
|
||||
|
||||
The forbidden pattern: do not propose "let's make a `UIFrameworkPlugin`
|
||||
interface and dependency-inject the provider per test" during F2/F3.
|
||||
That's exactly the abstraction this section forbids.
|
||||
|
||||
---
|
||||
|
||||
## 8. Host setup requirements
|
||||
|
||||
For Playwright tests to run, the host must have:
|
||||
|
||||
- **Node v22+** with **pnpm 10+** (matching `apps/app/`'s expectations)
|
||||
- **Chromium** installed via `pnpm exec playwright install chromium`
|
||||
(downloads to `~/Library/Caches/ms-playwright` on macOS)
|
||||
- **Git LFS** installed (`brew install git-lfs` on macOS) and active
|
||||
(`git lfs install --skip-repo` to avoid hook conflict with lefthook;
|
||||
the LFS pre-push step is delegated through `lefthook.yml`)
|
||||
- **MySQL 8** running locally via `make services` for e2e tests, with
|
||||
the `crewli_test` database created via `make test-db-create`
|
||||
- **PHP 8.2+ + composer** for the Laravel test server in e2e tests
|
||||
- **`api/.env`** present with valid `APP_KEY` (e2e `globalSetup`
|
||||
inherits this; only `DB_DATABASE` is overridden to `crewli_test` on
|
||||
the command line)
|
||||
|
||||
### Known risks
|
||||
|
||||
- **`unpkg.com` dependency** — the prototype HTML loads React + Babel
|
||||
from unpkg.com via `<script src="https://unpkg.com/...">`. Local
|
||||
network outage or unpkg CDN issues will flake B3 baselines. Mitigation
|
||||
if it bites: vendor `react.umd.js` + `babel.min.js` into the
|
||||
prototype directory. Defer until it actually breaks.
|
||||
- **Test DB shared with PHPUnit** — `crewli_test` is used by both the
|
||||
PHPUnit suite (transaction-rollback per test) and the e2e fixture
|
||||
(migrate:fresh + seed once). Running them concurrently would
|
||||
collide. Lifecycle assumes serial execution, which is the realistic
|
||||
local-dev flow.
|
||||
|
||||
---
|
||||
|
||||
## 9. Deferred to BACKLOG
|
||||
|
||||
- **TEST-INFRA-002** — CI runner selection (Gitea Actions vs. GitHub
|
||||
Actions decision), runner image with PHP+MySQL+Node+pnpm, caching
|
||||
strategy, screenshot-diff artifact upload, label-gated nightly e2e.
|
||||
- F4 isolated component-level visual baselines (replacing the
|
||||
composite baselines in B3 with per-state baselines using stable
|
||||
`data-test-id` attributes).
|
||||
- Multi-context concurrent-edit e2e patterns — see TEST-INFRA-002 in BACKLOG.md
|
||||
- Multi-browser (Firefox, WebKit) baselines — Linux+Chromium only
|
||||
for v1 per RFC §A.5.
|
||||
- Mobile viewport baselines — desktop 1440×900 only for v1.
|
||||
- Soketi / WebSocket testing infrastructure when ART-15 lands.
|
||||
@@ -2304,7 +2304,28 @@ Removed both packages from `apps/app/package.json`, regenerated
|
||||
|
||||
**Refs:** Session 4 follow-up Step 1; `apps/app/src/components/timetable/AddPerformanceDialog.vue` and `apps/app/src/components/sections/CreateShiftDialog.vue` as canonical references.
|
||||
|
||||
### TEST-INFRA-001 — Migrate timetable component+a11y tests to Playwright Component Testing
|
||||
### TEST-INFRA-001 — Migrate timetable component+a11y tests to Playwright Component Testing ✅ Resolved
|
||||
|
||||
**Status:** Closed in `chore/test-infra-001` (commits `b8d18e6`,
|
||||
`82af117`, `f6509d9`, `2dfb1e8`). Sprint executed per
|
||||
RFC-WS-FRONTEND-PRIMEVUE Amendment A-1.
|
||||
|
||||
**Resolution:** Playwright + axe-core installed; CT and e2e runners
|
||||
configured; Git LFS enabled for screenshots; `mountWithProviders`
|
||||
helper established; full provider stack (Vuetify [TEMPORARY: replaced
|
||||
in F3], Pinia, TanStack Query, Memory-history Router) wired in
|
||||
`apps/app/playwright/index.ts`'s `beforeMount` hook. Existing
|
||||
402 Vitest+jsdom tests left unchanged per amendment §A.3 goal 5
|
||||
(natural replacement during F4 component migration).
|
||||
|
||||
**Deviations from original:**
|
||||
- **CI integration deferred** to TEST-INFRA-002 — no CI exists in repo
|
||||
today. Sprint scope cut to "passes locally" per amendment A-1
|
||||
acceptance terms.
|
||||
- Provider plugins wired in `playwright/index.ts` rather than at mount
|
||||
call time (Playwright CT API divergence from `@vue/test-utils`).
|
||||
Documented in `mountWithProviders.ts` JSDoc and
|
||||
`dev-docs/ARCH-TESTING.md` §6.
|
||||
|
||||
**Aanleiding:** Session 4 follow-up landed component-mount, integration,
|
||||
keyboard a11y, and axe-core tests on Vitest + jsdom as a deliberate
|
||||
@@ -2347,7 +2368,28 @@ helper (designed to translate cleanly into Playwright CT's `mount()` API).
|
||||
|
||||
---
|
||||
|
||||
### TEST-CONTRACT-001 — End-to-end 409 conflict contract test against running Laravel
|
||||
### TEST-CONTRACT-001 — End-to-end 409 conflict contract test against running Laravel ✅ Resolved
|
||||
|
||||
**Status:** Closed in `chore/test-infra-001` commit `2dfb1e8` (B4 of
|
||||
TEST-INFRA-001 sprint).
|
||||
|
||||
**Resolution:** `apps/app/tests/playwright-e2e/timetable/409-conflict.
|
||||
spec.ts` runs against a real Laravel test server (`php artisan serve
|
||||
--port=8001`) seeded by `api/database/seeders/E2EBaselineSeeder.php`
|
||||
via Playwright's `globalSetup`. Test asserts first-move 200 and
|
||||
second-move 409 with `errors.conflict: 'version_mismatch'`. The
|
||||
schema-drift bug class that motivated the entry (timetable-
|
||||
stabilization B5) is now caught end-to-end.
|
||||
|
||||
**Deviations from original:**
|
||||
- **Single-context replay** instead of two-browser-context concurrent
|
||||
edit. The 409 is server-determined by stored version, not by
|
||||
session identity, so single-context replay is functionally
|
||||
equivalent for contract validation. Multi-context concurrent-edit
|
||||
test is documented in ARCH-TESTING.md §9 as deferred to F4.
|
||||
- **UI rollback assertion** (popover toast, block snap-back) deferred
|
||||
to F4 UI-driven e2e — out of scope for B4 contract test.
|
||||
- CI integration deferred to TEST-INFRA-002.
|
||||
|
||||
**Aanleiding:** The 409 rollback path in `useTimetableMutations.move()`
|
||||
is currently asserted against a mocked axios response shape (Session 4
|
||||
@@ -2379,7 +2421,42 @@ contract-protection value per line of test code.
|
||||
|
||||
---
|
||||
|
||||
### TEST-VISUAL-001 — Visual regression baselines for PerformanceBlock states
|
||||
### TEST-VISUAL-001 — Visual regression baselines for PerformanceBlock states ✅ Resolved
|
||||
|
||||
**Status:** Closed in `chore/test-infra-001` commit `f6509d9` (B3 of
|
||||
TEST-INFRA-001 sprint).
|
||||
|
||||
**Resolution:** 5 composite baselines captured from the canonical
|
||||
prototype at `resources/Crewli - Artist Timetable Management/
|
||||
crewli-timetable.html` (note: actual filename, not "Crewli Timetable.
|
||||
html" as referenced in the older Aanleiding section below). Tests
|
||||
live in `apps/app/tests/playwright-ct/visual/prototype.spec.ts`,
|
||||
PNGs at `apps/app/tests/playwright-ct/__screenshots__/visual/
|
||||
prototype.spec.ts/`. Tracked via Git LFS.
|
||||
|
||||
| Baseline | Captures |
|
||||
| ----------------------------- | --------------------------------------------------- |
|
||||
| `canvas-friday.png` | Status colors, B2B indicators, multi-lane stacking |
|
||||
| `canvas-saturday.png` | Conflict ring, capacity warning |
|
||||
| `stage-row-multilane.png` | First row in isolation |
|
||||
| `wachtrij-populated.png` | Sidebar list, status badges, counts |
|
||||
| `popover.png` | Block-click popover layout |
|
||||
|
||||
**Deviations from original 8-state-minimum scope:**
|
||||
- Composite-over-isolated strategy. Prototype DOM exposes status only
|
||||
via inline `style.background`, no `data-*` attributes. Isolated-
|
||||
block locators by artist name would lock tests to specific seed
|
||||
data. Composite captures yield the same visual vocabulary in fewer
|
||||
more stable images. Documented in `dev-docs/ARCH-TESTING.md` §4.
|
||||
- 9 surfaces from RFC §A.3's enumerated list documented as
|
||||
`test.skip()` with gap reasons (cancelled status absent from
|
||||
prototype data, drag-mode flaky under simulated pointer events,
|
||||
empty-state surfaces unreachable from canonical seed). All
|
||||
deferred to F4 isolated component-level baselines using stable
|
||||
`data-test-id` attributes.
|
||||
- Pixel tolerance `maxDiffPixelRatio: 0.001` (0.1%) per RFC §A.6.
|
||||
- Linux+Chromium only per RFC §A.5; no Mac/Windows baselines.
|
||||
- CI integration deferred to TEST-INFRA-002.
|
||||
|
||||
**Aanleiding:** Status badge colors, capacity icon presence, B2B dots,
|
||||
conflict ring, and cascade-pulse animation are UX contracts encoded in
|
||||
@@ -2421,6 +2498,67 @@ TEST-CONTRACT-001.
|
||||
|
||||
---
|
||||
|
||||
### TEST-INFRA-002 — CI integration for Playwright + visual + e2e
|
||||
|
||||
**Aanleiding:** TEST-INFRA-001 sprint scope was cut to "passes
|
||||
locally" because no CI exists in this repo today. The test
|
||||
infrastructure is operational on developer machines (Vitest 402,
|
||||
Playwright CT smoke + sanity + 5 visual baselines, e2e 409
|
||||
contract test) but no automated gate prevents drift over time.
|
||||
|
||||
**Wat:**
|
||||
- **Decision:** Gitea Actions vs. GitHub Actions vs. self-hosted
|
||||
runner. Determines runner image availability, secrets management,
|
||||
and pipeline DSL. No deadline; surface when first review cycle
|
||||
feels drift without automated tests.
|
||||
- **Runner image** with PHP 8.2+ (composer, ext-bcmath, ext-zip),
|
||||
MySQL 8 (or service container), Node v22, pnpm 10, Chromium for
|
||||
Playwright. Probably layered on top of a stock `ubuntu-22.04`
|
||||
image with explicit installs to keep image size predictable.
|
||||
- **Caching strategy:**
|
||||
- `pnpm-store` keyed on `pnpm-lock.yaml` hash
|
||||
- `vendor/` keyed on `composer.lock` hash
|
||||
- `~/.cache/ms-playwright` keyed on Playwright version
|
||||
- `__screenshots__/` cached locally for diff base; fetched via
|
||||
Git LFS on baseline tests
|
||||
- **Pipeline jobs (suggested):**
|
||||
1. `lint+typecheck` — pnpm lint, pnpm typecheck (fast lane)
|
||||
2. `vitest` — pnpm test (fast lane, runs in parallel with #1)
|
||||
3. `playwright-component` — pnpm test:component (medium lane,
|
||||
after #1+#2 pass)
|
||||
4. `playwright-visual` — pnpm test:visual (medium lane, parallel
|
||||
with #3). Diff PNGs uploaded as artifacts on failure; PR
|
||||
comment with diff summary if runner supports it.
|
||||
5. `playwright-e2e` — pnpm test:e2e (slow lane). Likely **label-
|
||||
gated** or **nightly only**, not on every PR. Requires MySQL
|
||||
service + Laravel server; ~10× more expensive than CT.
|
||||
6. `phpunit` — composer test (medium lane). Independent of #3-#5.
|
||||
- **Screenshot-diff artifact upload** — failing visual tests upload
|
||||
expected.png + actual.png + diff.png as job artifacts. PR comment
|
||||
links to artifact download.
|
||||
- **Branch protection** — pre-merge required: lint, typecheck,
|
||||
vitest, playwright-component, playwright-visual, phpunit. e2e
|
||||
optional gate.
|
||||
- **E2E DB strategy in CI** — fresh `crewli_test` per workflow run
|
||||
via `make test-db-create` then `migrate:fresh + seed` from
|
||||
globalSetup. No state shared across runs (unlike local
|
||||
development).
|
||||
- Multi-browser-context e2e patterns for optimistic locking flows
|
||||
with UI rollback validation in the second context (cut #4 from
|
||||
TEST-INFRA-001 sprint)
|
||||
|
||||
**Trigger:** No explicit deadline. Surfaces when first review cycle
|
||||
feels drift without automated tests, OR when first regression slips
|
||||
through the local-only gates, OR when team scales beyond solo
|
||||
maintainer.
|
||||
|
||||
**Refs:** RFC-WS-FRONTEND-PRIMEVUE Amendment A-1 §A.7 DoD-17/19
|
||||
deferral; `chore/test-infra-001` sprint commits `b8d18e6`-`2dfb1e8`
|
||||
(local infrastructure operational); `dev-docs/ARCH-TESTING.md` §5
|
||||
(CI strategy stub).
|
||||
|
||||
---
|
||||
|
||||
### ART-S4-UX-PARITY — Timetable UX parity with prototype
|
||||
|
||||
**Aanleiding:** Manual browser testing after `fix/timetable-stabilization`
|
||||
|
||||
11
lefthook.yml
11
lefthook.yml
@@ -17,6 +17,10 @@ post-commit:
|
||||
run: bash .githooks/post-commit
|
||||
|
||||
pre-push:
|
||||
# piped: true forces serial execution. Both sync-check and git-lfs
|
||||
# read from stdin (git pipes the push refspec to pre-push hooks);
|
||||
# default parallel execution deadlocks with two stdin readers.
|
||||
piped: true
|
||||
commands:
|
||||
sync-check:
|
||||
run: bash .githooks/pre-push
|
||||
@@ -29,3 +33,10 @@ pre-push:
|
||||
# behaviour. (Pushing with zero new commits would be skipped
|
||||
# under lefthook but is a no-op for the sync-staleness warning
|
||||
# anyway, so behaviour stays effectively 1:1.)
|
||||
git-lfs:
|
||||
# `git lfs install --skip-repo` only sets up global clean/smudge
|
||||
# filters — it does NOT install the per-repo pre-push hook
|
||||
# (which would conflict with lefthook). We delegate the LFS
|
||||
# upload step here so screenshot baselines tracked via LFS get
|
||||
# pushed alongside their commits.
|
||||
run: git lfs pre-push {1} {2}
|
||||
|
||||
Reference in New Issue
Block a user