test(e2e): tests Playwright multi-stack — vrai navigateur, DB isolée, Stripe mocké
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m39s
Build & Deploy API / build-and-deploy (push) Successful in 2m30s
Build & Deploy Web / build-and-deploy (push) Successful in 1m21s

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 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-18 14:58:51 +02:00
parent 0db7ff877c
commit 59f81879d8
15 changed files with 926 additions and 24 deletions

5
.gitignore vendored
View File

@ -31,3 +31,8 @@ apps/web/public/mockServiceWorker.js
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
# Playwright
test-results/
playwright-report/
e2e/playwright/.cache/

View File

@ -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<string, unknown> | 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<string, unknown> | 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<string, unknown> }) => ({
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)
}

View File

@ -1,5 +1,5 @@
import { ClientSchema } from '#database/schema' 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 type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
import Organization from '#models/organization' import Organization from '#models/organization'
import Invoice from '#models/invoice' import Invoice from '#models/invoice'

View File

@ -1,5 +1,12 @@
import app from '@adonisjs/core/services/app' import app from '@adonisjs/core/services/app'
import { defineConfig } from '@adonisjs/cors' 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 * Configuration options to tweak the CORS policy. The following
@ -14,11 +21,12 @@ const corsConfig = defineConfig({
enabled: true, enabled: true,
/** /**
* In development, allow every origin to simplify local front/backend setup. * In development & E2E tests, allow every origin to simplify local
* In production, keep an explicit allowlist (empty by default, so no * front/backend setup. In production, keep an explicit allowlist
* cross-origin browser access is allowed until configured). * (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. * HTTP methods accepted for cross-origin requests.

View File

@ -13,7 +13,7 @@ import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), { export default await Env.create(new URL('../', import.meta.url), {
// Node // 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(), PORT: Env.schema.number(),
HOST: Env.schema.string({ format: 'host' }), HOST: Env.schema.string({ format: 'host' }),
LOG_LEVEL: Env.schema.string(), LOG_LEVEL: Env.schema.string(),

View File

@ -25,6 +25,7 @@ const BankingController = () => import('#controllers/banking_controller')
const WebhooksPowensController = () => import('#controllers/webhooks_powens_controller') const WebhooksPowensController = () => import('#controllers/webhooks_powens_controller')
const InvoiceSettingsController = () => import('#controllers/invoice_settings_controller') const InvoiceSettingsController = () => import('#controllers/invoice_settings_controller')
const InvoiceThemesController = () => import('#controllers/invoice_themes_controller') const InvoiceThemesController = () => import('#controllers/invoice_themes_controller')
const TestE2eController = () => import('#controllers/test_e2e_controller')
router router
@ -115,6 +116,25 @@ router
}) })
.prefix('/api/v1') .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 router
.group(() => { .group(() => {
/** /**

View File

@ -2,7 +2,6 @@ import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils' import testUtils from '@adonisjs/core/services/test_utils'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import Organization from '#models/organization'
import { import {
__setStripeForTests, __setStripeForTests,
} from '#services/stripe' } from '#services/stripe'

178
docs/tech/e2e-tests.md Normal file
View File

@ -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/<feature>.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.

91
e2e/playwright.config.ts Normal file
View File

@ -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,
},
],
})

41
e2e/setup-db.sh Executable file
View File

@ -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."

View File

@ -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')
})
})

96
e2e/tests/helpers/api.ts Normal file
View File

@ -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<APIRequestContext> {
if (_ctx) return _ctx
_ctx = await request.newContext({ baseURL: API_URL })
return _ctx
}
/**
* Vide les tables applicatives + -installe un Stripe mock par défaut.
* À appeler en `test.beforeEach`.
*/
export async function resetDb(): Promise<void> {
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<void> {
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<string, unknown> }
}): Promise<void> {
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<OrgState | null> {
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<OrgState | null> {
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
}

58
e2e/tests/signup.spec.ts Normal file
View File

@ -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()
})
})

View File

