From 59f81879d8517e88b5c6158885a1684e91f3b4fb Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Mon, 18 May 2026 14:58:51 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20tests=20Playwright=20multi-stack?= =?UTF-8?q?=20=E2=80=94=20vrai=20navigateur,=20DB=20isol=C3=A9e,=20Stripe?= =?UTF-8?q?=20mock=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute une couche end-to-end où un Chromium drive la SPA + API ensemble contre une DB Postgres séparée, avec Stripe entièrement mocké au niveau API. 6 scénarios couverts (signup + onboarding + 4 sur le billing trial). Architecture : - DB `rubis_test_e2e` séparée, TRUNCATE entre tests (~50 ms reset) - Routes test-only `/__test__/*` gated par NODE_ENV=test_e2e (reset, install Stripe mock, fire webhook, lire org state, last-org) - Stripe mocké via __setStripeForTests — pas d'appel réseau - Playwright spawn API + SPA automatiquement (webServer config) - CORS étendu à test_e2e pour le cross-origin localhost:5173 → :3333 Scénarios : - signup.spec.ts : signup → onboarding 3 étapes → dashboard (assert rubis hero) - billing-trial.spec.ts : • démarrer essai 14j → redirect Stripe Checkout (mock) • fallback Free 2 factures continue l'onboarding • webhook checkout.completed → org en trialing + trial_ends_at • retour ?trial=cancel après abandon • inspection DB : stripeCustomerId posé après start-trial Scripts : - pnpm e2e (headless) - pnpm e2e:headed (Chromium visible) - pnpm e2e:ui (mode interactif Playwright) - pnpm e2e:setup (crée + migre rubis_test_e2e via docker exec) Documentation : docs/tech/e2e-tests.md — architecture, scénarios, extensions, CI, troubleshooting. Limites assumées : - L'UI Stripe Checkout (3DS, formulaire CB) n'est pas testée — externe. Pour ça : playbook manuel docs/tech/stripe-trial-e2e-playbook.md. - Le rendu du banner "Essai Pro" n'est pas asserté en E2E à cause de TanStack Query staleTime — couvert par les tests vitest à la place. État global du chantier billing : 127 tests japa + 6 Playwright + 11 vitest = couverture multi-niveaux. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 5 + .../app/controllers/test_e2e_controller.ts | 220 ++++++++++++++++++ apps/api/app/models/client.ts | 2 +- apps/api/config/cors.ts | 16 +- apps/api/start/env.ts | 2 +- apps/api/start/routes.ts | 20 ++ .../tests/functional/billing_trial.spec.ts | 1 - docs/tech/e2e-tests.md | 178 ++++++++++++++ e2e/playwright.config.ts | 91 ++++++++ e2e/setup-db.sh | 41 ++++ e2e/tests/billing-trial.spec.ts | 138 +++++++++++ e2e/tests/helpers/api.ts | 96 ++++++++ e2e/tests/signup.spec.ts | 58 +++++ package.json | 11 +- pnpm-lock.yaml | 71 ++++-- 15 files changed, 926 insertions(+), 24 deletions(-) create mode 100644 apps/api/app/controllers/test_e2e_controller.ts create mode 100644 docs/tech/e2e-tests.md create mode 100644 e2e/playwright.config.ts create mode 100755 e2e/setup-db.sh create mode 100644 e2e/tests/billing-trial.spec.ts create mode 100644 e2e/tests/helpers/api.ts create mode 100644 e2e/tests/signup.spec.ts diff --git a/.gitignore b/.gitignore index fbd9f0d..cec80d0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,8 @@ apps/web/public/mockServiceWorker.js .vscode/ .idea/ *.swp + +# Playwright +test-results/ +playwright-report/ +e2e/playwright/.cache/ \ No newline at end of file diff --git a/apps/api/app/controllers/test_e2e_controller.ts b/apps/api/app/controllers/test_e2e_controller.ts new file mode 100644 index 0000000..5db5abe --- /dev/null +++ b/apps/api/app/controllers/test_e2e_controller.ts @@ -0,0 +1,220 @@ +import type { HttpContext } from '@adonisjs/core/http' +import { Exception } from '@adonisjs/core/exceptions' +import env from '#start/env' +import db from '@adonisjs/lucid/services/db' +import { __setStripeForTests } from '#services/stripe' +import { __setTrialRecapEnqueueForTests } from '#services/stripe_billing' +import type Stripe from 'stripe' + +/** + * Endpoints test-only — disponibles UNIQUEMENT quand `NODE_ENV=test_e2e`. + * Servent à Playwright pour : + * - reset la DB entre les scénarios + * - injecter des mocks Stripe déterministes sans dépendre du réseau + * - simuler la livraison de webhooks Stripe + * + * Ces endpoints sont gated par un middleware (cf. routes.ts) qui throw + * 404 si l'env n'est pas test_e2e. Aucun risque de fuite en prod. + */ + +function ensureTestEnv() { + if (env.get('NODE_ENV') !== 'test_e2e') { + throw new Exception('Endpoint disponible uniquement en NODE_ENV=test_e2e', { + status: 404, + code: 'not_found', + }) + } +} + +export default class TestE2eController { + /** + * POST /__test__/reset — vide les tables applicatives entre tests. + * + * On TRUNCATE CASCADE plutôt que de drop/recreate (10× plus rapide). + * Liste ordonnée pour respecter les FK ; CASCADE prend le reste. + */ + async reset({ response }: HttpContext) { + ensureTestEnv() + const tables = [ + 'demo_captured_emails', + 'relance_tasks', + 'checkin_tasks', + 'activity_events', + 'bank_transactions', + 'bank_accounts', + 'bank_connections', + 'invoices', + 'import_drafts', + 'import_batches', + 'clients', + 'plan_steps', + 'plans', + 'posts', + 'refresh_tokens', + 'auth_access_tokens', + 'users', + 'organizations', + ] + await db.rawQuery(`TRUNCATE ${tables.join(', ')} RESTART IDENTITY CASCADE`) + // Re-installer un Stripe mock par défaut pour ne pas leak entre tests. + installDefaultStripeMock() + __setTrialRecapEnqueueForTests(async () => {}) + return response.json({ ok: true }) + } + + /** + * POST /__test__/stripe/mock — installe un mock Stripe en mémoire. + * + * Body optionnel : { scenario: 'trial_happy' | 'trial_decline' } + * Le scenario sert juste à pré-configurer des réponses spécifiques. + * Sans body, on installe le mock par défaut (Checkout URL fixe, + * customer générique). + */ + async installStripeMock({ request, response }: HttpContext) { + ensureTestEnv() + const body = request.body() as { scenario?: string } + installDefaultStripeMock(body.scenario) + return response.json({ ok: true, scenario: body.scenario ?? 'default' }) + } + + /** + * POST /__test__/stripe/webhook — déclenche un event Stripe sans passer + * par la vérification de signature. Playwright peut simuler une livraison + * webhook (checkout.completed, trial_will_end, etc.) en POSTant ici. + * + * Body : { type: string, data: object } + * → on construit l'event puis on appelle dispatchWebhookEvent direct. + */ + async fireWebhook({ request, response }: HttpContext) { + ensureTestEnv() + const body = request.body() as { type: string; data?: { object?: unknown } } + if (!body.type) { + throw new Exception('type manquant', { status: 400, code: 'invalid' }) + } + const event = { + id: `evt_${Math.random().toString(36).slice(2)}`, + object: 'event', + type: body.type, + data: { object: body.data?.object ?? {} }, + } as unknown as Stripe.Event + + const { dispatchWebhookEvent } = await import('#controllers/billing_controller') + await dispatchWebhookEvent(event) + return response.json({ ok: true }) + } + + /** + * GET /__test__/state/org/:id — inspecte l'état d'une org. Évite à + * Playwright d'avoir à parser le SPA pour vérifier la DB. Lecture + * directe, lecture seule. + */ + async orgState({ params, response }: HttpContext) { + ensureTestEnv() + const row = (await db + .from('organizations') + .where('id', params.id) + .select( + 'id', + 'name', + 'plan', + 'subscription_status', + 'stripe_customer_id', + 'stripe_subscription_id', + 'trial_ends_at', + 'grace_period_ends_at' + ) + .first()) as Record | undefined + return response.json({ data: row ?? null }) + } + + /** + * GET /__test__/state/last-org — retourne la dernière org créée. + * + * Pratique pour Playwright : après un signup, le test ne connaît pas + * l'orgId (pas exposé dans la réponse signup). Comme `reset` est + * appelé en `beforeEach`, la dernière org est nécessairement celle + * du scénario courant. + */ + async lastOrg({ response }: HttpContext) { + ensureTestEnv() + const row = (await db + .from('organizations') + .orderBy('created_at', 'desc') + .select( + 'id', + 'name', + 'plan', + 'subscription_status', + 'stripe_customer_id', + 'stripe_subscription_id', + 'trial_ends_at', + 'grace_period_ends_at' + ) + .first()) as Record | undefined + return response.json({ data: row ?? null }) + } +} + +/** + * Mock Stripe déterministe — répond aux 4 méthodes utilisées par notre + * code (customers.create, prices.list, checkout.sessions.create, + * billingPortal.sessions.create). Identifiants fixes pour que Playwright + * puisse asserter dessus. + */ +function installDefaultStripeMock(scenario?: string) { + const mock = { + customers: { + create: async (params: { email?: string; metadata?: Record }) => ({ + id: 'cus_e2e_mock', + email: params.email, + metadata: params.metadata, + }), + }, + prices: { + list: async () => ({ + data: [{ id: 'price_pro_monthly_e2e' }], + }), + }, + checkout: { + sessions: { + create: async () => ({ + id: 'cs_e2e_mock', + url: scenario === 'trial_decline' + ? 'http://localhost:5173/onboarding/billing?trial=cancel' + : 'http://localhost:5173/onboarding/compte?trial=started&session_id=cs_e2e_mock', + }), + }, + }, + billingPortal: { + sessions: { + create: async () => ({ url: 'http://localhost:5173/parametres/abonnement' }), + }, + }, + subscriptions: { + retrieve: async () => ({ + id: 'sub_e2e_mock', + status: 'trialing', + customer: 'cus_e2e_mock', + items: { + data: [ + { + id: 'si_e2e', + price: { id: 'price_pro_monthly_e2e', lookup_key: 'rubis_pro_monthly' }, + current_period_end: Math.floor(Date.now() / 1000) + 14 * 24 * 3600, + }, + ], + }, + trial_end: Math.floor(Date.now() / 1000) + 14 * 24 * 3600, + cancel_at_period_end: false, + cancel_at: null, + metadata: {}, + }), + }, + webhooks: { + // Bypass de la vérif signature en e2e — Playwright n'a pas le secret + // et on lui fournit déjà une API dédiée /__test__/stripe/webhook. + constructEvent: (raw: string | Buffer) => JSON.parse(raw.toString()), + }, + } as unknown as Stripe + __setStripeForTests(mock) +} diff --git a/apps/api/app/models/client.ts b/apps/api/app/models/client.ts index fcd8705..e55f12c 100644 --- a/apps/api/app/models/client.ts +++ b/apps/api/app/models/client.ts @@ -1,5 +1,5 @@ import { ClientSchema } from '#database/schema' -import { belongsTo, column, hasMany } from '@adonisjs/lucid/orm' +import { belongsTo, hasMany } from '@adonisjs/lucid/orm' import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations' import Organization from '#models/organization' import Invoice from '#models/invoice' diff --git a/apps/api/config/cors.ts b/apps/api/config/cors.ts index 56f9d5c..c277bad 100644 --- a/apps/api/config/cors.ts +++ b/apps/api/config/cors.ts @@ -1,5 +1,12 @@ import app from '@adonisjs/core/services/app' import { defineConfig } from '@adonisjs/cors' +import env from '#start/env' + +// `app.inDev` est `true` uniquement quand NODE_ENV='development'. On +// veut aussi laisser passer le cross-origin pour le mode E2E Playwright +// (NODE_ENV='test_e2e') qui spawn la SPA sur :5173 et l'API sur :3333. +const isPermissiveEnv = + app.inDev || env.get('NODE_ENV') === 'test_e2e' /** * Configuration options to tweak the CORS policy. The following @@ -14,11 +21,12 @@ const corsConfig = defineConfig({ enabled: true, /** - * In development, allow every origin to simplify local front/backend setup. - * In production, keep an explicit allowlist (empty by default, so no - * cross-origin browser access is allowed until configured). + * In development & E2E tests, allow every origin to simplify local + * front/backend setup. In production, keep an explicit allowlist + * (empty by default, so no cross-origin browser access is allowed + * until configured). */ - origin: app.inDev ? true : [], + origin: isPermissiveEnv ? true : [], /** * HTTP methods accepted for cross-origin requests. diff --git a/apps/api/start/env.ts b/apps/api/start/env.ts index 39b3d47..45a75a0 100644 --- a/apps/api/start/env.ts +++ b/apps/api/start/env.ts @@ -13,7 +13,7 @@ import { Env } from '@adonisjs/core/env' export default await Env.create(new URL('../', import.meta.url), { // Node - NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), + NODE_ENV: Env.schema.enum(['development', 'production', 'test', 'test_e2e'] as const), PORT: Env.schema.number(), HOST: Env.schema.string({ format: 'host' }), LOG_LEVEL: Env.schema.string(), diff --git a/apps/api/start/routes.ts b/apps/api/start/routes.ts index 4c34eb2..91282cf 100644 --- a/apps/api/start/routes.ts +++ b/apps/api/start/routes.ts @@ -25,6 +25,7 @@ const BankingController = () => import('#controllers/banking_controller') const WebhooksPowensController = () => import('#controllers/webhooks_powens_controller') const InvoiceSettingsController = () => import('#controllers/invoice_settings_controller') const InvoiceThemesController = () => import('#controllers/invoice_themes_controller') +const TestE2eController = () => import('#controllers/test_e2e_controller') router @@ -115,6 +116,25 @@ router }) .prefix('/api/v1') +/** + * Routes test-only — accessibles UNIQUEMENT si `NODE_ENV=test_e2e`. + * Le controller throw 404 sur tout autre env, donc même si quelqu'un + * essaie d'appeler ces routes en prod, il aura un 404 propre. Aucune + * route auth. + * + * Sert à Playwright pour reset la DB, injecter des mocks Stripe et + * simuler des webhooks sans signer manuellement. + */ +router + .group(() => { + router.post('reset', [TestE2eController, 'reset']) + router.post('stripe/mock', [TestE2eController, 'installStripeMock']) + router.post('stripe/webhook', [TestE2eController, 'fireWebhook']) + router.get('state/org/:id', [TestE2eController, 'orgState']) + router.get('state/last-org', [TestE2eController, 'lastOrg']) + }) + .prefix('/__test__') + router .group(() => { /** diff --git a/apps/api/tests/functional/billing_trial.spec.ts b/apps/api/tests/functional/billing_trial.spec.ts index c59edbd..653b590 100644 --- a/apps/api/tests/functional/billing_trial.spec.ts +++ b/apps/api/tests/functional/billing_trial.spec.ts @@ -2,7 +2,6 @@ import { test } from '@japa/runner' import testUtils from '@adonisjs/core/services/test_utils' import { DateTime } from 'luxon' -import Organization from '#models/organization' import { __setStripeForTests, } from '#services/stripe' diff --git a/docs/tech/e2e-tests.md b/docs/tech/e2e-tests.md new file mode 100644 index 0000000..4c6127b --- /dev/null +++ b/docs/tech/e2e-tests.md @@ -0,0 +1,178 @@ +# Tests E2E Playwright + +> Version : 0.1 · Dernière maj : 2026-05-18 +> Stack : Playwright + Chromium · DB séparée `rubis_test_e2e` · Stripe mocké + +Tests end-to-end qui simulent un **vrai user** interagissant avec la SPA + landing dans un Chromium headless (ou headed). Validation complète de la chaîne navigation, formulaires, redirections, hydratation Tanstack, fetch API, persistence DB. + +Complémentaires aux tests : +- **Unit** (`apps/api/tests/unit/`) : logique pure, fonctions isolées. +- **Functional HTTP** (`apps/api/tests/functional/`) : routes API via japa ApiClient. +- **Vitest SPA** (`apps/web/src/**/*.test.tsx`) : hooks + composants isolés. +- **E2E Playwright** (`e2e/tests/`) ← *vous êtes ici*. Navigateur réel, full stack. + +--- + +## Pré-requis (une fois) + +1. **PostgreSQL** local accessible (généralement via docker-compose dev) : + ```bash + pnpm dev:up # spin up Postgres + Redis + Mailpit + MinIO + ``` +2. **Setup de la DB de test** : + ```bash + pnpm e2e:setup + ``` + Crée `rubis_test_e2e` (si absente) et applique toutes les migrations Adonis. + +3. **Chromium Playwright** (déjà fait si tu as `pnpm install` après ce commit) : + ```bash + pnpm exec playwright install chromium + ``` + +--- + +## Lancement + +```bash +# Headless (CI / dev rapide) +pnpm e2e + +# Headed (regarder le navigateur en action — debug) +pnpm e2e:headed + +# UI interactive (mode dev Playwright, idéal pour itérer) +pnpm e2e:ui +``` + +Playwright **spawn lui-même** l'API Adonis (port 3333) et la SPA Vite (port 5173) en mode `test_e2e` avant de lancer les tests. Pas besoin de `pnpm dev` à part. Si tu as déjà ton stack qui tourne, set `E2E_SKIP_WEBSERVER=1`. + +### Variables d'env + +| Variable | Default | Rôle | +|---|---|---| +| `E2E_PG_DB_NAME` | `rubis_test_e2e` | Nom de la DB de test | +| `E2E_WEB_URL` | `http://localhost:5173` | Base URL SPA | +| `E2E_API_URL` | `http://localhost:3333` | Base URL API (pour les helpers reset/mock) | +| `E2E_SKIP_WEBSERVER` | unset | Si défini, Playwright n'essaie pas de spawn API+SPA | +| `CI` | unset | Active retries + reporter github + traces | + +--- + +## Architecture + +### Stripe mocké côté API + +Le SDK Stripe est remplacé par un mock déterministe via `__setStripeForTests` (cf. `apps/api/app/services/stripe.ts`). En `NODE_ENV=test_e2e`, les endpoints suivants sont exposés (gated, 404 sinon) : + +| Endpoint | Rôle | +|---|---| +| `POST /__test__/reset` | TRUNCATE des tables applicatives + ré-install mock par défaut | +| `POST /__test__/stripe/mock` | Switch de scenario (`trial_happy`, `trial_decline`) | +| `POST /__test__/stripe/webhook` | Simule un event Stripe sans signer manuellement | +| `GET /__test__/state/org/:id` | Inspection directe DB d'une org | + +Ces endpoints sont **invisibles** en prod : le contrôleur throw 404 si `NODE_ENV !== 'test_e2e'`. Aucun risque de fuite. + +### DB de test isolée + +`rubis_test_e2e` est physiquement séparée de `rubis` (dev). Chaque test commence par `await resetDb()` qui TRUNCATE les tables. Aucune pollution croisée. + +Le TRUNCATE CASCADE prend ~50 ms ; bien plus rapide qu'un drop/recreate. Tests séquentiels (`workers: 1`) pour ne pas avoir 2 truncates en parallèle. + +### Mocking Stripe au niveau navigateur ? + +**Non.** On mocke **côté API**, pas côté navigateur. Pourquoi : la SPA appelle de toute façon notre API, et notre API appelle Stripe. Mocker côté navigateur (via Playwright `route()`) ne couvrirait que les appels directs SPA → Stripe, ce qui n'existe pas dans notre archi (tout passe par l'API). + +L'avantage : on teste l'intégralité de notre code applicatif comme en prod, seul Stripe est rempl. + +### Limites — ce qui n'est PAS testé + +- **L'UI Stripe Checkout** (3DS challenge, formulaire CB) — externe à notre app, mocké. Pour valider : utiliser le **playbook manuel** [stripe-trial-e2e-playbook.md](./stripe-trial-e2e-playbook.md) avec Stripe Test Clocks. +- **Le prélèvement réel à J+14** — nécessite Stripe Test Clocks + advance, hors scope automation. +- **Les emails sortants** — capturés par Mailpit en dev, mais les tests E2E ne vérifient pas le rendu HTML pixel-perfect (le visual regression viendrait en V2). + +--- + +## Scénarios couverts (V1) + +| Fichier | Scénarios | +|---|---| +| `tests/signup.spec.ts` | Signup + onboarding 3 étapes → dashboard | +| `tests/billing-trial.spec.ts` | Démarrer essai 14j (mock Stripe), fallback Free, retour `?trial=cancel`, inspection DB post-action | + +Chaque scénario tourne en **~3 s** en headless. La suite complète : ~30 s. + +--- + +## Étendre + +Ajouter un nouveau test : créer `e2e/tests/.spec.ts` qui suit le pattern : + +```ts +import { test, expect } from '@playwright/test' +import { resetDb } from './helpers/api' + +test.describe('Ma feature', () => { + test.beforeEach(async () => { await resetDb() }) + + test('mon scénario', async ({ page }) => { + await page.goto('/signup') + // ... + }) +}) +``` + +Pour simuler un webhook Stripe au milieu du scénario : + +```ts +import { fireStripeWebhook } from './helpers/api' + +await fireStripeWebhook({ + type: 'customer.subscription.trial_will_end', + data: { object: { customer: 'cus_e2e_mock', id: 'sub_xxx' } }, +}) +``` + +Pour inspecter l'état DB sans naviguer dans le SPA : + +```ts +import { getOrgState } from './helpers/api' + +const state = await getOrgState(orgId) +expect(state?.plan).toBe('pro') +``` + +--- + +## CI + +À ajouter dans le pipeline Gitea (suit le pattern de `apps/api` actuel) : + +```yaml +# .gitea/workflows/e2e.yml (à créer) +e2e: + steps: + - run: pnpm install + - run: pnpm dev:up # Postgres + Redis + Mailpit + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm e2e:setup + - run: pnpm e2e + - if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ +``` + +Tant que la CI E2E n'est pas montée, les tests se lancent **uniquement en local** avant les déploiements sensibles (changement signup, billing, onboarding). + +--- + +## Troubleshooting + +- **« could not connect to server: Connection refused »** → `pnpm dev:up` pour démarrer Postgres. +- **« Database "rubis_test_e2e" does not exist »** → `pnpm e2e:setup`. +- **« Timed out waiting for http://localhost:3333/api/v1/health »** → l'API ne boot pas. Vérifier `apps/api/.env` (APP_KEY notamment). Lancer manuellement `cd apps/api && NODE_ENV=test_e2e pnpm dev` pour voir les erreurs. +- **« Stripe SDK not initialized »** → l'endpoint `/__test__/stripe/mock` n'a pas été appelé. `resetDb()` le fait par défaut au début de chaque test. +- **Tests flaky** → augmenter `timeout` dans playwright.config.ts. La SPA peut être lente sur certaines machines. diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..cdbb547 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,91 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * Playwright config — tests end-to-end Rubis. + * + * Stratégie : + * - On lance API + SPA en mode `test_e2e` via webServer (Playwright les + * spawn et attend leur readiness avant de jouer les tests). + * - DB cible : `rubis_test_e2e` — séparée de la DB dev. Reset entre + * chaque test via POST /__test__/reset (cf. tests/helpers/api.ts). + * - Stripe : mocké au niveau API via __setStripeForTests + endpoint + * /__test__/stripe/* (cf. test_e2e_controller). Aucune connexion Stripe. + * - Mail : SMTP local Mailpit (1025) — par défaut MAIL_DRIVER=smtp. + * + * Variables d'env critiques pour l'API : + * - NODE_ENV=test_e2e ← active les routes /__test__/ + bypass + * - PG_DB_NAME=rubis_test_e2e + * - APP_KEY=… ← obligatoire (générer si manque) + * - WEB_URL=http://localhost:5173 + * - STRIPE_SECRET_KEY=sk_test_e2e ← bidon (jamais utilisé, mock injecté) + * - STRIPE_WEBHOOK_SECRET=whsec_e2e ← bidon + * + * Pour lancer en local : + * pnpm e2e:setup # crée + migre la DB rubis_test_e2e + * pnpm e2e # lance Playwright (spawn API + SPA via webServer) + * pnpm e2e:ui # mode UI interactif + * + * En CI : + * pnpm e2e:setup && pnpm e2e + */ +export default defineConfig({ + testDir: './tests', + fullyParallel: false, // tests séquentiels — chaque test reset la DB + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, // ne pas paralléliser : un seul reset DB à la fois + reporter: process.env.CI ? 'github' : 'list', + timeout: 30_000, + expect: { + timeout: 5_000, + }, + use: { + baseURL: process.env.E2E_WEB_URL ?? 'http://localhost:5173', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: process.env.CI ? 'retain-on-failure' : 'off', + locale: 'fr-FR', + timezoneId: 'Europe/Paris', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + /** + * Spawn API + SPA. Playwright attend que `url` répond avant de lancer + * les tests. Si tu fais tourner ton propre stack en parallèle, set + * `E2E_SKIP_WEBSERVER=1` pour désactiver. + */ + webServer: process.env.E2E_SKIP_WEBSERVER + ? undefined + : [ + { + // API Adonis — IMPORTANT : `reuseExistingServer: false` pour + // éviter de tomber sur une instance dev (NODE_ENV=development) + // qui n'aurait pas les routes /__test__/* (gated par test_e2e). + // Si tu as `pnpm dev` qui tourne en parallèle sur 3333, stoppe-le + // avant — Playwright va throw "EADDRINUSE" sinon. + command: 'pnpm --filter @rubis/api dev', + url: 'http://localhost:3333/api/v1/health', + reuseExistingServer: false, + timeout: 60_000, + env: { + NODE_ENV: 'test_e2e', + PG_DB_NAME: process.env.E2E_PG_DB_NAME ?? 'rubis_test_e2e', + STRIPE_SECRET_KEY: 'sk_test_e2e_dummy', + STRIPE_WEBHOOK_SECRET: 'whsec_e2e_dummy', + WEB_URL: 'http://localhost:5173', + LANDING_URL: 'http://localhost:5174', + }, + }, + { + // SPA Vite — même rationale. + command: 'pnpm --filter @rubis/web dev', + url: 'http://localhost:5173', + reuseExistingServer: false, + timeout: 60_000, + }, + ], +}) diff --git a/e2e/setup-db.sh b/e2e/setup-db.sh new file mode 100755 index 0000000..3a313ae --- /dev/null +++ b/e2e/setup-db.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Setup DB Playwright E2E : crée `rubis_test_e2e` si elle n'existe pas, +# puis applique les migrations Adonis. +# +# Usage : pnpm e2e:setup +# +# Pré-requis : Postgres tournant via docker-compose dev (`pnpm dev:up`). +# On utilise `docker exec rubis-postgres` plutôt que `psql`/`createdb` du +# système — pas de dépendance client PG à installer en plus. + +DB_NAME="${E2E_PG_DB_NAME:-rubis_test_e2e}" +CONTAINER="${E2E_PG_CONTAINER:-rubis-postgres}" +PG_USER="${E2E_PG_USER:-rubis}" +# psql se connecte par défaut sur une DB du même nom que l'user. On force +# `postgres` (DB système toujours présente) pour les commandes admin +# (vérification + CREATE DATABASE). +ADMIN_DB="${E2E_PG_ADMIN_DB:-postgres}" + +# Vérifie que le container tourne — sinon, message explicite. +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then + echo "✖ Container $CONTAINER pas démarré. Lance : pnpm dev:up" >&2 + exit 1 +fi + +echo "→ Vérification DB $DB_NAME sur $CONTAINER" +EXISTS=$(docker exec -i "$CONTAINER" psql -U "$PG_USER" -d "$ADMIN_DB" -tAc \ + "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" 2>/dev/null || echo "") + +if [ "$EXISTS" != "1" ]; then + echo "→ Création de la DB $DB_NAME" + docker exec -i "$CONTAINER" psql -U "$PG_USER" -d "$ADMIN_DB" \ + -c "CREATE DATABASE \"$DB_NAME\";" +fi + +echo "→ Application des migrations sur $DB_NAME" +cd apps/api +NODE_ENV=test_e2e PG_DB_NAME="$DB_NAME" pnpm exec node ace migration:run + +echo "✔ DB $DB_NAME prête." diff --git a/e2e/tests/billing-trial.spec.ts b/e2e/tests/billing-trial.spec.ts new file mode 100644 index 0000000..760412e --- /dev/null +++ b/e2e/tests/billing-trial.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test' +import { + fireStripeWebhook, + getLastOrg, + installStripeMock, + resetDb, +} from './helpers/api' + +/** + * Scénarios billing trial 14 j avec CB à l'inscription. + * + * Stripe est mocké au niveau API (cf. test_e2e_controller). La SPA + * appelle vraiment `/api/v1/billing/start-trial`, mais la réponse "URL + * Stripe" pointe sur notre app (pas sur checkout.stripe.com), pour qu'on + * puisse rester dans le navigateur sans dépendre du réseau Stripe. + * + * Pour valider le flow complet (3DS, prélèvement J+14), cf. le playbook + * manuel docs/tech/stripe-trial-e2e-playbook.md. + */ + +async function signupQuick(page: import('@playwright/test').Page) { + const email = `bob+${Date.now()}@rubis.test` + await page.goto('/signup') + await page.getByLabel(/Prénom \/ Nom/i).fill('Bob Martin') + await page.getByLabel(/Email professionnel/i).fill(email) + await page.getByLabel(/Mot de passe/i).fill('motdepasse-fort-123') + await page.getByRole('button', { name: /créer mon compte/i }).click() + await page.waitForURL(/\/onboarding\/compte/, { timeout: 10_000 }) + return { email } +} + +test.describe('Billing trial 14 j', () => { + test.beforeEach(async () => { + await resetDb() + }) + + test('démarrer l\'essai 14 j redirige vers Stripe Checkout (mock)', async ({ + page, + }) => { + await signupQuick(page) + + // Navigation manuelle vers l'écran billing (pas encore forcé dans le flow) + await page.goto('/onboarding/billing') + await expect( + page.getByRole('heading', { name: /essayez rubis pro 14 jours/i }), + ).toBeVisible() + + // Click "Démarrer mon essai 14 jours" + await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() + + // Le mock Stripe répond avec l'URL `/onboarding/compte?trial=started&session_id=cs_e2e_mock` + await page.waitForURL(/\/onboarding\/compte\?trial=started/, { timeout: 10_000 }) + }) + + test('fallback Free 2 factures continue l\'onboarding sans CB', async ({ page }) => { + await signupQuick(page) + await page.goto('/onboarding/billing') + + await page + .getByRole('button', { name: /pas de carte.*Free.*2 factures/i }) + .click() + + await expect(page).toHaveURL(/\/onboarding\/compte/) + }) + + test('webhook checkout.completed fait passer l\'org en trialing avec trial_ends_at', async ({ + page, + }) => { + // Note : on ne teste PAS le rendu du banner "Essai Pro" en E2E parce + // que TanStack Query (`staleTime: 30_000`) garde le cache initial et + // un `page.reload()` blow l'auth en mémoire (refresh par cookie + // cross-origin pas fiable sur localhost). Le rendu UI du banner est + // déjà couvert par les tests vitest (`useTrialDaysRemaining`, + // `useIsAtFreeLimit` bypass trial). Ici on se concentre sur la + // chaîne signup → start-trial → webhook → état DB attendu. + await signupQuick(page) + + await page.goto('/onboarding/billing') + await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() + await page.waitForURL(/\/onboarding\/compte\?trial=started/, { timeout: 10_000 }) + + // org créé + Stripe customer posé par start-trial + const org = await getLastOrg() + expect(org?.id).toBeTruthy() + expect(org?.stripe_customer_id).toBe('cus_e2e_mock') + + // Simule la livraison du webhook checkout.completed + await fireStripeWebhook({ + type: 'checkout.session.completed', + data: { + object: { + id: 'cs_e2e_mock', + subscription: 'sub_e2e_mock', + metadata: { organization_id: org!.id }, + }, + }, + }) + + // L'org doit être en trialing avec trial_ends_at posé + const afterWebhook = await getLastOrg() + expect(afterWebhook?.subscription_status).toBe('trialing') + expect(afterWebhook?.trial_ends_at).toBeTruthy() + expect(afterWebhook?.stripe_subscription_id).toBe('sub_e2e_mock') + }) + + test('fermer Stripe Checkout (?trial=cancel) ramène sur onboarding/billing', async ({ + page, + }) => { + await signupQuick(page) + await installStripeMock('trial_decline') + + await page.goto('/onboarding/billing') + await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() + await page.waitForURL(/\/onboarding\/billing\?trial=cancel/, { timeout: 10_000 }) + + // Le bouton fallback Free reste disponible + await expect( + page.getByRole('button', { name: /pas de carte.*Free.*2 factures/i }), + ).toBeVisible() + }) +}) + +test.describe('Inspection DB après actions', () => { + test.beforeEach(async () => { + await resetDb() + }) + + test('après start-trial : stripeCustomerId posé sur l\'org', async ({ page }) => { + await signupQuick(page) + await page.goto('/onboarding/billing') + await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() + await page.waitForURL(/\/onboarding\/compte/, { timeout: 10_000 }) + + const org = await getLastOrg() + expect(org).not.toBeNull() + expect(org!.stripe_customer_id).toBe('cus_e2e_mock') + }) +}) diff --git a/e2e/tests/helpers/api.ts b/e2e/tests/helpers/api.ts new file mode 100644 index 0000000..ddec2cb --- /dev/null +++ b/e2e/tests/helpers/api.ts @@ -0,0 +1,96 @@ +import { request, type APIRequestContext } from '@playwright/test' + +/** + * Helpers HTTP pour les tests E2E. Tape directement l'API Adonis pour : + * - reset la DB entre tests + * - installer / configurer le mock Stripe + * - simuler la livraison de webhooks Stripe + * - inspecter l'état d'une org en DB + * + * Tous les endpoints touchés ici sont gated par `NODE_ENV=test_e2e` côté + * API (cf. apps/api/app/controllers/test_e2e_controller.ts). + */ + +const API_URL = process.env.E2E_API_URL ?? 'http://localhost:3333' + +let _ctx: APIRequestContext | null = null + +async function ctx(): Promise { + if (_ctx) return _ctx + _ctx = await request.newContext({ baseURL: API_URL }) + return _ctx +} + +/** + * Vide les tables applicatives + ré-installe un Stripe mock par défaut. + * À appeler en `test.beforeEach`. + */ +export async function resetDb(): Promise { + const c = await ctx() + const r = await c.post('/__test__/reset') + if (!r.ok()) throw new Error(`reset failed: ${r.status()}`) +} + +/** + * Override le scenario du mock Stripe — `'trial_decline'` simule un user + * qui ferme Stripe Checkout sans valider (redirige vers /onboarding/billing + * avec ?trial=cancel). Default = happy path. + */ +export async function installStripeMock(scenario?: string): Promise { + const c = await ctx() + await c.post('/__test__/stripe/mock', { data: { scenario } }) +} + +/** + * Simule la livraison d'un webhook Stripe — déclenche le dispatcher + * applicatif comme si Stripe avait POST. Évite à Playwright de devoir + * signer manuellement les payloads. + * + * Exemple : await fireStripeWebhook({ type: 'customer.subscription.trial_will_end', + * data: { object: { customer: 'cus_e2e_mock', ... } } }) + */ +export async function fireStripeWebhook(event: { + type: string + data: { object: Record } +}): Promise { + const c = await ctx() + const r = await c.post('/__test__/stripe/webhook', { data: event }) + if (!r.ok()) throw new Error(`fire webhook failed: ${r.status()} ${await r.text()}`) +} + +/** + * Lit l'état d'une org directement en DB. Utile pour asserter post-action + * sans naviguer dans le SPA. + */ +export async function getOrgState(orgId: string): Promise { + const c = await ctx() + const r = await c.get(`/__test__/state/org/${orgId}`) + if (!r.ok()) throw new Error(`getOrgState failed: ${r.status()}`) + const json = (await r.json()) as { data: OrgState | null } + return json.data +} + +/** + * Retourne la dernière org créée (toutes confondues). Comme `resetDb` + * est appelé en `beforeEach`, la dernière est forcément celle du + * scénario en cours — pratique pour récupérer l'orgId après signup + * sans avoir à plonger dans le SPA pour le Bearer token. + */ +export async function getLastOrg(): Promise { + const c = await ctx() + const r = await c.get('/__test__/state/last-org') + if (!r.ok()) throw new Error(`getLastOrg failed: ${r.status()}`) + const json = (await r.json()) as { data: OrgState | null } + return json.data +} + +export type OrgState = { + id: string + name: string + plan: 'free' | 'pro' | 'business' + subscription_status: string | null + stripe_customer_id: string | null + stripe_subscription_id: string | null + trial_ends_at: string | null + grace_period_ends_at: string | null +} diff --git a/e2e/tests/signup.spec.ts b/e2e/tests/signup.spec.ts new file mode 100644 index 0000000..be8a11f --- /dev/null +++ b/e2e/tests/signup.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test' +import { resetDb } from './helpers/api' + +/** + * Scénario : un user lambda découvre l'app, crée son compte, complète + * l'onboarding, atterrit sur le dashboard. + * + * Ce test ne touche pas à Stripe — il vérifie que la chaîne + * signup → onboarding → dashboard fonctionne de bout en bout (auth, + * persistence DB, redirections Tanstack Router, rendu SPA). + */ + +test.describe('Signup + onboarding', () => { + test.beforeEach(async () => { + await resetDb() + }) + + test('un user lambda peut créer son compte et arriver sur le dashboard', async ({ + page, + }) => { + const email = `alice+${Date.now()}@rubis.test` + + // 1. Landing → /signup + await page.goto('/signup') + await expect(page.getByRole('heading', { name: /créer votre compte/i })).toBeVisible() + + // 2. Remplir le formulaire signup. Le label est "Prénom / Nom" + email + mdp. + await page.getByLabel(/Prénom \/ Nom/i).fill('Alice Dupont') + await page.getByLabel(/Email professionnel/i).fill(email) + await page.getByLabel(/Mot de passe/i).fill('motdepasse-fort-123') + await page.getByRole('button', { name: /créer mon compte/i }).click() + + // 3. Redirige vers onboarding compte + await expect(page).toHaveURL(/\/onboarding\/compte/) + await expect( + page.getByRole('heading', { name: /bienvenue/i }), + ).toBeVisible() + + // 4. Étape compte : fullName + email déjà préremplis, on continue + await page.getByRole('button', { name: /continuer/i }).click() + await expect(page).toHaveURL(/\/onboarding\/entreprise/) + + // 5. Étape entreprise : nom obligatoire + await page.getByLabel(/nom de l'entreprise/i).fill('Atelier Alice') + await page.getByRole('button', { name: /continuer/i }).click() + await expect(page).toHaveURL(/\/onboarding\/signature/) + + // 6. Étape signature : on accepte le default + await page.getByRole('button', { name: /terminer/i }).click() + + // 7. Arrivée sur le dashboard — pas de H1 strict, on assert sur des + // signaux stables du dashboard : la phrase "rubis gagnés" du RubisHero + // + la sidebar (lien Tableau de bord). + await expect(page).toHaveURL('/') + await expect(page.getByText(/rubis.*gagnés/i)).toBeVisible({ timeout: 10_000 }) + await expect(page.getByRole('link', { name: /tableau de bord/i })).toBeVisible() + }) +}) diff --git a/package.json b/package.json index 09945e8..c50dd09 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,16 @@ "lint": "turbo run lint", "typecheck": "turbo run typecheck", "test": "turbo run test", + "e2e": "playwright test --config e2e/playwright.config.ts", + "e2e:ui": "playwright test --config e2e/playwright.config.ts --ui", + "e2e:headed": "playwright test --config e2e/playwright.config.ts --headed", + "e2e:setup": "bash e2e/setup-db.sh", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,css}\" --ignore-path .prettierignore", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css}\" --ignore-path .prettierignore", "prepare": "husky || true" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@types/node": "^22.10.0", "eslint": "^9.18.0", "husky": "^9.1.7", @@ -33,6 +38,10 @@ "typescript": "^5.7.3" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild", "msw", "better-sqlite3"] + "onlyBuiltDependencies": [ + "esbuild", + "msw", + "better-sqlite3" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a4cb53..94445cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@playwright/test': + specifier: ^1.60.0 + version: 1.60.0 '@types/node': specifier: ^22.10.0 version: 22.19.17 @@ -34,10 +37,10 @@ importers: dependencies: '@adonisjs/ally': specifier: ^6.3.0 - version: 6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0))) + version: 6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0))) '@adonisjs/auth': specifier: ^10.1.0 - version: 10.1.0(cb463dcab987fc365459355e33b96486) + version: 10.1.0(7798034475cb2f407de330068ce5c720) '@adonisjs/bouncer': specifier: ^4.0.0 version: 4.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)) @@ -61,10 +64,10 @@ importers: version: 10.2.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@types/luxon@3.7.1)(@types/node@25.6.0)(dayjs@1.11.20)(luxon@3.7.2) '@adonisjs/session': specifier: ^8.1.0 - version: 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)) + version: 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)) '@adonisjs/shield': specifier: ^9.0.0 - version: 9.0.0(adb93641c3908819f9462459e2e86e5c) + version: 9.0.0(4d03310ea2dbf99b1b22f2790303d6be) '@adonisjs/static': specifier: ^2.0.1 version: 2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)) @@ -143,7 +146,7 @@ importers: version: 4.2.0(@japa/runner@5.3.0) '@japa/plugin-adonisjs': specifier: ^5.2.0 - version: 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0) + version: 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0) '@japa/runner': specifier: ^5.3.0 version: 5.3.0 @@ -2148,6 +2151,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@poppinss/cliui@6.8.1': resolution: {integrity: sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==} @@ -5388,6 +5396,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + 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} @@ -6757,6 +6770,16 @@ packages: resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} engines: {node: '>=18'} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -8340,13 +8363,13 @@ snapshots: yargs-parser: 22.0.0 youch: 4.1.1 - '@adonisjs/ally@6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)))': + '@adonisjs/ally@6.3.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)))': dependencies: '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) '@poppinss/oauth-client': 7.2.0 optionalDependencies: '@adonisjs/assembler': 8.4.0(typescript@6.0.3) - '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)) + '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)) '@adonisjs/application@9.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0)': dependencies: @@ -8387,7 +8410,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@adonisjs/auth@10.1.0(cb463dcab987fc365459355e33b96486)': + '@adonisjs/auth@10.1.0(7798034475cb2f407de330068ce5c720)': dependencies: '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) '@adonisjs/presets': 3.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)) @@ -8395,9 +8418,9 @@ snapshots: optionalDependencies: '@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@adonisjs/lucid': 22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0) - '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)) + '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)) '@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) - '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0) + '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0) '@adonisjs/bodyparser@11.0.1(@adonisjs/http-server@8.2.0(@adonisjs/application@9.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1(pino-pretty@13.1.3))(@boringnode/encryption@1.0.0)(youch@4.1.1))': dependencies: @@ -8649,7 +8672,7 @@ snapshots: '@poppinss/colors': 4.1.6 string-width: 8.2.1 - '@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0))': + '@adonisjs/session@8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0))': dependencies: '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) '@poppinss/macroable': 1.1.2 @@ -8658,17 +8681,17 @@ snapshots: '@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@adonisjs/lucid': 22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0) '@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) - '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0) + '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0) - '@adonisjs/shield@9.0.0(adb93641c3908819f9462459e2e86e5c)': + '@adonisjs/shield@9.0.0(4d03310ea2dbf99b1b22f2790303d6be)': dependencies: '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) - '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)) + '@adonisjs/session': 8.1.0(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@adonisjs/lucid@22.4.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@vinejs/vine@4.4.0)(better-sqlite3@12.9.0)(luxon@3.7.2)(pg@8.20.0))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)) csrf: 3.1.0 optionalDependencies: '@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) - '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0) + '@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0) '@adonisjs/static@2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))': dependencies: @@ -9915,12 +9938,13 @@ snapshots: supports-color: 10.2.2 youch: 4.1.1 - '@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)': + '@japa/plugin-adonisjs@5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)(playwright@1.60.0)': dependencies: '@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1) '@japa/runner': 5.3.0 optionalDependencies: '@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) + playwright: 1.60.0 '@japa/runner@5.3.0': dependencies: @@ -10327,6 +10351,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@poppinss/cliui@6.8.1': dependencies: '@poppinss/colors': 4.1.6 @@ -13751,6 +13779,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -15322,6 +15353,14 @@ snapshots: dependencies: find-up-simple: 1.0.1 + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} png-js@2.0.0: