test(e2e): tests Playwright multi-stack — vrai navigateur, DB isolée, Stripe mocké
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:
parent
0db7ff877c
commit
59f81879d8
5
.gitignore
vendored
5
.gitignore
vendored
@ -31,3 +31,8 @@ apps/web/public/mockServiceWorker.js
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
e2e/playwright/.cache/
|
||||
220
apps/api/app/controllers/test_e2e_controller.ts
Normal file
220
apps/api/app/controllers/test_e2e_controller.ts
Normal 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)
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(() => {
|
||||
/**
|
||||
|
||||
@ -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'
|
||||
|
||||
178
docs/tech/e2e-tests.md
Normal file
178
docs/tech/e2e-tests.md
Normal 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
91
e2e/playwright.config.ts
Normal 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
41
e2e/setup-db.sh
Executable 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."
|
||||
138
e2e/tests/billing-trial.spec.ts
Normal file
138
e2e/tests/billing-trial.spec.ts
Normal 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
96
e2e/tests/helpers/api.ts
Normal 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 + ré-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
58
e2e/tests/signup.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
11
package.json
11
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user