@ -19,11 +19,16 @@
"lint": "turbo run lint", "lint": "turbo run lint",
"typecheck": "turbo run typecheck", "typecheck": "turbo run typecheck",
"test": "turbo run test", "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": "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", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css}\" --ignore-path .prettierignore",
"prepare": "husky || true" "prepare": "husky || true"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@ -33,6 +38,10 @@
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": ["esbuild", "msw", "better-sqlite3"] "onlyBuiltDependencies": [
"esbuild",
"msw",
"better-sqlite3"
]
} }
} }

71
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
devDependencies: devDependencies:
'@playwright/test':
specifier: ^1.60.0
version: 1.60.0
'@types/node': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.19.17 version: 22.19.17
@ -34,10 +37,10 @@ importers:
dependencies: dependencies:
'@adonisjs/ally': '@adonisjs/ally':
specifier: ^6.3.0 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': '@adonisjs/auth':
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0(cb463dcab987fc365459355e33b96486) version: 10.1.0(7798034475cb2f407de330068ce5c720)
'@adonisjs/bouncer': '@adonisjs/bouncer':
specifier: ^4.0.0 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)) 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) 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': '@adonisjs/session':
specifier: ^8.1.0 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': '@adonisjs/shield':
specifier: ^9.0.0 specifier: ^9.0.0
version: 9.0.0(adb93641c3908819f9462459e2e86e5c) version: 9.0.0(4d03310ea2dbf99b1b22f2790303d6be)
'@adonisjs/static': '@adonisjs/static':
specifier: ^2.0.1 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)) 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) version: 4.2.0(@japa/runner@5.3.0)
'@japa/plugin-adonisjs': '@japa/plugin-adonisjs':
specifier: ^5.2.0 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': '@japa/runner':
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
@ -2148,6 +2151,11 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 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': '@poppinss/cliui@6.8.1':
resolution: {integrity: sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==} resolution: {integrity: sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==}
@ -5388,6 +5396,11 @@ packages:
fs-constants@1.0.0: fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} 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: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -6757,6 +6770,16 @@ packages:
resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==}
engines: {node: '>=18'} 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: pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -8340,13 +8363,13 @@ snapshots:
yargs-parser: 22.0.0 yargs-parser: 22.0.0
youch: 4.1.1 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: 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/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 '@poppinss/oauth-client': 7.2.0
optionalDependencies: optionalDependencies:
'@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@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)': '@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: dependencies:
@ -8387,7 +8410,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- babel-plugin-macros - babel-plugin-macros
'@adonisjs/auth@10.1.0(cb463dcab987fc365459355e33b96486)': '@adonisjs/auth@10.1.0(7798034475cb2f407de330068ce5c720)':
dependencies: 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/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)) '@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: optionalDependencies:
'@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@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/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/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))': '@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: dependencies:
@ -8649,7 +8672,7 @@ snapshots:
'@poppinss/colors': 4.1.6 '@poppinss/colors': 4.1.6
string-width: 8.2.1 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: 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/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 '@poppinss/macroable': 1.1.2
@ -8658,17 +8681,17 @@ snapshots:
'@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@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/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/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: 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/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 csrf: 3.1.0
optionalDependencies: optionalDependencies:
'@adonisjs/assembler': 8.4.0(typescript@6.0.3) '@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/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))': '@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: dependencies:
@ -9915,12 +9938,13 @@ snapshots:
supports-color: 10.2.2 supports-color: 10.2.2
youch: 4.1.1 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: 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/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 '@japa/runner': 5.3.0
optionalDependencies: optionalDependencies:
'@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0) '@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': '@japa/runner@5.3.0':
dependencies: dependencies:
@ -10327,6 +10351,10 @@ snapshots:
'@pkgr/core@0.2.9': {} '@pkgr/core@0.2.9': {}
'@playwright/test@1.60.0':
dependencies:
playwright: 1.60.0
'@poppinss/cliui@6.8.1': '@poppinss/cliui@6.8.1':
dependencies: dependencies:
'@poppinss/colors': 4.1.6 '@poppinss/colors': 4.1.6
@ -13751,6 +13779,9 @@ snapshots:
fs-constants@1.0.0: {} fs-constants@1.0.0: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@ -15322,6 +15353,14 @@ snapshots:
dependencies: dependencies:
find-up-simple: 1.0.1 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: {} pluralize@8.0.0: {}
png-js@2.0.0: png-js@2.0.0: