From 20d52aee2d24a5f09841ec61091e01ec9c49e673 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 19 Aug 2026 09:42:29 +0200 Subject: [PATCH] feat(web): migre les specs Cypress vers Cucumber/Gherkin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les tests e2e (apps/web/cypress/e2e/) étaient de simples specs Cypress (.cy.ts), sans lien avec Cucumber alors qu'apps/api utilise déjà Gherkin pour ses propres tests BDD. Intègre @badeball/cypress-cucumber-preprocessor pour écrire les scénarios utilisateurs en Gherkin des deux côtés, même vocabulaire. - cypress.config.ts : specPattern sur *.feature, wiring du préprocesseur (esbuild bundler + plugin cucumber) - Les 11 fichiers .cy.ts sont remplacés par des paires .feature/.steps.ts (co-localisées, même nom) — conversion complète, comportement équivalent (mêmes intercepts, mêmes assertions) - cypress/support/step_definitions/common.steps.ts : steps partagés entre features (connexion, navigation, assertions génériques de texte/URL/champ) — globaux à toute la suite, réutilisables tels quels - cypress/support/profile.ts : profil du compte "connecté" courant, assemblé au fil de plusieurs Given avant le premier visit/When - README : nouvelle section "Cucumber (apps/web)" (miroir de la section existante pour apps/api), mise à jour des références aux anciens noms de fichiers .cy.ts (déjà obsolètes avant ce changement) Vérification : impossible d'exécuter Cypress dans cet environnement (crash Electron/GPU au lancement, limitation déjà documentée dans le README — reproductible sur main, indépendante de ce changement). À la place : - les 447 steps Gherkin des 11 .feature ont été vérifiés programmatiquement contre les 165 patterns de step enregistrés : 0 non résolu, 0 ambigu - les 11 .feature parsent correctement avec le parser Gherkin officiel (57 scénarios au total) - tous les .steps.ts passent `biome check` (syntaxe + style) sans erreur - CYPRESS_INSTALL_BINARY déjà géré (voir PR précédente) — le binaire est bien présent localement (`cypress verify` OK), donc le blocage est spécifiquement le sandbox GPU de cet environnement, pas l'installation La vraie exécution reste à vérifier via le job `e2e` de la CI GitHub Actions sur cette PR — c'est le chemin déjà documenté dans le README pour cet environnement précis. --- README.md | 31 +- apps/web/cypress.config.ts | 22 +- apps/web/cypress/e2e/account.cy.ts | 68 - apps/web/cypress/e2e/account.feature | 40 + apps/web/cypress/e2e/account.steps.ts | 32 + apps/web/cypress/e2e/auth.cy.ts | 165 -- apps/web/cypress/e2e/auth.feature | 71 + apps/web/cypress/e2e/auth.steps.ts | 82 + apps/web/cypress/e2e/household-settings.cy.ts | 185 -- .../cypress/e2e/household-settings.feature | 90 + .../cypress/e2e/household-settings.steps.ts | 120 ++ apps/web/cypress/e2e/onboarding.cy.ts | 148 -- apps/web/cypress/e2e/onboarding.feature | 67 + apps/web/cypress/e2e/onboarding.steps.ts | 65 + apps/web/cypress/e2e/planning-page.cy.ts | 163 -- apps/web/cypress/e2e/planning-page.feature | 102 + apps/web/cypress/e2e/planning-page.steps.ts | 92 + apps/web/cypress/e2e/preferences.cy.ts | 81 - apps/web/cypress/e2e/preferences.feature | 35 + apps/web/cypress/e2e/preferences.steps.ts | 52 + apps/web/cypress/e2e/recipe-form.cy.ts | 178 -- apps/web/cypress/e2e/recipe-form.feature | 69 + apps/web/cypress/e2e/recipe-form.steps.ts | 159 ++ apps/web/cypress/e2e/recipes.cy.ts | 241 --- apps/web/cypress/e2e/recipes.feature | 98 + apps/web/cypress/e2e/recipes.steps.ts | 215 ++ apps/web/cypress/e2e/sidebar.cy.ts | 83 - apps/web/cypress/e2e/sidebar.feature | 47 + apps/web/cypress/e2e/sidebar.steps.ts | 36 + apps/web/cypress/e2e/smoke.cy.ts | 13 - apps/web/cypress/e2e/smoke.feature | 10 + apps/web/cypress/e2e/user-preferences.cy.ts | 54 - apps/web/cypress/e2e/user-preferences.feature | 31 + .../web/cypress/e2e/user-preferences.steps.ts | 15 + apps/web/cypress/support/profile.ts | 33 + .../support/step_definitions/common.steps.ts | 184 ++ apps/web/package.json | 2 + pnpm-lock.yaml | 1746 ++++++++++++++++- 38 files changed, 3540 insertions(+), 1385 deletions(-) delete mode 100644 apps/web/cypress/e2e/account.cy.ts create mode 100644 apps/web/cypress/e2e/account.feature create mode 100644 apps/web/cypress/e2e/account.steps.ts delete mode 100644 apps/web/cypress/e2e/auth.cy.ts create mode 100644 apps/web/cypress/e2e/auth.feature create mode 100644 apps/web/cypress/e2e/auth.steps.ts delete mode 100644 apps/web/cypress/e2e/household-settings.cy.ts create mode 100644 apps/web/cypress/e2e/household-settings.feature create mode 100644 apps/web/cypress/e2e/household-settings.steps.ts delete mode 100644 apps/web/cypress/e2e/onboarding.cy.ts create mode 100644 apps/web/cypress/e2e/onboarding.feature create mode 100644 apps/web/cypress/e2e/onboarding.steps.ts delete mode 100644 apps/web/cypress/e2e/planning-page.cy.ts create mode 100644 apps/web/cypress/e2e/planning-page.feature create mode 100644 apps/web/cypress/e2e/planning-page.steps.ts delete mode 100644 apps/web/cypress/e2e/preferences.cy.ts create mode 100644 apps/web/cypress/e2e/preferences.feature create mode 100644 apps/web/cypress/e2e/preferences.steps.ts delete mode 100644 apps/web/cypress/e2e/recipe-form.cy.ts create mode 100644 apps/web/cypress/e2e/recipe-form.feature create mode 100644 apps/web/cypress/e2e/recipe-form.steps.ts delete mode 100644 apps/web/cypress/e2e/recipes.cy.ts create mode 100644 apps/web/cypress/e2e/recipes.feature create mode 100644 apps/web/cypress/e2e/recipes.steps.ts delete mode 100644 apps/web/cypress/e2e/sidebar.cy.ts create mode 100644 apps/web/cypress/e2e/sidebar.feature create mode 100644 apps/web/cypress/e2e/sidebar.steps.ts delete mode 100644 apps/web/cypress/e2e/smoke.cy.ts create mode 100644 apps/web/cypress/e2e/smoke.feature delete mode 100644 apps/web/cypress/e2e/user-preferences.cy.ts create mode 100644 apps/web/cypress/e2e/user-preferences.feature create mode 100644 apps/web/cypress/e2e/user-preferences.steps.ts create mode 100644 apps/web/cypress/support/profile.ts create mode 100644 apps/web/cypress/support/step_definitions/common.steps.ts diff --git a/README.md b/README.md index 80f253a..bd9e3de 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ pnpm lint # Biome (lint + format check) pnpm lint:fix # Biome --write pnpm test # tests unitaires/intégration (Mocha, apps/api) pnpm --filter api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, apps/api) -pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement) +pnpm --filter web e2e # tests e2e (Cypress + Cucumber/Gherkin, démarre le serveur dev automatiquement) pnpm build # build de tous les workspaces ``` @@ -132,6 +132,31 @@ qui n'existent pas encore). > explicitement CommonJS ; les steps/world restent en `.ts` ESM classique et sont > chargés via `tsx` (`NODE_OPTIONS=--import=tsx`, voir le script `test:bdd`). +### Cucumber (apps/web) + +Les scénarios e2e (`apps/web/cypress/e2e/`) sont eux aussi écrits en Gherkin, via +[`@badeball/cypress-cucumber-preprocessor`](https://github.com/badeball/cypress-cucumber-preprocessor) +— même langage que les tests BDD d'`apps/api` ci-dessus, deux suites différentes +(frontend mocké vs backend contre une vraie base) mais un seul vocabulaire pour +décrire un scénario utilisateur : + +- `apps/web/cypress/e2e/*.feature` — scénarios en Given/When/Then, un fichier par + fonctionnalité (`auth.feature`, `recipes.feature`, `planning-page.feature`, …) +- `apps/web/cypress/e2e/*.steps.ts` — steps propres à une feature (co-localisé, + même nom que le `.feature` correspondant) +- `apps/web/cypress/support/step_definitions/common.steps.ts` — steps partagés par + plusieurs features (se connecter, naviguer, assertions génériques de texte/URL/ + champ) ; un step déjà défini là (ou dans un autre `*.steps.ts`) est réutilisable + tel quel dans n'importe quelle feature, pas besoin de le redéfinir +- `apps/web/cypress/support/profile.ts` — profil du compte "connecté" courant, + construit au fil de plusieurs `Given` (`I am signed in as "..." "..."`, `my + household id is ...`) avant le premier `cy.visit`/`When` du scénario + +Pour ajouter un scénario : écrire le `.feature`, réutiliser les steps existants +quand c'est possible (`common.steps.ts` ou un autre `*.steps.ts` — les +définitions de steps sont globales à toute la suite), sinon en ajouter un nouveau +dans le `.steps.ts` de la feature concernée. + ## Déploiement Une seule image Docker (`apps/api/Dockerfile`) sert à la fois l'API et le frontend @@ -363,8 +388,8 @@ page fetch son propre profil frais (`apiClient.me()`) au montage, et `AuthContext.refreshUser()` (nouveau) est appelé après une sauvegarde réussie du régime pour que le reste de l'app reste cohérent aussi. -Tests Cypress (`apps/web/cypress/e2e/`) : `smoke.cy.ts` + `auth.cy.ts` + -`home-planning.cy.ts` + `onboarding.cy.ts` + `household.cy.ts` mockent l'API via +Tests Cypress (`apps/web/cypress/e2e/*.feature`, scénarios Gherkin — voir +[Cucumber (apps/web)](#cucumber-appsweb) ci-dessous) mockent l'API via `cy.intercept` plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne provisionne pas de Postgres/API, seulement le serveur de dev Vite. Le comportement réel de l'API est couvert par les suites Mocha/Cucumber d'`apps/api` (contre une diff --git a/apps/web/cypress.config.ts b/apps/web/cypress.config.ts index 4efc7f8..e600e2b 100644 --- a/apps/web/cypress.config.ts +++ b/apps/web/cypress.config.ts @@ -1,9 +1,19 @@ +import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor"; +import { createEsbuildPlugin } from "@badeball/cypress-cucumber-preprocessor/esbuild"; +import createBundler from "@bahmutov/cypress-esbuild-preprocessor"; import { defineConfig } from "cypress"; export default defineConfig({ e2e: { baseUrl: "http://localhost:5173", - setupNodeEvents(on) { + // Scenarios live in `.feature` files (Gherkin), one per + // cypress/e2e/*.cy.ts spec this replaced — see cypress/e2e/README.md. + // Step definitions are picked up from the preprocessor's default globs: + // co-located `cypress/e2e//*.ts` for scenario-specific + // steps, `cypress/support/step_definitions/*.ts` for steps shared across + // features (auth, navigation, generic API mocking). + specPattern: "cypress/e2e/**/*.feature", + async setupNodeEvents(on, config) { // Disable GPU for headless/sandboxed environments (e.g. CI containers) // where no GPU device is available. on("before:browser:launch", (browser, launchOptions) => { @@ -12,6 +22,16 @@ export default defineConfig({ } return launchOptions; }); + + await addCucumberPreprocessorPlugin(on, config); + on( + "file:preprocessor", + createBundler({ + plugins: [createEsbuildPlugin(config)], + }), + ); + + return config; }, }, }); diff --git a/apps/web/cypress/e2e/account.cy.ts b/apps/web/cypress/e2e/account.cy.ts deleted file mode 100644 index 65a79da..0000000 --- a/apps/web/cypress/e2e/account.cy.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ErrorCode } from "@batch-cooking/shared"; - -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null, - dietId: null, -}; - -describe("Account settings (/parametres/compte)", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - }); - - it("shows the signed-in profile's identity", () => { - cy.visit("/parametres/compte"); - - cy.contains("Alice").should("be.visible"); - cy.contains("Martin").should("be.visible"); - cy.contains("alice@example.com").should("be.visible"); - }); - - it("shows an error and keeps the session when the password is wrong", () => { - cy.intercept("DELETE", "**/auth/me", { - statusCode: 401, - body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid password" }, - }).as("deleteAccount"); - - cy.visit("/parametres/compte"); - cy.contains("button", "Supprimer mon compte").click(); - cy.get("#deleteAccountPassword").type("wrong-password"); - cy.contains("button", "Confirmer la suppression").click(); - - cy.wait("@deleteAccount"); - cy.contains("Email ou mot de passe incorrect").should("be.visible"); - cy.url().should("include", "/parametres/compte"); - }); - - it("deletes the account and returns to the login page", () => { - cy.intercept("DELETE", "**/auth/me", { statusCode: 204 }).as("deleteAccount"); - - cy.visit("/parametres/compte"); - cy.contains("button", "Supprimer mon compte").click(); - cy.get("#deleteAccountPassword").type("correct-horse-battery-staple"); - cy.contains("button", "Confirmer la suppression").click(); - - cy.wait("@deleteAccount") - .its("request.body") - .should("deep.equal", { password: "correct-horse-battery-staple" }); - cy.url().should("include", "/login"); - }); - - it("cancels the deletion without calling the API", () => { - cy.intercept("DELETE", "**/auth/me").as("deleteAccount"); - - cy.visit("/parametres/compte"); - cy.contains("button", "Supprimer mon compte").click(); - cy.contains("button", "Annuler").click(); - - cy.contains("button", "Confirmer la suppression").should("not.exist"); - cy.get("@deleteAccount.all").should("have.length", 0); - }); -}); diff --git a/apps/web/cypress/e2e/account.feature b/apps/web/cypress/e2e/account.feature new file mode 100644 index 0000000..d7ad1af --- /dev/null +++ b/apps/web/cypress/e2e/account.feature @@ -0,0 +1,40 @@ +Feature: Account settings + As a signed-in user + I want to view my account details and be able to delete my account + So that I stay in control of my data + + Background: + Given I am signed in as "Alice" "Martin" + + Scenario: Shows the signed-in profile's identity + When I visit "/parametres/compte" + Then I should see "Alice" + And I should see "Martin" + And I should see "alice@example.com" + + Scenario: Shows an error and keeps the session when the password is wrong + Given the account deletion request will fail because the credentials are invalid + When I visit "/parametres/compte" + And I click the button "Supprimer mon compte" + And I fill in the "deleteAccountPassword" field with "wrong-password" + And I click the button "Confirmer la suppression" + Then the account deletion request should have been made + And I should see "Email ou mot de passe incorrect" + And the URL should include "/parametres/compte" + + Scenario: Deletes the account and returns to the login page + Given the account deletion request will succeed + When I visit "/parametres/compte" + And I click the button "Supprimer mon compte" + And I fill in the "deleteAccountPassword" field with "correct-horse-battery-staple" + And I click the button "Confirmer la suppression" + Then the account deletion request should have been made with password "correct-horse-battery-staple" + And the URL should include "/login" + + Scenario: Cancels the deletion without calling the API + Given the account deletion request is being watched + When I visit "/parametres/compte" + And I click the button "Supprimer mon compte" + And I click the button "Annuler" + Then I should not see "Confirmer la suppression" + And the account deletion request should not have been made diff --git a/apps/web/cypress/e2e/account.steps.ts b/apps/web/cypress/e2e/account.steps.ts new file mode 100644 index 0000000..a3f0163 --- /dev/null +++ b/apps/web/cypress/e2e/account.steps.ts @@ -0,0 +1,32 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; +import { ErrorCode } from "@batch-cooking/shared"; + +Given("the account deletion request will fail because the credentials are invalid", () => { + cy.intercept("DELETE", "**/auth/me", { + statusCode: 401, + body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid password" }, + }).as("deleteAccount"); +}); + +Given("the account deletion request will succeed", () => { + cy.intercept("DELETE", "**/auth/me", { statusCode: 204 }).as("deleteAccount"); +}); + +Given("the account deletion request is being watched", () => { + cy.intercept("DELETE", "**/auth/me").as("deleteAccount"); +}); + +Then("the account deletion request should have been made", () => { + cy.wait("@deleteAccount"); +}); + +Then( + "the account deletion request should have been made with password {string}", + (password: string) => { + cy.wait("@deleteAccount").its("request.body").should("deep.equal", { password }); + }, +); + +Then("the account deletion request should not have been made", () => { + cy.get("@deleteAccount.all").should("have.length", 0); +}); diff --git a/apps/web/cypress/e2e/auth.cy.ts b/apps/web/cypress/e2e/auth.cy.ts deleted file mode 100644 index e0baebd..0000000 --- a/apps/web/cypress/e2e/auth.cy.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { ErrorCode } from "@batch-cooking/shared"; - -// Mocks the API via cy.intercept — this job doesn't run a live backend (see -// .github/workflows/ci.yml), and it keeps these specs focused on frontend -// behavior. Backend behavior itself is covered by apps/api's Mocha/Cucumber -// suites against a real database. - -describe("Signup", () => { - it("creates a profile and starts the onboarding wizard (regime/household/allergens)", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - // The onboarding wizard's first step (see onboarding.cy.ts for the full - // walkthrough) is the regime step, which fetches the reference list. - cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); - cy.intercept("POST", "**/auth/signup", { - statusCode: 201, - body: { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null, - dietId: null, - }, - }).as("signup"); - - cy.visit("/signup"); - cy.get("#firstName").type("Alice"); - cy.get("#lastName").type("Martin"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Créer mon profil").click(); - - cy.wait("@signup"); - // Not the home page directly — signup hands off to the onboarding - // wizard first (RedirectIfAuthenticated no longer applies here, it's a - // RequireAuth-gated route of its own, see App.tsx). - cy.url().should("include", "/onboarding/regime"); - cy.contains("Étape 1 sur 3").should("be.visible"); - }); - - it("shows a client-side validation error without calling the API", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/signup").as("signup"); - - cy.visit("/signup"); - cy.get("#firstName").type("A"); - cy.get("#lastName").type("B"); - cy.get("#email").type("a@example.com"); - cy.get("#password").type("short"); - cy.contains("button", "Créer mon profil").click(); - - cy.contains("8 caractères minimum").should("be.visible"); - cy.get("@signup.all").should("have.length", 0); - }); - - it("shows the API's error when the email is already taken", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/signup", { - statusCode: 409, - body: { code: ErrorCode.EMAIL_ALREADY_IN_USE, message: "Email already in use" }, - }).as("signup"); - - cy.visit("/signup"); - cy.get("#firstName").type("Alice"); - cy.get("#lastName").type("Martin"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Créer mon profil").click(); - - cy.wait("@signup"); - cy.contains("Cet email est déjà utilisé").should("be.visible"); - }); -}); - -describe("Login", () => { - it("logs in and lands on the home page", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - cy.intercept("POST", "**/auth/login", { - statusCode: 200, - body: { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, - }, - }).as("login"); - - cy.visit("/login"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Se connecter").click(); - - cy.wait("@login"); - cy.contains("Bonjour Alice").should("be.visible"); - }); - - it("shows an error on invalid credentials", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/login", { - statusCode: 401, - body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid email or password" }, - }).as("login"); - - cy.visit("/login"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("wrong-password"); - cy.contains("button", "Se connecter").click(); - - cy.wait("@login"); - cy.contains("Email ou mot de passe incorrect").should("be.visible"); - }); -}); - -describe("Already authenticated", () => { - it("redirects away from /login to the home page", () => { - cy.intercept("GET", "**/auth/me", { - statusCode: 200, - body: { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, - }, - }); - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - - cy.visit("/login"); - cy.url().should("not.include", "/login"); - cy.contains("Bonjour Alice").should("be.visible"); - }); - - it("logs out and returns to the login page", () => { - cy.intercept("GET", "**/auth/me", { - statusCode: 200, - body: { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, - }, - }); - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); - - cy.visit("/"); - // "Se déconnecter" lives inside the account menu, opened by clicking - // the greeting button — see AppLayout.tsx's AccountMenu. - cy.contains("button", "Bonjour Alice").click(); - cy.contains("button", "Se déconnecter").click(); - - cy.wait("@logout"); - cy.url().should("include", "/login"); - }); -}); diff --git a/apps/web/cypress/e2e/auth.feature b/apps/web/cypress/e2e/auth.feature new file mode 100644 index 0000000..2d2a2ae --- /dev/null +++ b/apps/web/cypress/e2e/auth.feature @@ -0,0 +1,71 @@ +Feature: Signup and login + As a visitor + I want to create a profile or log into an existing one + So that I can access my household's batch-cooking planning + + Background: + Given I am not signed in + + Scenario: Signing up creates a profile and starts the onboarding wizard + Given the signup request will succeed + And the diets reference list is empty + When I sign up with: + | firstName | Alice | + | lastName | Martin | + | email | alice@example.com | + | password | correct-horse-battery-staple | + Then the signup request should have been made + And the URL should include "/onboarding/regime" + And I should see "Étape 1 sur 3" + + Scenario: Signing up shows a client-side validation error without calling the API + Given the signup request is being watched + When I sign up with: + | firstName | A | + | lastName | B | + | email | a@example.com | + | password | short | + Then I should see "8 caractères minimum" + And the signup request should not have been made + + Scenario: Signing up shows the API's error when the email is already taken + Given the signup request will fail because the email is already used + When I sign up with: + | firstName | Alice | + | lastName | Martin | + | email | alice@example.com | + | password | correct-horse-battery-staple | + Then the signup request should have been made + And I should see "Cet email est déjà utilisé" + + Scenario: Logging in lands on the home page + Given the login request will succeed + And the planning request returns nothing + When I log in with email "alice@example.com" and password "correct-horse-battery-staple" + Then the login request should have been made + And I should see "Bonjour Alice" + + Scenario: Logging in shows an error on invalid credentials + Given the login request will fail because the credentials are invalid + When I log in with email "alice@example.com" and password "wrong-password" + Then the login request should have been made + And I should see "Email ou mot de passe incorrect" + + Scenario: An already signed-in visitor is redirected away from the login page + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the planning request returns nothing + When I visit "/login" + Then the URL should not include "/login" + And I should see "Bonjour Alice" + + Scenario: Logging out returns to the login page + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the planning request returns nothing + And the logout request will succeed + When I visit "/" + And I open the account menu + And I click the button "Se déconnecter" + Then the logout request should have been made + And the URL should include "/login" diff --git a/apps/web/cypress/e2e/auth.steps.ts b/apps/web/cypress/e2e/auth.steps.ts new file mode 100644 index 0000000..9aaacc4 --- /dev/null +++ b/apps/web/cypress/e2e/auth.steps.ts @@ -0,0 +1,82 @@ +import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; +import { ErrorCode } from "@batch-cooking/shared"; + +const signupResponse = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null as number | null, + dietId: null, +}; + +Given("the signup request will succeed", () => { + cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup"); +}); + +Given("the signup request is being watched", () => { + cy.intercept("POST", "**/auth/signup").as("signup"); +}); + +Given("the signup request will fail because the email is already used", () => { + cy.intercept("POST", "**/auth/signup", { + statusCode: 409, + body: { code: ErrorCode.EMAIL_ALREADY_IN_USE, message: "Email already in use" }, + }).as("signup"); +}); + +Given("the diets reference list is empty", () => { + cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); +}); + +When("I sign up with:", (dataTable: DataTable) => { + const { firstName, lastName, email, password } = dataTable.rowsHash(); + cy.visit("/signup"); + cy.get("#firstName").type(firstName); + cy.get("#lastName").type(lastName); + cy.get("#email").type(email); + cy.get("#password").type(password); + cy.contains("button", "Créer mon profil").click(); +}); + +Then("the signup request should have been made", () => { + cy.wait("@signup"); +}); + +Then("the signup request should not have been made", () => { + cy.get("@signup.all").should("have.length", 0); +}); + +Given("the login request will succeed", () => { + cy.intercept("POST", "**/auth/login", { + statusCode: 200, + body: { ...signupResponse, houseId: 1 }, + }).as("login"); +}); + +Given("the login request will fail because the credentials are invalid", () => { + cy.intercept("POST", "**/auth/login", { + statusCode: 401, + body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid email or password" }, + }).as("login"); +}); + +When("I log in with email {string} and password {string}", (email: string, password: string) => { + cy.visit("/login"); + cy.get("#email").type(email); + cy.get("#password").type(password); + cy.contains("button", "Se connecter").click(); +}); + +Then("the login request should have been made", () => { + cy.wait("@login"); +}); + +Given("the logout request will succeed", () => { + cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); +}); + +Then("the logout request should have been made", () => { + cy.wait("@logout"); +}); diff --git a/apps/web/cypress/e2e/household-settings.cy.ts b/apps/web/cypress/e2e/household-settings.cy.ts deleted file mode 100644 index eb4acd1..0000000 --- a/apps/web/cypress/e2e/household-settings.cy.ts +++ /dev/null @@ -1,185 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. - -const adminProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null as number | null, - dietId: null, -}; - -const houseWithTwoMembers = { - id: 1, - name: "Chez Alice", - adminId: 1, - inviteCode: "ABCD2345", - members: [ - { id: 1, firstName: "Alice", lastName: "Martin" }, - { id: 2, firstName: "Bob", lastName: "Dupont" }, - ], -}; - -describe("Household settings (/parametres/foyer) — no household yet", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: adminProfile }); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); - }); - - it("offers to create or join a household", () => { - cy.visit("/parametres/foyer"); - - cy.contains("Créer un foyer").should("be.visible"); - cy.contains("Rejoindre un foyer").should("be.visible"); - }); - - it("creates a household", () => { - const createdHouse = { - id: 1, - name: "Chez Alice", - adminId: 1, - inviteCode: "ABCD2345", - members: [{ id: 1, firstName: "Alice", lastName: "Martin" }], - }; - // The page reloads `GET /house/current` right after creating succeeds — - // see the "deletes the household" test above for the same pattern. - let created = false; - cy.intercept("GET", "**/house/current", (req) => { - req.reply({ statusCode: 200, body: created ? createdHouse : null }); - }); - cy.intercept("POST", "**/house", (req) => { - created = true; - req.reply({ statusCode: 201, body: createdHouse }); - }).as("createHouse"); - - cy.visit("/parametres/foyer"); - cy.get("#houseName").type("Chez Alice"); - cy.contains("button", "Créer").click(); - - cy.wait("@createHouse").its("request.body").should("deep.equal", { name: "Chez Alice" }); - cy.contains("ABCD2345").should("be.visible"); - }); - - it("joins a household by invite code", () => { - let joined = false; - cy.intercept("GET", "**/house/current", (req) => { - req.reply({ statusCode: 200, body: joined ? houseWithTwoMembers : null }); - }); - cy.intercept("POST", "**/house/join", (req) => { - joined = true; - req.reply({ statusCode: 200, body: houseWithTwoMembers }); - }).as("joinHouse"); - - cy.visit("/parametres/foyer"); - cy.get("#inviteCode").type("abcd2345"); - cy.contains("button", "Rejoindre").click(); - - cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: "ABCD2345" }); - cy.contains("Bob Dupont").should("be.visible"); - }); -}); - -describe("Household settings (/parametres/foyer) — as the admin", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: { ...adminProfile, houseId: 1 } }); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); - }); - - it("shows the household's name, invite code, and members with an admin badge", () => { - cy.visit("/parametres/foyer"); - - cy.get("#houseName").should("have.value", "Chez Alice"); - cy.contains("ABCD2345").should("be.visible"); - cy.contains("Bob Dupont").should("be.visible"); - cy.contains("Alice Martin").parent().contains("Admin"); - }); - - it("autosaves the household name", () => { - cy.intercept("PATCH", "**/house/current", { - statusCode: 200, - body: { ...houseWithTwoMembers, name: "Chez les Martin" }, - }).as("renameHouse"); - - cy.visit("/parametres/foyer"); - cy.get("#houseName").clear(); - cy.get("#houseName").type("Chez les Martin"); - - cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez les Martin" }); - cy.contains("Enregistré ✓").should("be.visible"); - }); - - it("removes a member", () => { - // Same reasoning as the "deletes the household" test below — the page - // reloads `GET /house/current` right after the removal succeeds. - let memberRemoved = false; - cy.intercept("GET", "**/house/current", (req) => { - const body = memberRemoved - ? { ...houseWithTwoMembers, members: [houseWithTwoMembers.members[0]] } - : houseWithTwoMembers; - req.reply({ statusCode: 200, body }); - }); - cy.intercept("DELETE", "**/house/members/2", (req) => { - memberRemoved = true; - req.reply({ - statusCode: 200, - body: { ...houseWithTwoMembers, members: [houseWithTwoMembers.members[0]] }, - }); - }).as("removeMember"); - - cy.visit("/parametres/foyer"); - cy.contains("li", "Bob Dupont").contains("button", "Retirer").click(); - - cy.wait("@removeMember"); - cy.contains("Bob Dupont").should("not.exist"); - }); - - it("deletes the household after confirming", () => { - // The page reloads `GET /house/current` right after the delete - // succeeds — this intercept needs to answer differently before/after - // that DELETE, hence the shared mutable flag rather than two static - // `cy.intercept` calls (the later one would just win for every request, - // including the initial page load). - let houseDeleted = false; - cy.intercept("GET", "**/house/current", (req) => { - req.reply({ statusCode: 200, body: houseDeleted ? null : houseWithTwoMembers }); - }); - cy.intercept("DELETE", "**/house/current", (req) => { - houseDeleted = true; - req.reply({ statusCode: 204 }); - }).as("deleteHouse"); - - cy.visit("/parametres/foyer"); - cy.contains("button", "Supprimer le foyer").click(); - cy.contains("button", "Confirmer la suppression").click(); - - cy.wait("@deleteHouse"); - cy.contains("Créer un foyer").should("be.visible"); - }); -}); - -describe("Household settings (/parametres/foyer) — as a non-admin member", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { - statusCode: 200, - body: { ...adminProfile, id: 2, firstName: "Bob", lastName: "Dupont", houseId: 1 }, - }); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); - }); - - it("offers to leave the household instead of deleting it", () => { - cy.visit("/parametres/foyer"); - - cy.contains("button", "Quitter le foyer").should("be.visible"); - cy.contains("button", "Supprimer le foyer").should("not.exist"); - }); - - it("leaves the household", () => { - cy.intercept("POST", "**/house/leave", { statusCode: 204 }).as("leaveHouse"); - - cy.visit("/parametres/foyer"); - cy.contains("button", "Quitter le foyer").click(); - - cy.wait("@leaveHouse"); - }); -}); diff --git a/apps/web/cypress/e2e/household-settings.feature b/apps/web/cypress/e2e/household-settings.feature new file mode 100644 index 0000000..07985cc --- /dev/null +++ b/apps/web/cypress/e2e/household-settings.feature @@ -0,0 +1,90 @@ +Feature: Household settings + As a signed-in user + I want to create, join, manage, or leave a household + So that I can share a batch-cooking plan with the people I cook with + + Scenario: Offers to create or join a household when I have none yet + Given I am signed in as "Alice" "Martin" + And the household request returns no household + When I visit "/parametres/foyer" + Then I should see "Créer un foyer" + And I should see "Rejoindre un foyer" + + Scenario: Creates a household + Given I am signed in as "Alice" "Martin" + And creating a household will succeed + When I visit "/parametres/foyer" + And I fill in the "houseName" field with "Chez Alice" + And I click the button "Créer" + Then the household creation request should have been made with name "Chez Alice" + And I should see "ABCD2345" + + Scenario: Joins a household by invite code + Given I am signed in as "Alice" "Martin" + And joining a household will succeed + When I visit "/parametres/foyer" + And I fill in the "inviteCode" field with "abcd2345" + And I click the button "Rejoindre" + Then the household join request should have been made with invite code "ABCD2345" + And I should see "Bob Dupont" + + Scenario: Shows the household's name, invite code, and members with an admin badge + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the household request returns the two-member household + When I visit "/parametres/foyer" + Then the "houseName" field should have the value "Chez Alice" + And I should see "ABCD2345" + And I should see "Bob Dupont" + And "Alice Martin" should be marked as Admin + + Scenario: Autosaves the household name + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the household request returns the two-member household + And renaming the household will succeed + When I visit "/parametres/foyer" + And I clear the "houseName" field + And I fill in the "houseName" field with "Chez les Martin" + Then the household rename request should have been made with name "Chez les Martin" + And I should see "Enregistré ✓" + + Scenario: Removes a member + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the household request returns the two-member household + And removing Bob from the household will succeed + When I visit "/parametres/foyer" + And I click "Retirer" for the member "Bob Dupont" + Then the member removal request should have been made + And I should not see "Bob Dupont" + + Scenario: Deletes the household after confirming + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the household request returns the two-member household + And deleting the household will succeed + When I visit "/parametres/foyer" + And I click the button "Supprimer le foyer" + And I click the button "Confirmer la suppression" + Then the household deletion request should have been made + And I should see "Créer un foyer" + + Scenario: Offers to leave the household instead of deleting it, as a non-admin member + Given I am signed in as "Bob" "Dupont" + And my user id is 2 + And my household id is 1 + And the household request returns the two-member household + When I visit "/parametres/foyer" + Then I should see "Quitter le foyer" + And I should not see "Supprimer le foyer" + + Scenario: Leaves the household + Given I am signed in as "Bob" "Dupont" + And my user id is 2 + And my household id is 1 + And the household request returns the two-member household + And leaving the household will succeed + When I visit "/parametres/foyer" + And I click the button "Quitter le foyer" + Then the household leave request should have been made diff --git a/apps/web/cypress/e2e/household-settings.steps.ts b/apps/web/cypress/e2e/household-settings.steps.ts new file mode 100644 index 0000000..48e04c9 --- /dev/null +++ b/apps/web/cypress/e2e/household-settings.steps.ts @@ -0,0 +1,120 @@ +import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; + +const houseWithTwoMembers = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [ + { id: 1, firstName: "Alice", lastName: "Martin" }, + { id: 2, firstName: "Bob", lastName: "Dupont" }, + ], +}; + +Given("the household request returns the two-member household", () => { + cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); +}); + +// The page reloads `GET /house/current` right after each mutation below +// succeeds — these intercepts need to answer differently before/after that +// follow-up GET, hence a shared mutable flag rather than a single static +// `cy.intercept` (a later static one would just win for every request, +// including the initial page load). + +Given("creating a household will succeed", () => { + const createdHouse = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [{ id: 1, firstName: "Alice", lastName: "Martin" }], + }; + let created = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: created ? createdHouse : null }); + }); + cy.intercept("POST", "**/house", (req) => { + created = true; + req.reply({ statusCode: 201, body: createdHouse }); + }).as("createHouse"); +}); + +Given("joining a household will succeed", () => { + let joined = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: joined ? houseWithTwoMembers : null }); + }); + cy.intercept("POST", "**/house/join", (req) => { + joined = true; + req.reply({ statusCode: 200, body: houseWithTwoMembers }); + }).as("joinHouse"); +}); + +Given("renaming the household will succeed", () => { + cy.intercept("PATCH", "**/house/current", { + statusCode: 200, + body: { ...houseWithTwoMembers, name: "Chez les Martin" }, + }).as("renameHouse"); +}); + +Given("removing Bob from the household will succeed", () => { + const householdWithoutBob = { ...houseWithTwoMembers, members: [houseWithTwoMembers.members[0]] }; + let memberRemoved = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: memberRemoved ? householdWithoutBob : houseWithTwoMembers }); + }); + cy.intercept("DELETE", "**/house/members/2", (req) => { + memberRemoved = true; + req.reply({ statusCode: 200, body: householdWithoutBob }); + }).as("removeMember"); +}); + +Given("deleting the household will succeed", () => { + let houseDeleted = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: houseDeleted ? null : houseWithTwoMembers }); + }); + cy.intercept("DELETE", "**/house/current", (req) => { + houseDeleted = true; + req.reply({ statusCode: 204 }); + }).as("deleteHouse"); +}); + +Given("leaving the household will succeed", () => { + cy.intercept("POST", "**/house/leave", { statusCode: 204 }).as("leaveHouse"); +}); + +When("I click {string} for the member {string}", (action: string, member: string) => { + cy.contains("li", member).contains("button", action).click(); +}); + +Then("{string} should be marked as Admin", (member: string) => { + cy.contains(member).parent().contains("Admin"); +}); + +Then("the household creation request should have been made with name {string}", (name: string) => { + cy.wait("@createHouse").its("request.body").should("deep.equal", { name }); +}); + +Then( + "the household join request should have been made with invite code {string}", + (code: string) => { + cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: code }); + }, +); + +Then("the household rename request should have been made with name {string}", (name: string) => { + cy.wait("@renameHouse").its("request.body").should("deep.equal", { name }); +}); + +Then("the member removal request should have been made", () => { + cy.wait("@removeMember"); +}); + +Then("the household deletion request should have been made", () => { + cy.wait("@deleteHouse"); +}); + +Then("the household leave request should have been made", () => { + cy.wait("@leaveHouse"); +}); diff --git a/apps/web/cypress/e2e/onboarding.cy.ts b/apps/web/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index 93c2af8..0000000 --- a/apps/web/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no -// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover -// real API behavior against a real database). - -const signupResponse = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null as number | null, - dietId: null, -}; - -/** Signs up and lands on the wizard's first step (regime) — shared setup for every scenario below. */ -function signupAndReachOnboarding() { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup"); - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - - cy.visit("/signup"); - cy.get("#firstName").type("Alice"); - cy.get("#lastName").type("Martin"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Créer mon profil").click(); - cy.wait("@signup"); -} - -describe("Onboarding wizard (regime → foyer → allergens)", () => { - it("walks through all three steps, creating a household on the way, and lands on the home", () => { - cy.intercept("GET", "**/reference/diets", { - statusCode: 200, - body: [ - { id: 1, key: "omnivore" }, - { id: 2, key: "vegetarian" }, - ], - }); - cy.intercept("PATCH", "**/profile/diet", { - statusCode: 200, - body: { ...signupResponse, dietId: 2 }, - }).as("updateDiet"); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); - cy.intercept("POST", "**/house", { - statusCode: 201, - body: { id: 1, name: "Chez Alice", adminId: 1, inviteCode: "ABCD2345", members: [] }, - }).as("createHouse"); - cy.intercept("GET", "**/reference/allergies", { - statusCode: 200, - body: [ - { id: 1, key: "peanuts", kind: "ALLERGY" }, - { id: 2, key: "gluten", kind: "INTOLERANCE" }, - ], - }); - cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as( - "updateAllergies", - ); - - signupAndReachOnboarding(); - - // Step 1/3 — dietary regime. - cy.url().should("include", "/onboarding/regime"); - cy.contains("Étape 1 sur 3").should("be.visible"); - cy.get("#diet").select("Végétarien"); - cy.contains("button", "Continuer").click(); - cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 2 }); - - // Step 2/3 — household, optional: creating one here. - cy.url().should("include", "/onboarding/foyer"); - cy.contains("Étape 2 sur 3").should("be.visible"); - cy.get("#houseName").type("Chez Alice"); - cy.contains("button", "Créer").click(); - cy.wait("@createHouse").its("request.body").should("deep.equal", { name: "Chez Alice" }); - - // Step 3/3 — allergens (grouped into two lists) and intolerances, then finish. - cy.url().should("include", "/onboarding/allergenes"); - cy.contains("Étape 3 sur 3").should("be.visible"); - cy.contains("legend", "Allergies").should("be.visible"); - cy.contains("legend", "Intolérances").should("be.visible"); - cy.contains("label", "Arachides").find("input[type=checkbox]").check(); - cy.contains("button", "Terminer").click(); - cy.wait("@updateAllergies") - .its("request.body") - .should("deep.equal", { allergyIds: [1] }); - - cy.url().should("eq", `${Cypress.config().baseUrl}/`); - cy.contains("h1", "Planning de la semaine").should("be.visible"); - }); - - it("lets the regime and allergens steps be skipped without changing anything", () => { - cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); - cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: signupResponse }).as( - "updateDiet", - ); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); - cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); - cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [] }).as( - "updateAllergies", - ); - - signupAndReachOnboarding(); - - cy.url().should("include", "/onboarding/regime"); - cy.contains("button", "Continuer").click(); - cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: null }); - - cy.url().should("include", "/onboarding/foyer"); - cy.contains("button", "Passer cette étape").click(); - - cy.url().should("include", "/onboarding/allergenes"); - cy.contains("button", "Terminer").click(); - cy.wait("@updateAllergies").its("request.body").should("deep.equal", { allergyIds: [] }); - - cy.url().should("eq", `${Cypress.config().baseUrl}/`); - }); - - it("lets the household step be completed by joining an existing household instead of creating one", () => { - cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); - cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: signupResponse }); - cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); - cy.intercept("POST", "**/house/join", { - statusCode: 200, - body: { - id: 1, - name: "Chez Bob", - adminId: 2, - inviteCode: "ABCD2345", - members: [ - { id: 1, firstName: "Alice", lastName: "Martin" }, - { id: 2, firstName: "Bob", lastName: "Dupont" }, - ], - }, - }).as("joinHouse"); - cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); - cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [] }); - - signupAndReachOnboarding(); - - cy.contains("button", "Continuer").click(); - cy.url().should("include", "/onboarding/foyer"); - - cy.get("#inviteCode").type("abcd2345"); - cy.contains("button", "Rejoindre").click(); - - cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: "ABCD2345" }); - cy.url().should("include", "/onboarding/allergenes"); - }); -}); diff --git a/apps/web/cypress/e2e/onboarding.feature b/apps/web/cypress/e2e/onboarding.feature new file mode 100644 index 0000000..c429a82 --- /dev/null +++ b/apps/web/cypress/e2e/onboarding.feature @@ -0,0 +1,67 @@ +Feature: Onboarding wizard + As a newly signed-up user + I want to set my diet, household, and allergens + So that my profile is ready before I start planning meals + + Background: + Given the planning request returns nothing + + Scenario: Walks through all three steps, creating a household on the way, and lands on the home page + Given the diets reference list has options + And selecting the diet will succeed + And the household request returns no household + And creating a household will succeed + And the allergies reference list has options + And updating allergies will succeed + And I have signed up + Then the URL should include "/onboarding/regime" + And I should see "Étape 1 sur 3" + When I select "Végétarien" from the "diet" field + And I click the button "Continuer" + Then the diet update request should have been made with diet id 2 + And the URL should include "/onboarding/foyer" + And I should see "Étape 2 sur 3" + When I fill in the "houseName" field with "Chez Alice" + And I click the button "Créer" + Then the household creation request should have been made with name "Chez Alice" + And the URL should include "/onboarding/allergenes" + And I should see "Étape 3 sur 3" + And I should see the section "Allergies" + And I should see the section "Intolérances" + When I check the checkbox "Arachides" + And I click the button "Terminer" + Then the allergies update request should have been made with allergy id 1 + And the URL should be the home page + And I should see the heading "Planning de la semaine" + + Scenario: Lets the regime and allergens steps be skipped without changing anything + Given the diets reference list is empty + And selecting the diet will succeed + And the household request returns no household + And the allergies reference list is empty + And updating allergies will succeed + And I have signed up + Then the URL should include "/onboarding/regime" + When I click the button "Continuer" + Then the diet update request should have been made with no diet id + And the URL should include "/onboarding/foyer" + When I click the button "Passer cette étape" + Then the URL should include "/onboarding/allergenes" + When I click the button "Terminer" + Then the allergies update request should have been made with no allergy ids + And the URL should be the home page + + Scenario: Lets the household step be completed by joining an existing household instead of creating one + Given the diets reference list is empty + And selecting the diet will succeed + And the household request returns no household + And joining a household will succeed + And the allergies reference list is empty + And updating allergies will succeed + And I have signed up + When I click the button "Continuer" + Then the URL should include "/onboarding/foyer" + When I fill in the "inviteCode" field with "abcd2345" + And I click the button "Rejoindre" + Then the household join request should have been made with invite code "ABCD2345" + And the URL should include "/onboarding/allergenes" diff --git a/apps/web/cypress/e2e/onboarding.steps.ts b/apps/web/cypress/e2e/onboarding.steps.ts new file mode 100644 index 0000000..ff922ba --- /dev/null +++ b/apps/web/cypress/e2e/onboarding.steps.ts @@ -0,0 +1,65 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; + +const signupResponse = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null as number | null, + dietId: null, +}; + +Given("the diets reference list has options", () => { + cy.intercept("GET", "**/reference/diets", { + statusCode: 200, + body: [ + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, + ], + }); +}); + +Given("the allergies reference list has options", () => { + cy.intercept("GET", "**/reference/allergies", { + statusCode: 200, + body: [ + { id: 1, key: "peanuts", kind: "ALLERGY" }, + { id: 2, key: "gluten", kind: "INTOLERANCE" }, + ], + }); +}); + +Given("the allergies reference list is empty", () => { + cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); +}); + +// Signs up and lands on the wizard's first step (regime) — shared setup for +// every scenario in this feature, mirroring `signupAndReachOnboarding` from +// the pre-conversion spec. +Given("I have signed up", () => { + cy.intercept("GET", "**/auth/me", { statusCode: 401 }); + cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup"); + + cy.visit("/signup"); + cy.get("#firstName").type("Alice"); + cy.get("#lastName").type("Martin"); + cy.get("#email").type("alice@example.com"); + cy.get("#password").type("correct-horse-battery-staple"); + cy.contains("button", "Créer mon profil").click(); + cy.wait("@signup"); +}); + +Then("the diet update request should have been made with no diet id", () => { + cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: null }); +}); + +Then("the allergies update request should have been made with allergy id {int}", (id: number) => { + cy.wait("@updateAllergies") + .its("request.body") + .should("deep.equal", { allergyIds: [id] }); +}); + +Then("the allergies update request should have been made with no allergy ids", () => { + cy.wait("@updateAllergies").its("request.body").should("deep.equal", { allergyIds: [] }); +}); diff --git a/apps/web/cypress/e2e/planning-page.cy.ts b/apps/web/cypress/e2e/planning-page.cy.ts deleted file mode 100644 index ab13c85..0000000 --- a/apps/web/cypress/e2e/planning-page.cy.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no -// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover -// real API behavior against a real database). - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, -}; - -// 2026-08-17 is a Monday — frozen via `cy.clock` so "today"/"this week" -// assertions are deterministic instead of depending on the day the suite -// happens to run. -const TODAY = new Date("2026-08-17T09:00:00Z"); - -function freezeToday() { - cy.clock(TODAY, ["Date"]); -} - -describe("Sidebar navigation", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - cy.visit("/"); - }); - - // The Foyer/Compte/Préférences links — behind the sidebar's "Paramètres" - // toggle, not the main nav tested here — are covered by sidebar.cy.ts. - it("highlights the current section and navigates between stub pages", () => { - cy.contains("nav a", "Planning").should("have.class", "active"); - - cy.contains("nav a", "Recettes").click(); - cy.url().should("include", "/recettes"); - cy.contains("h1", "Recettes").should("be.visible"); - cy.contains("nav a", "Recettes").should("have.class", "active"); - cy.contains("nav a", "Planning").should("not.have.class", "active"); - - cy.contains("nav a", "Liste de courses").click(); - cy.url().should("include", "/liste-de-courses"); - cy.contains("h1", "Liste de courses").should("be.visible"); - - cy.contains("nav a", "Planning").click(); - cy.url().should("eq", `${Cypress.config().baseUrl}/`); - cy.contains("h1", "Planning de la semaine").should("be.visible"); - }); - - it("shows the signed-in user's name and lets them log out from the account menu", () => { - cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); - - cy.contains("button", "Bonjour Alice").should("be.visible").click(); - cy.contains("button", "Se déconnecter").click(); - - cy.wait("@logout"); - cy.url().should("include", "/login"); - }); -}); - -describe("Planning grid", () => { - beforeEach(() => { - // Desktop-only design (see the plan/PR description) — wider than - // Cypress's default 1000×660 so all 7 day columns fit without the grid's - // horizontal scroll hiding the later ones from visibility assertions. - cy.viewport(1600, 900); - freezeToday(); - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - }); - - it("shows an empty grid (every slot just offering '+') when the household has no planning yet", () => { - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); - - cy.visit("/"); - cy.wait("@getPlanning").its("request.url").should("include", "date=2026-08-17"); - - cy.contains("h1", "Planning de la semaine").should("be.visible"); - // 5 meal rows × 7 days = 35 empty slots, each just a "+". - cy.get(".add-recipe-btn").should("have.length", 35); - cy.get(".recipe-chip").should("not.exist"); - }); - - it("renders each recipe in its (day, meal) cell, and highlights today's column", () => { - cy.intercept("GET", /\/planning\?/, { - statusCode: 200, - body: { - id: 1, - startDate: "2026-08-17T00:00:00.000Z", - finishDate: "2026-08-23T00:00:00.000Z", - items: [ - { id: 1, weekDay: "mardi", meal: "diner", recipe: { id: 1, name: "Ratatouille" } }, - { - id: 2, - weekDay: "mercredi", - meal: "dejeuner", - recipe: { id: 2, name: "Curry de lentilles" }, - }, - ], - }, - }); - - cy.visit("/"); - - cy.contains("th", "Lundi").should("be.visible"); - cy.contains("th", "Dimanche").should("be.visible"); - cy.contains(".recipe-chip", "Ratatouille").should("be.visible"); - cy.contains(".recipe-chip", "Curry de lentilles").should("be.visible"); - - // Today (17 août, Lundi) is marked — its column header carries `.today`. - cy.contains("th.today .day-date", "17").should("be.visible"); - }); - - it("shows a loading state, then an error state when the request fails", () => { - cy.intercept("GET", /\/planning\?/, { - statusCode: 500, - body: { code: 5000, message: "boom" }, - }); - - cy.visit("/"); - - cy.contains("Impossible de charger le planning, réessayez plus tard").should("be.visible"); - }); - - // Assertions below check the rendered week label/badge, not the intercepted - // request count — React StrictMode (see main.tsx) double-invokes effects in - // dev, so the `GET /planning` mount effect can fire twice per navigation; - // counting exact `cy.wait` calls against that would be flaky, but the - // rendered result is the same either way. - it("navigates to the next/previous week, re-fetching each time", () => { - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); - - cy.visit("/"); - cy.wait("@getPlanning").its("request.url").should("include", "date=2026-08-17"); - cy.contains("Semaine du 17 au 23 août 2026").should("be.visible"); - cy.contains("Cette semaine").should("be.visible"); - - cy.get(".week-nav__arrow").last().click(); - cy.contains("Semaine du 24 au 30 août 2026").should("be.visible"); - cy.contains("Cette semaine").should("not.exist"); - - cy.get(".week-nav__arrow").first().click(); - cy.contains("Semaine du 17 au 23 août 2026").should("be.visible"); - cy.get(".week-nav__arrow").first().click(); - cy.contains("Semaine du 10 au 16 août 2026").should("be.visible"); - }); - - it("jumps to an arbitrary week by picking a day in the calendar popover", () => { - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); - - cy.visit("/"); - cy.wait("@getPlanning"); - - cy.contains("button", "Semaine du").click(); - cy.get(".calendar-popover").should("be.visible"); - // Picking the 25th (still August, unambiguous in the visible grid) - // should jump to the week of the 24th–30th. - cy.get(".calendar-grid__day").contains(/^25$/).click(); - - cy.contains("Semaine du 24 au 30 août 2026").should("be.visible"); - cy.get(".calendar-popover").should("not.exist"); - }); -}); diff --git a/apps/web/cypress/e2e/planning-page.feature b/apps/web/cypress/e2e/planning-page.feature new file mode 100644 index 0000000..0469a86 --- /dev/null +++ b/apps/web/cypress/e2e/planning-page.feature @@ -0,0 +1,102 @@ +Feature: Planning page — sidebar navigation and weekly grid + As a signed-in user + I want to navigate between sections and see my household's weekly planning + So that I know what meals are planned this week + + Background: + Given I am signed in as "Alice" "Martin" + And my household id is 1 + + # The Foyer/Compte/Préférences links — behind the sidebar's "Paramètres" + # toggle, not the main nav tested here — are covered by sidebar.feature. + Scenario: Highlights the current section and navigates between stub pages + Given the planning request returns nothing + When I visit "/" + Then the nav link "Planning" should be active + When I click the nav link "Recettes" + Then the URL should include "/recettes" + And I should see the heading "Recettes" + And the nav link "Recettes" should be active + And the nav link "Planning" should not be active + When I click the nav link "Liste de courses" + Then the URL should include "/liste-de-courses" + And I should see the heading "Liste de courses" + When I click the nav link "Planning" + Then the URL should be the home page + And I should see the heading "Planning de la semaine" + + Scenario: Shows the signed-in user's name and lets them log out from the account menu + Given the planning request returns nothing + And the logout request will succeed + When I visit "/" + And I open the account menu + And I click the button "Se déconnecter" + Then the logout request should have been made + And the URL should include "/login" + + # Desktop-only design (see the plan/PR description) — wider than Cypress's + # default 1000×660 so all 7 day columns fit without the grid's horizontal + # scroll hiding the later ones from visibility assertions. + Scenario: Shows an empty grid when the household has no planning yet + Given the viewport is 1600 by 900 + And today is frozen at "2026-08-17T09:00:00Z" + And the current planning is empty + When I visit "/" + Then the planning request should have been made for the week of "2026-08-17" + And I should see the heading "Planning de la semaine" + And the grid should have 35 empty slots + And no recipe chips should be shown + + Scenario: Renders each recipe in its (day, meal) cell, and highlights today's column + Given the viewport is 1600 by 900 + And today is frozen at "2026-08-17T09:00:00Z" + And the current planning includes: + | day | meal | recipe | + | mardi | diner | Ratatouille | + | mercredi | dejeuner | Curry de lentilles | + When I visit "/" + Then the day column "Lundi" should be visible + And the day column "Dimanche" should be visible + And the recipe chip "Ratatouille" should be visible + And the recipe chip "Curry de lentilles" should be visible + And today's column should show the date "17" + + Scenario: Shows a loading state, then an error state when the request fails + Given the viewport is 1600 by 900 + And today is frozen at "2026-08-17T09:00:00Z" + And the current planning request fails + When I visit "/" + Then I should see "Impossible de charger le planning, réessayez plus tard" + + # Assertions below check the rendered week label/badge, not the intercepted + # request count — React StrictMode (see main.tsx) double-invokes effects in + # dev, so the `GET /planning` mount effect can fire twice per navigation; + # counting exact `cy.wait` calls against that would be flaky, but the + # rendered result is the same either way. + Scenario: Navigates to the next/previous week, re-fetching each time + Given the viewport is 1600 by 900 + And today is frozen at "2026-08-17T09:00:00Z" + And the current planning is empty + When I visit "/" + Then the planning request should have been made for the week of "2026-08-17" + And I should see "Semaine du 17 au 23 août 2026" + And I should see "Cette semaine" + When I click the next week arrow + Then I should see "Semaine du 24 au 30 août 2026" + And I should not see "Cette semaine" + When I click the previous week arrow + Then I should see "Semaine du 17 au 23 août 2026" + When I click the previous week arrow + Then I should see "Semaine du 10 au 16 août 2026" + + Scenario: Jumps to an arbitrary week by picking a day in the calendar popover + Given the viewport is 1600 by 900 + And today is frozen at "2026-08-17T09:00:00Z" + And the current planning is empty + When I visit "/" + Then the planning request should have been made for the week of "2026-08-17" + When I open the week calendar + Then the calendar popover should be visible + When I pick day 25 in the calendar + Then I should see "Semaine du 24 au 30 août 2026" + And the calendar popover should be closed diff --git a/apps/web/cypress/e2e/planning-page.steps.ts b/apps/web/cypress/e2e/planning-page.steps.ts new file mode 100644 index 0000000..8af53ec --- /dev/null +++ b/apps/web/cypress/e2e/planning-page.steps.ts @@ -0,0 +1,92 @@ +import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; + +Given("the current planning is empty", () => { + cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); +}); + +Given("the current planning includes:", (dataTable: DataTable) => { + const items = dataTable.hashes().map((row, index) => ({ + id: index + 1, + weekDay: row.day, + meal: row.meal, + recipe: { id: index + 1, name: row.recipe }, + })); + cy.intercept("GET", /\/planning\?/, { + statusCode: 200, + body: { + id: 1, + startDate: "2026-08-17T00:00:00.000Z", + finishDate: "2026-08-23T00:00:00.000Z", + items, + }, + }); +}); + +Given("the current planning request fails", () => { + cy.intercept("GET", /\/planning\?/, { + statusCode: 500, + body: { code: 5000, message: "boom" }, + }); +}); + +Then("the planning request should have been made for the week of {string}", (date: string) => { + cy.wait("@getPlanning").its("request.url").should("include", `date=${date}`); +}); + +Then("the grid should have {int} empty slots", (count: number) => { + cy.get(".add-recipe-btn").should("have.length", count); +}); + +Then("no recipe chips should be shown", () => { + cy.get(".recipe-chip").should("not.exist"); +}); + +Then("the day column {string} should be visible", (day: string) => { + cy.contains("th", day).should("be.visible"); +}); + +Then("the recipe chip {string} should be visible", (name: string) => { + cy.contains(".recipe-chip", name).should("be.visible"); +}); + +Then("today's column should show the date {string}", (day: string) => { + cy.contains("th.today .day-date", day).should("be.visible"); +}); + +When("I click the next week arrow", () => { + cy.get(".week-nav__arrow").last().click(); +}); + +When("I click the previous week arrow", () => { + cy.get(".week-nav__arrow").first().click(); +}); + +When("I open the week calendar", () => { + cy.contains("button", "Semaine du").click(); +}); + +Then("the calendar popover should be visible", () => { + cy.get(".calendar-popover").should("be.visible"); +}); + +Then("the calendar popover should be closed", () => { + cy.get(".calendar-popover").should("not.exist"); +}); + +When("I pick day {int} in the calendar", (day: number) => { + cy.get(".calendar-grid__day") + .contains(new RegExp(`^${day}$`)) + .click(); +}); + +Then("the nav link {string} should be active", (text: string) => { + cy.contains("nav a", text).should("have.class", "active"); +}); + +Then("the nav link {string} should not be active", (text: string) => { + cy.contains("nav a", text).should("not.have.class", "active"); +}); + +When("I click the nav link {string}", (text: string) => { + cy.contains("nav a", text).click(); +}); diff --git a/apps/web/cypress/e2e/preferences.cy.ts b/apps/web/cypress/e2e/preferences.cy.ts deleted file mode 100644 index 65f3111..0000000 --- a/apps/web/cypress/e2e/preferences.cy.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: 2, -}; - -describe("Dietary preferences (/parametres/preferences) — hot saving", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/reference/diets", { - statusCode: 200, - body: [ - { id: 1, key: "omnivore" }, - { id: 2, key: "vegetarian" }, - ], - }); - cy.intercept("GET", "**/reference/allergies", { - statusCode: 200, - body: [ - { id: 1, key: "peanuts", kind: "ALLERGY" }, - { id: 2, key: "gluten", kind: "INTOLERANCE" }, - ], - }); - cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] }); - // The page also loads the reference ingredient list + the profile's - // disliked-ingredients selection for `DislikedIngredientsField` — added - // alongside `getDiets`/`getAllergies` in the same `Promise.all` (see - // PreferencesPage.tsx), so both need mocking here too or that `Promise.all` - // rejects and the whole page renders its error state instead of the form, - // taking `#diet`/the allergy checkboxes down with it. - cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [] }); - cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); - }); - - it("loads the current regime, and shows allergies/intolerances as two groups", () => { - cy.visit("/parametres/preferences"); - - cy.get("#diet").should("have.value", "2"); - cy.contains("legend", "Allergies").should("be.visible"); - cy.contains("legend", "Intolérances").should("be.visible"); - cy.contains("label", "Gluten").find("input[type=checkbox]").should("be.checked"); - cy.contains("label", "Arachides").find("input[type=checkbox]").should("not.be.checked"); - }); - - it("has no explicit save button anywhere on the page", () => { - cy.visit("/parametres/preferences"); - cy.contains("button", "Enregistrer").should("not.exist"); - }); - - it("autosaves the regime as soon as it's selected", () => { - cy.intercept("PATCH", "**/profile/diet", { - statusCode: 200, - body: { ...authenticatedProfile, dietId: 1 }, - }).as("updateDiet"); - - cy.visit("/parametres/preferences"); - cy.get("#diet").select("Omnivore"); - - cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 1 }); - }); - - it("autosaves allergies and intolerances together after checking boxes", () => { - cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [2, 1] }).as( - "updateAllergies", - ); - - cy.visit("/parametres/preferences"); - cy.contains("label", "Arachides").find("input[type=checkbox]").check(); - - cy.wait("@updateAllergies") - .its("request.body") - .should("deep.equal", { allergyIds: [2, 1] }); - cy.contains("Enregistré ✓").should("be.visible"); - }); -}); diff --git a/apps/web/cypress/e2e/preferences.feature b/apps/web/cypress/e2e/preferences.feature new file mode 100644 index 0000000..14badc8 --- /dev/null +++ b/apps/web/cypress/e2e/preferences.feature @@ -0,0 +1,35 @@ +Feature: Dietary preferences + As a signed-in user + I want my regime and allergies/intolerances to autosave as I edit them + So that my preferences are always up to date without an explicit save step + + Background: + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And my diet id is 2 + And the dietary preferences reference data is ready + + Scenario: Loads the current regime, and shows allergies/intolerances as two groups + When I visit "/parametres/preferences" + Then the "diet" field should have the value "2" + And I should see the section "Allergies" + And I should see the section "Intolérances" + And the checkbox "Gluten" should be checked + And the checkbox "Arachides" should not be checked + + Scenario: Has no explicit save button anywhere on the page + When I visit "/parametres/preferences" + Then I should not see "Enregistrer" + + Scenario: Autosaves the regime as soon as it's selected + Given selecting the diet will succeed + When I visit "/parametres/preferences" + And I select "Omnivore" from the "diet" field + Then the diet update request should have been made with diet id 1 + + Scenario: Autosaves allergies and intolerances together after checking boxes + Given updating allergies will succeed + When I visit "/parametres/preferences" + And I check the checkbox "Arachides" + Then the allergies update request should have been made with allergy ids 2 and 1 + And I should see "Enregistré ✓" diff --git a/apps/web/cypress/e2e/preferences.steps.ts b/apps/web/cypress/e2e/preferences.steps.ts new file mode 100644 index 0000000..30e5809 --- /dev/null +++ b/apps/web/cypress/e2e/preferences.steps.ts @@ -0,0 +1,52 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; + +// The page also loads the reference ingredient list + the profile's +// disliked-ingredients selection for `DislikedIngredientsField` — added +// alongside diets/allergies in the same `Promise.all` (see +// PreferencesPage.tsx), so both need mocking here too or that `Promise.all` +// rejects and the whole page renders its error state instead of the form, +// taking `#diet`/the allergy checkboxes down with it. +Given("the dietary preferences reference data is ready", () => { + cy.intercept("GET", "**/reference/diets", { + statusCode: 200, + body: [ + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, + ], + }); + cy.intercept("GET", "**/reference/allergies", { + statusCode: 200, + body: [ + { id: 1, key: "peanuts", kind: "ALLERGY" }, + { id: 2, key: "gluten", kind: "INTOLERANCE" }, + ], + }); + cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] }); + cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [] }); + cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); +}); + +Given("selecting the diet will succeed", () => { + cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: { dietId: 1 } }).as( + "updateDiet", + ); +}); + +Then("the diet update request should have been made with diet id {int}", (dietId: number) => { + cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId }); +}); + +Given("updating allergies will succeed", () => { + cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [2, 1] }).as( + "updateAllergies", + ); +}); + +Then( + "the allergies update request should have been made with allergy ids {int} and {int}", + (first: number, second: number) => { + cy.wait("@updateAllergies") + .its("request.body") + .should("deep.equal", { allergyIds: [first, second] }); + }, +); diff --git a/apps/web/cypress/e2e/recipe-form.cy.ts b/apps/web/cypress/e2e/recipe-form.cy.ts deleted file mode 100644 index 44ae9fb..0000000 --- a/apps/web/cypress/e2e/recipe-form.cy.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no -// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover -// real API behavior against a real database). - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, -}; - -const tomato = { - id: 1, - key: "tomato", - icon: "VEGETABLE", - category: "PRODUITS_FRAIS", - subcategory: "LEGUMES", - allergens: [], - diets: [{ id: 2, key: "vegetarian" }], -}; -const egg = { - id: 2, - key: "egg", - icon: "EGG", - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", - allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }], - diets: [], -}; -const carrot = { - id: 3, - key: "carrot", - icon: "VEGETABLE", - category: "PRODUITS_FRAIS", - subcategory: "LEGUMES", - allergens: [], - diets: [{ id: 2, key: "vegetarian" }], -}; - -const diets = [ - { id: 1, key: "omnivore" }, - { id: 2, key: "vegetarian" }, -]; - -function interceptCatalog() { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/reference/ingredients", { - statusCode: 200, - body: [tomato, egg, carrot], - }); - cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets }); -} - -describe("Recipe form — associating ingredients", () => { - beforeEach(() => { - interceptCatalog(); - }); - - it("adds an ingredient from the picker, fills its quantity/unit, and creates the recipe", () => { - cy.intercept("POST", "**/recipes", { statusCode: 201, body: { id: 42 } }).as("createRecipe"); - - cy.visit("/recettes/nouvelle"); - - cy.get("#recipe-name").type("Salade de tomates"); - - cy.get("input[placeholder='Rechercher un ingrédient…']").type("tomat"); - cy.contains(".ingredient-picker__card", "Tomate").click(); - - // The card disappears from the picker once selected (excludeIds), and - // a row for it appears in the recipe's own ingredient list. - cy.contains(".ingredient-picker__card", "Tomate").should("not.exist"); - cy.contains(".ingredient-row__name", "Tomate").should("be.visible"); - - cy.get(".ingredient-row .ingredient-row__quantity").type("3"); - cy.get(".ingredient-row .ingredient-row__unit").type("unité"); - - cy.contains("button", "Ajouter une étape").click(); - cy.get(".step-list-editor__item textarea").type("Couper les tomates."); - - cy.contains("button", "Enregistrer").should("not.be.disabled").click(); - - cy.wait("@createRecipe") - .its("request.body") - .should("deep.include", { - name: "Salade de tomates", - ingredients: [{ ingredientId: 1, quantity: 3, unit: "unité" }], - }); - cy.url().should("include", "/recettes/42"); - }); - - // Regression test for the exact bug reported: `crypto.randomUUID()` (used - // to mint each ingredient/step draft's client-only React key) throws - // outside a secure context — https, or literally the hostname - // `localhost` — so a LAN IP during on-device testing or a Capacitor - // WebView's `capacitor://` origin hit a black screen with "TypeError: - // crypto.randomUUID is not a function" the instant an ingredient was - // added. Cypress's own origin is secure, so this forces the same failure - // by deleting `crypto.randomUUID` before the app boots — see - // `apps/web/src/lib/client-key.ts`, which replaced it. - it("still works when crypto.randomUUID is unavailable (insecure-context regression)", () => { - cy.visit("/recettes/nouvelle", { - onBeforeLoad(win) { - Object.defineProperty(win.crypto, "randomUUID", { - value: undefined, - configurable: true, - }); - }, - }); - - cy.get("#recipe-name").type("Recette hors contexte sécurisé"); - - cy.contains(".ingredient-picker__card", "Tomate").click(); - cy.contains(".ingredient-picker__card", "Œuf").click(); - - // Both rows rendered with distinct identities — no crash, no React - // "same key" warning silently collapsing one of them. - cy.get(".ingredient-row").should("have.length", 2); - cy.contains(".ingredient-row__name", "Tomate").should("be.visible"); - cy.contains(".ingredient-row__name", "Œuf").should("be.visible"); - - cy.contains("button", "Ajouter une étape").click(); - cy.contains("button", "Ajouter une étape").click(); - cy.get(".step-list-editor__item").should("have.length", 2); - }); - - it("excludes an already-selected ingredient from the picker, and removing it brings it back", () => { - cy.visit("/recettes/nouvelle"); - - cy.contains(".ingredient-picker__card", "Carotte").click(); - cy.contains(".ingredient-picker__card", "Carotte").should("not.exist"); - - cy.contains(".ingredient-row", "Carotte") - .find("button[title='Retirer cet ingrédient']") - .click(); - cy.contains(".ingredient-picker__card", "Carotte").should("be.visible"); - cy.get(".ingredient-row").should("have.length", 0); - }); - - it("preloads an existing recipe's ingredients when editing, and lets you add another", () => { - const existingRecipe = { - id: 7, - name: "Omelette", - description: null, - picture: null, - authorId: 1, - visibility: "PERSONAL", - allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }], - diets: [], - isFavorite: false, - ingredients: [{ ingredient: egg, quantity: 3, unit: "unité" }], - steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }], - }; - cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe }); - cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe"); - - cy.visit("/recettes/7/modifier"); - - cy.contains(".ingredient-row__name", "Œuf").should("be.visible"); - cy.get(".ingredient-row .ingredient-row__quantity").should("have.value", "3"); - - cy.contains(".ingredient-picker__card", "Tomate").click(); - cy.get(".ingredient-row").should("have.length", 2); - cy.get(".ingredient-row .ingredient-row__quantity").last().type("1"); - cy.get(".ingredient-row .ingredient-row__unit").last().type("unité"); - - cy.contains("button", "Enregistrer").click(); - - cy.wait("@updateRecipe") - .its("request.body.ingredients") - .should("deep.equal", [ - { ingredientId: 2, quantity: 3, unit: "unité" }, - { ingredientId: 1, quantity: 1, unit: "unité" }, - ]); - }); -}); diff --git a/apps/web/cypress/e2e/recipe-form.feature b/apps/web/cypress/e2e/recipe-form.feature new file mode 100644 index 0000000..33cd1fc --- /dev/null +++ b/apps/web/cypress/e2e/recipe-form.feature @@ -0,0 +1,69 @@ +Feature: Recipe form — associating ingredients + As a signed-in user + I want to build a recipe by picking ingredients, quantities, and steps + So that I can save a complete recipe in one form + + Background: + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the ingredient/diet catalog is available + + Scenario: Adds an ingredient from the picker, fills its quantity/unit, and creates the recipe + Given creating the recipe will succeed and return id 42 + When I visit "/recettes/nouvelle" + And I fill in the "recipe-name" field with "Salade de tomates" + And I search the ingredient picker for "tomat" + And I select the ingredient "Tomate" from the picker + Then the ingredient "Tomate" should no longer be in the picker + And the recipe should include the ingredient "Tomate" + When I fill in the ingredient's quantity with "3" and unit "unité" + And I add a step + And I fill in the step description with "Couper les tomates." + Then the "Enregistrer" button should not be disabled + When I click the button "Enregistrer" + Then the recipe creation request should have included name "Salade de tomates" and ingredient 1 with quantity 3 and unit "unité" + And the URL should include "/recettes/42" + + # Regression test for the exact bug reported: `crypto.randomUUID()` (used + # to mint each ingredient/step draft's client-only React key) throws + # outside a secure context — https, or literally the hostname `localhost` + # — so a LAN IP during on-device testing or a Capacitor WebView's + # `capacitor://` origin hit a black screen with "TypeError: + # crypto.randomUUID is not a function" the instant an ingredient was + # added. Cypress's own origin is secure, so this forces the same failure + # by deleting `crypto.randomUUID` before the app boots — see + # `apps/web/src/lib/client-key.ts`, which replaced it. + Scenario: Still works when crypto.randomUUID is unavailable (insecure-context regression) + When I visit the new recipe form without a secure random UUID + And I fill in the "recipe-name" field with "Recette hors contexte sécurisé" + And I select the ingredient "Tomate" from the picker + And I select the ingredient "Œuf" from the picker + Then there should be 2 ingredient rows + And the recipe should include the ingredient "Tomate" + And the recipe should include the ingredient "Œuf" + When I add a step + And I add a step + Then there should be 2 step editor items + + Scenario: Excludes an already-selected ingredient from the picker, and removing it brings it back + When I visit "/recettes/nouvelle" + And I select the ingredient "Carotte" from the picker + Then the ingredient "Carotte" should no longer be in the picker + When I remove the ingredient "Carotte" from the recipe + Then the ingredient "Carotte" should be visible in the picker + And there should be 0 ingredient rows + + Scenario: Preloads an existing recipe's ingredients when editing, and lets you add another + Given recipe 7 exists with an egg omelette + And updating recipe 7 will succeed + When I visit "/recettes/7/modifier" + Then the recipe should include the ingredient "Œuf" + And the ingredient's quantity should be "3" + When I select the ingredient "Tomate" from the picker + Then there should be 2 ingredient rows + When I fill in the last ingredient's quantity with "1" and unit "unité" + And I click the button "Enregistrer" + Then the recipe update request should have included these ingredients: + | ingredientId | quantity | unit | + | 2 | 3 | unité | + | 1 | 1 | unité | diff --git a/apps/web/cypress/e2e/recipe-form.steps.ts b/apps/web/cypress/e2e/recipe-form.steps.ts new file mode 100644 index 0000000..4a8ec69 --- /dev/null +++ b/apps/web/cypress/e2e/recipe-form.steps.ts @@ -0,0 +1,159 @@ +import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; + +const tomato = { + id: 1, + key: "tomato", + icon: "VEGETABLE", + category: "PRODUITS_FRAIS", + subcategory: "LEGUMES", + allergens: [], + diets: [{ id: 2, key: "vegetarian" }], +}; +const egg = { + id: 2, + key: "egg", + icon: "EGG", + category: "CREMERIE_FROMAGE", + subcategory: "OEUFS", + allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }], + diets: [], +}; +const carrot = { + id: 3, + key: "carrot", + icon: "VEGETABLE", + category: "PRODUITS_FRAIS", + subcategory: "LEGUMES", + allergens: [], + diets: [{ id: 2, key: "vegetarian" }], +}; + +const diets = [ + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, +]; + +Given("the ingredient/diet catalog is available", () => { + cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [tomato, egg, carrot] }); + cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets }); +}); + +Given("creating the recipe will succeed and return id {int}", (id: number) => { + cy.intercept("POST", "**/recipes", { statusCode: 201, body: { id } }).as("createRecipe"); +}); + +When("I visit the new recipe form without a secure random UUID", () => { + cy.visit("/recettes/nouvelle", { + onBeforeLoad(win) { + Object.defineProperty(win.crypto, "randomUUID", { + value: undefined, + configurable: true, + }); + }, + }); +}); + +When("I search the ingredient picker for {string}", (text: string) => { + cy.get("input[placeholder='Rechercher un ingrédient…']").type(text); +}); + +When("I select the ingredient {string} from the picker", (name: string) => { + cy.contains(".ingredient-picker__card", name).click(); +}); + +Then("the ingredient {string} should no longer be in the picker", (name: string) => { + cy.contains(".ingredient-picker__card", name).should("not.exist"); +}); + +Then("the ingredient {string} should be visible in the picker", (name: string) => { + cy.contains(".ingredient-picker__card", name).should("be.visible"); +}); + +Then("the recipe should include the ingredient {string}", (name: string) => { + cy.contains(".ingredient-row__name", name).should("be.visible"); +}); + +Then("there should be {int} ingredient rows", (count: number) => { + cy.get(".ingredient-row").should("have.length", count); +}); + +When("I remove the ingredient {string} from the recipe", (name: string) => { + cy.contains(".ingredient-row", name).find("button[title='Retirer cet ingrédient']").click(); +}); + +When( + "I fill in the ingredient's quantity with {string} and unit {string}", + (quantity: string, unit: string) => { + cy.get(".ingredient-row .ingredient-row__quantity").type(quantity); + cy.get(".ingredient-row .ingredient-row__unit").type(unit); + }, +); + +Then("the ingredient's quantity should be {string}", (quantity: string) => { + cy.get(".ingredient-row .ingredient-row__quantity").should("have.value", quantity); +}); + +When( + "I fill in the last ingredient's quantity with {string} and unit {string}", + (quantity: string, unit: string) => { + cy.get(".ingredient-row .ingredient-row__quantity").last().type(quantity); + cy.get(".ingredient-row .ingredient-row__unit").last().type(unit); + }, +); + +When("I add a step", () => { + cy.contains("button", "Ajouter une étape").click(); +}); + +When("I fill in the step description with {string}", (text: string) => { + cy.get(".step-list-editor__item textarea").type(text); +}); + +Then("there should be {int} step editor items", (count: number) => { + cy.get(".step-list-editor__item").should("have.length", count); +}); + +Then( + "the recipe creation request should have included name {string} and ingredient {int} with quantity {int} and unit {string}", + (name: string, ingredientId: number, quantity: number, unit: string) => { + cy.wait("@createRecipe") + .its("request.body") + .should("deep.include", { + name, + ingredients: [{ ingredientId, quantity, unit }], + }); + }, +); + +Given("recipe 7 exists with an egg omelette", () => { + const existingRecipe = { + id: 7, + name: "Omelette", + description: null, + picture: null, + authorId: 1, + visibility: "PERSONAL", + allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }], + diets: [], + isFavorite: false, + ingredients: [{ ingredient: egg, quantity: 3, unit: "unité" }], + steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }], + }; + cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe }); +}); + +Given("updating recipe 7 will succeed", () => { + cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe"); +}); + +Then( + "the recipe update request should have included these ingredients:", + (dataTable: DataTable) => { + const expected = dataTable.hashes().map((row) => ({ + ingredientId: Number(row.ingredientId), + quantity: Number(row.quantity), + unit: row.unit, + })); + cy.wait("@updateRecipe").its("request.body.ingredients").should("deep.equal", expected); + }, +); diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts deleted file mode 100644 index 318a410..0000000 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ /dev/null @@ -1,241 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no -// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover -// real API behavior against a real database). - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, -}; - -const vegetarien = { id: 1, key: "vegetarian" }; -const gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" }; -const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" }; - -const ratatouille = { - id: 1, - name: "Ratatouille", - description: null, - picture: null, - authorId: 1, - visibility: "PERSONAL", - allergens: [], - diets: [vegetarien], - isFavorite: true, -}; - -const omelette = { - id: 2, - name: "Omelette", - description: null, - picture: null, - authorId: 1, - visibility: "PERSONAL", - allergens: [oeufs], - diets: [], - isFavorite: false, -}; - -const omeletteDetail = { - ...omelette, - description: "Une omelette toute simple.", - ingredients: [ - { - ingredient: { - id: 10, - key: "egg", - icon: "EGG", - category: "CREMERIE_FROMAGE", - subcategory: "OEUFS", - allergens: [oeufs], - diets: [], - }, - quantity: 3, - unit: "unité", - }, - ], - steps: [ - { id: 1, description: "Battre les œufs.", picture: null, order: 1 }, - { id: 2, description: "Cuire à la poêle.", picture: null, order: 2 }, - ], -}; - -function interceptAuth() { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); -} - -describe("Recipe catalog", () => { - beforeEach(() => { - interceptAuth(); - }); - - it("defaults to the Favoris tab and lists its recipes", () => { - cy.intercept("GET", /\/recipes\?/, (req) => { - expect(req.url).to.include("tab=favoris"); - req.reply({ statusCode: 200, body: [ratatouille] }); - }).as("listRecipes"); - - cy.visit("/recettes"); - cy.wait("@listRecipes"); - - cy.contains("h1", "Recettes").should("be.visible"); - cy.get(".recipe-tabs__tab.active").should("contain.text", "Favoris"); - cy.contains(".recipe-table__name", "Ratatouille").should("be.visible"); - // The favorited row carries the ★ fav-mark. - cy.contains(".recipe-table__name", "Ratatouille") - .find(".recipe-table__fav-mark") - .should("exist"); - cy.contains(".recipe-table__name", "Ratatouille") - .parents("tr") - .find(".diet-badge") - .should("contain.text", "Végétarien"); - }); - - it("shows the empty state when a tab has no recipes", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); - - cy.visit("/recettes"); - - cy.contains("Aucune recette pour le moment.").should("be.visible"); - }); - - it("shows an error state when the catalog fails to load", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 500, body: { code: 5000, message: "boom" } }); - - cy.visit("/recettes"); - - cy.contains(".recipes-page__status--error", "Impossible de charger").should("be.visible"); - }); - - it("switches tabs, re-fetching each one's own recipes", () => { - cy.intercept("GET", /\/recipes\?/, (req) => { - const tab = new URL(req.url).searchParams.get("tab"); - const body = tab === "perso" ? [omelette] : [ratatouille]; - req.reply({ statusCode: 200, body }); - }).as("listRecipes"); - - cy.visit("/recettes"); - cy.wait("@listRecipes"); - cy.contains(".recipe-table__name", "Ratatouille").should("be.visible"); - - // Not asserting the specific request URL here — React StrictMode (see - // main.tsx) double-invokes mount/update effects in dev, so this can - // legitimately fire twice; the rendered result converges either way - // (same reasoning as planning-page.cy.ts's week-navigation tests). - cy.contains(".recipe-tabs__tab", "Perso").click(); - cy.get(".recipe-tabs__tab.active").should("contain.text", "Perso"); - cy.contains(".recipe-table__name", "Omelette").should("be.visible"); - cy.contains(".recipe-table__name", "Ratatouille").should("not.exist"); - - // The disabled "Sources (bientôt)" placeholder never becomes active. - cy.contains(".recipe-tabs__tab", "Sources (bientôt)").should("be.disabled"); - }); - - it("searches within the active tab, debounced", () => { - cy.intercept("GET", /\/recipes\?/, (req) => { - const search = new URL(req.url).searchParams.get("search"); - req.reply({ statusCode: 200, body: search ? [omelette] : [ratatouille, omelette] }); - }).as("listRecipes"); - - cy.visit("/recettes"); - cy.wait("@listRecipes"); - cy.contains(".recipe-table__name", "Ratatouille").should("be.visible"); - - // Same "assert the rendered result, not the request count/URL" reasoning - // as the tab-switch test above — the 300ms debounce plus StrictMode's - // double-invoked effects make the exact number/order of requests an - // implementation detail, not something worth pinning down here. - cy.get(".recipes-page__search").type("Omel"); - cy.contains(".recipe-table__name", "Omelette").should("be.visible"); - cy.contains(".recipe-table__name", "Ratatouille").should("not.exist"); - }); - - it("opens a recipe's detail alongside the table when its row is selected", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] }); - cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe"); - - cy.visit("/recettes"); - cy.contains(".recipe-table__name", "Omelette").click(); - cy.wait("@getRecipe"); - - cy.url().should("include", "/recettes/2"); - // The table stays mounted (master-detail, not a page navigation) — - // both rows are still visible next to the detail panel. - cy.contains(".recipe-table__name", "Ratatouille").should("be.visible"); - cy.get("tr.selected .recipe-table__name").should("contain.text", "Omelette"); - - cy.get(".recipe-detail-panel").within(() => { - cy.contains("h2", "Omelette").should("be.visible"); - cy.contains("Une omelette toute simple.").should("be.visible"); - cy.contains("Battre les œufs.").should("be.visible"); - cy.contains("Cuire à la poêle.").should("be.visible"); - }); - }); - - it("shows a not-found message for a selected id the API rejects", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); - cy.intercept("GET", "**/recipes/999", { - statusCode: 404, - // ErrorCode.RECIPE_NOT_FOUND (packages/shared/src/errors/error-codes.ts) - // — RecipeDetailPanel only renders the "not found" message for this - // exact code, anything else falls into its generic error state. - body: { code: 4045, message: "not found" }, - }); - - cy.visit("/recettes/999"); - - cy.contains(".recipe-detail-panel", "Cette recette n'existe pas.").should("be.visible"); - }); - - it("toggles a recipe's favorite from the detail panel and reflects it in the table", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] }); - cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }); - cy.intercept("POST", "**/recipes/2/favorite", { statusCode: 204 }).as("favorite"); - - cy.visit("/recettes/2"); - - cy.contains(".recipe-table__name", "Omelette") - .find(".recipe-table__fav-mark") - .should("not.exist"); - - cy.get(".favorite-star-button").click(); - cy.wait("@favorite"); - - cy.get(".favorite-star-button").should("have.class", "is-favorite"); - cy.contains(".recipe-table__name", "Omelette").find(".recipe-table__fav-mark").should("exist"); - }); - - it("deletes a recipe after a two-step confirmation, then clears the selection", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] }); - cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }); - cy.intercept("DELETE", "**/recipes/2", { statusCode: 204 }).as("deleteRecipe"); - - cy.visit("/recettes/2"); - cy.contains(".recipe-detail-panel", "Omelette").should("be.visible"); - - cy.contains(".recipe-detail-panel__danger-button", "Supprimer").click(); - cy.contains(".recipe-detail-panel__danger-button", "Confirmer la suppression").click(); - cy.wait("@deleteRecipe"); - - cy.url().should("match", /\/recettes\/?$/); - cy.contains(".recipe-table__name", "Omelette").should("not.exist"); - cy.contains("Sélectionnez une recette dans le tableau").should("be.visible"); - }); - - it("links the new-recipe button to the recipe form", () => { - cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); - - cy.visit("/recettes"); - - cy.contains(".recipes-page__new-button", "Nouvelle recette").should( - "have.attr", - "href", - "/recettes/nouvelle", - ); - }); -}); diff --git a/apps/web/cypress/e2e/recipes.feature b/apps/web/cypress/e2e/recipes.feature new file mode 100644 index 0000000..cbec66c --- /dev/null +++ b/apps/web/cypress/e2e/recipes.feature @@ -0,0 +1,98 @@ +Feature: Recipe catalog + As a signed-in user + I want to browse, search, and manage my recipes in a master-detail view + So that I can find a recipe and see/edit its details without leaving the list + + Background: + Given I am signed in as "Alice" "Martin" + And my household id is 1 + And the disliked ingredients list is empty + + Scenario: Defaults to the Favoris tab and lists its recipes + Given the recipe catalog defaults to the favorites tab with "Ratatouille" + When I visit "/recettes" + Then the recipe list request should have been made + And I should see the heading "Recettes" + And the active tab should be "Favoris" + And the recipe "Ratatouille" should be visible in the table + And the recipe "Ratatouille" should be marked as favorite + And the recipe "Ratatouille" should show the diet badge "Végétarien" + + Scenario: Shows the empty state when a tab has no recipes + Given the recipe catalog is empty + When I visit "/recettes" + Then I should see "Aucune recette pour le moment." + + Scenario: Shows an error state when the catalog fails to load + Given the recipe catalog fails to load + When I visit "/recettes" + Then I should see "Impossible de charger" + + Scenario: Switches tabs, re-fetching each one's own recipes + Given the recipe catalog switches between tabs + When I visit "/recettes" + Then the recipe list request should have been made + And the recipe "Ratatouille" should be visible in the table + When I click the tab "Perso" + Then the active tab should be "Perso" + And the recipe "Omelette" should be visible in the table + And the recipe "Ratatouille" should not be visible in the table + And the tab "Sources (bientôt)" should be disabled + + Scenario: Searches within the active tab, debounced + Given the recipe catalog supports searching + When I visit "/recettes" + Then the recipe list request should have been made + And the recipe "Ratatouille" should be visible in the table + When I search for "Omel" + Then the recipe "Omelette" should be visible in the table + And the recipe "Ratatouille" should not be visible in the table + + Scenario: Opens a recipe's detail alongside the table when its row is selected + Given the recipe catalog contains "Ratatouille" and "Omelette" + And recipe 2's detail is available + When I visit "/recettes" + And I click the recipe "Omelette" in the table + Then the recipe detail request should have been made + And the URL should include "/recettes/2" + And the recipe "Ratatouille" should be visible in the table + And the selected row should be "Omelette" + And the recipe detail panel heading should be "Omelette" + And the recipe detail panel should show "Une omelette toute simple." + And the recipe detail panel should show "Battre les œufs." + And the recipe detail panel should show "Cuire à la poêle." + + Scenario: Shows a not-found message for a selected id the API rejects + Given the recipe catalog is empty + And recipe 999 does not exist + When I visit "/recettes/999" + Then the recipe detail panel should say the recipe doesn't exist + + Scenario: Toggles a recipe's favorite from the detail panel and reflects it in the table + Given the recipe catalog contains "Omelette" + And recipe 2's detail is available + And toggling recipe 2's favorite will succeed + When I visit "/recettes/2" + Then the recipe "Omelette" should not be marked as favorite + When I click the favorite star + Then the favorite request should have been made + And the favorite star should be marked as favorite + And the recipe "Omelette" should be marked as favorite + + Scenario: Deletes a recipe after a two-step confirmation, then clears the selection + Given the recipe catalog contains "Omelette" + And recipe 2's detail is available + And deleting recipe 2 will succeed + When I visit "/recettes/2" + Then the recipe detail panel heading should be "Omelette" + When I click "Supprimer" in the recipe detail panel + And I confirm the deletion in the recipe detail panel + Then the delete request should have been made + And the URL should match the recipes list + And the recipe "Omelette" should not be visible in the table + And I should see "Sélectionnez une recette dans le tableau" + + Scenario: Links the new-recipe button to the recipe form + Given the recipe catalog is empty + When I visit "/recettes" + Then the new-recipe button should link to "/recettes/nouvelle" diff --git a/apps/web/cypress/e2e/recipes.steps.ts b/apps/web/cypress/e2e/recipes.steps.ts new file mode 100644 index 0000000..ae61ff6 --- /dev/null +++ b/apps/web/cypress/e2e/recipes.steps.ts @@ -0,0 +1,215 @@ +import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; + +const vegetarien = { id: 1, key: "vegetarian" }; +const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" }; + +const ratatouille = { + id: 1, + name: "Ratatouille", + description: null, + picture: null, + authorId: 1, + visibility: "PERSONAL", + allergens: [], + diets: [vegetarien], + isFavorite: true, +}; + +const omelette = { + id: 2, + name: "Omelette", + description: null, + picture: null, + authorId: 1, + visibility: "PERSONAL", + allergens: [oeufs], + diets: [], + isFavorite: false, +}; + +const omeletteDetail = { + ...omelette, + description: "Une omelette toute simple.", + ingredients: [ + { + ingredient: { + id: 10, + key: "egg", + icon: "EGG", + category: "CREMERIE_FROMAGE", + subcategory: "OEUFS", + allergens: [oeufs], + diets: [], + }, + quantity: 3, + unit: "unité", + }, + ], + steps: [ + { id: 1, description: "Battre les œufs.", picture: null, order: 1 }, + { id: 2, description: "Cuire à la poêle.", picture: null, order: 2 }, + ], +}; + +Given("the disliked ingredients list is empty", () => { + cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); +}); + +Given("the recipe catalog defaults to the favorites tab with {string}", () => { + cy.intercept("GET", /\/recipes\?/, (req) => { + expect(req.url).to.include("tab=favoris"); + req.reply({ statusCode: 200, body: [ratatouille] }); + }).as("listRecipes"); +}); + +Given("the recipe catalog is empty", () => { + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); +}); + +Given("the recipe catalog fails to load", () => { + cy.intercept("GET", /\/recipes\?/, { statusCode: 500, body: { code: 5000, message: "boom" } }); +}); + +Given("the recipe catalog switches between tabs", () => { + cy.intercept("GET", /\/recipes\?/, (req) => { + const tab = new URL(req.url).searchParams.get("tab"); + const body = tab === "perso" ? [omelette] : [ratatouille]; + req.reply({ statusCode: 200, body }); + }).as("listRecipes"); +}); + +Given("the recipe catalog supports searching", () => { + cy.intercept("GET", /\/recipes\?/, (req) => { + const search = new URL(req.url).searchParams.get("search"); + req.reply({ statusCode: 200, body: search ? [omelette] : [ratatouille, omelette] }); + }).as("listRecipes"); +}); + +Given("the recipe catalog contains {string} and {string}", () => { + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] }); +}); + +Given("the recipe catalog contains {string}", () => { + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] }); +}); + +Given("recipe 2's detail is available", () => { + cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe"); +}); + +Given("recipe 999 does not exist", () => { + cy.intercept("GET", "**/recipes/999", { + statusCode: 404, + // ErrorCode.RECIPE_NOT_FOUND (packages/shared/src/errors/error-codes.ts) + // — RecipeDetailPanel only renders the "not found" message for this + // exact code, anything else falls into its generic error state. + body: { code: 4045, message: "not found" }, + }); +}); + +Given("toggling recipe 2's favorite will succeed", () => { + cy.intercept("POST", "**/recipes/2/favorite", { statusCode: 204 }).as("favorite"); +}); + +Given("deleting recipe 2 will succeed", () => { + cy.intercept("DELETE", "**/recipes/2", { statusCode: 204 }).as("deleteRecipe"); +}); + +Then("the recipe list request should have been made", () => { + cy.wait("@listRecipes"); +}); + +Then("the recipe detail request should have been made", () => { + cy.wait("@getRecipe"); +}); + +Then("the active tab should be {string}", (tab: string) => { + cy.get(".recipe-tabs__tab.active").should("contain.text", tab); +}); + +Then("the recipe {string} should be visible in the table", (name: string) => { + cy.contains(".recipe-table__name", name).should("be.visible"); +}); + +Then("the recipe {string} should not be visible in the table", (name: string) => { + cy.contains(".recipe-table__name", name).should("not.exist"); +}); + +Then("the recipe {string} should be marked as favorite", (name: string) => { + cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("exist"); +}); + +Then("the recipe {string} should not be marked as favorite", (name: string) => { + cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("not.exist"); +}); + +Then("the recipe {string} should show the diet badge {string}", (name: string, badge: string) => { + cy.contains(".recipe-table__name", name) + .parents("tr") + .find(".diet-badge") + .should("contain.text", badge); +}); + +Then("the tab {string} should be disabled", (text: string) => { + cy.contains(".recipe-tabs__tab", text).should("be.disabled"); +}); + +When("I click the tab {string}", (text: string) => { + cy.contains(".recipe-tabs__tab", text).click(); +}); + +When("I search for {string}", (text: string) => { + cy.get(".recipes-page__search").type(text); +}); + +When("I click the recipe {string} in the table", (text: string) => { + cy.contains(".recipe-table__name", text).click(); +}); + +Then("the selected row should be {string}", (text: string) => { + cy.get("tr.selected .recipe-table__name").should("contain.text", text); +}); + +Then("the recipe detail panel heading should be {string}", (text: string) => { + cy.get(".recipe-detail-panel").contains("h2", text).should("be.visible"); +}); + +Then("the recipe detail panel should show {string}", (text: string) => { + cy.get(".recipe-detail-panel").contains(text).should("be.visible"); +}); + +Then("the recipe detail panel should say the recipe doesn't exist", () => { + cy.contains(".recipe-detail-panel", "Cette recette n'existe pas.").should("be.visible"); +}); + +When("I click the favorite star", () => { + cy.get(".favorite-star-button").click(); +}); + +Then("the favorite request should have been made", () => { + cy.wait("@favorite"); +}); + +Then("the favorite star should be marked as favorite", () => { + cy.get(".favorite-star-button").should("have.class", "is-favorite"); +}); + +When("I click {string} in the recipe detail panel", (text: string) => { + cy.contains(".recipe-detail-panel__danger-button", text).click(); +}); + +When("I confirm the deletion in the recipe detail panel", () => { + cy.contains(".recipe-detail-panel__danger-button", "Confirmer la suppression").click(); +}); + +Then("the delete request should have been made", () => { + cy.wait("@deleteRecipe"); +}); + +Then("the URL should match the recipes list", () => { + cy.url().should("match", /\/recettes\/?$/); +}); + +Then("the new-recipe button should link to {string}", (href: string) => { + cy.contains(".recipes-page__new-button", "Nouvelle recette").should("have.attr", "href", href); +}); diff --git a/apps/web/cypress/e2e/sidebar.cy.ts b/apps/web/cypress/e2e/sidebar.cy.ts deleted file mode 100644 index 2d69946..0000000 --- a/apps/web/cypress/e2e/sidebar.cy.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null, - dietId: null, -}; - -describe("Sidebar — settings menu and account menu", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - // Not "**/planning*" — that glob also matches the Vite dev request for - // planning-page.scss (see planning-page.cy.ts for the same gotcha). - cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); - }); - - it("no longer lists Foyer in the main nav", () => { - cy.visit("/"); - cy.get(".app-sidebar__nav a").should("not.contain", "Foyer"); - }); - - it("reveals the four settings pages behind the Paramètres toggle", () => { - cy.visit("/"); - - cy.contains("a", "Compte").should("not.exist"); - cy.contains("button", "Paramètres").click(); - - cy.contains("a", "Compte").should("have.attr", "href", "/parametres/compte"); - cy.contains("a", "Préférences alimentaires").should( - "have.attr", - "href", - "/parametres/preferences", - ); - cy.contains("a", "Foyer").should("have.attr", "href", "/parametres/foyer"); - cy.contains("a", "Préférences utilisateur").should( - "have.attr", - "href", - "/parametres/preferences-utilisateur", - ); - }); - - it("opens the account menu from the greeting and links to Mon compte", () => { - cy.visit("/"); - - cy.contains("a", "Mon compte").should("not.exist"); - cy.contains("button", "Bonjour Alice").click(); - cy.contains("a", "Mon compte").click(); - - cy.url().should("include", "/parametres/compte"); - }); - - it("redirects the old /foyer path to /parametres/foyer", () => { - cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); - cy.visit("/foyer"); - cy.url().should("include", "/parametres/foyer"); - }); - - it("collapses to an icon-only rail and back, persisting the choice across reloads", () => { - cy.visit("/"); - - cy.get(".app-sidebar").should("not.have.class", "collapsed"); - cy.contains("nav a", "Planning").should("be.visible"); - - cy.get(".app-sidebar__collapse-toggle").click(); - cy.get(".app-sidebar").should("have.class", "collapsed"); - // The label text hides (not removed from the DOM — still there for the - // `title` tooltip/accessibility), while the link itself stays visible, - // icon-only. - cy.contains("span.label", "Planning").should("exist").and("not.be.visible"); - cy.get("nav a[title='Planning']").should("be.visible"); - - cy.reload(); - cy.get(".app-sidebar").should("have.class", "collapsed"); - - cy.get(".app-sidebar__collapse-toggle").click(); - cy.get(".app-sidebar").should("not.have.class", "collapsed"); - cy.contains("nav a", "Planning").should("be.visible"); - }); -}); diff --git a/apps/web/cypress/e2e/sidebar.feature b/apps/web/cypress/e2e/sidebar.feature new file mode 100644 index 0000000..2a2a5e3 --- /dev/null +++ b/apps/web/cypress/e2e/sidebar.feature @@ -0,0 +1,47 @@ +Feature: Sidebar — settings menu and account menu + As a signed-in user + I want to reach the settings pages and collapse the sidebar + So that I can navigate the app comfortably regardless of screen space + + Background: + Given I am signed in as "Alice" "Martin" + And the planning request returns nothing + + Scenario: No longer lists Foyer in the main nav + When I visit "/" + Then the sidebar's main nav should not mention "Foyer" + + Scenario: Reveals the four settings pages behind the Paramètres toggle + When I visit "/" + Then I should not see "Compte" + When I click the button "Paramètres" + Then the link "Compte" should point to "/parametres/compte" + And the link "Préférences alimentaires" should point to "/parametres/preferences" + And the link "Foyer" should point to "/parametres/foyer" + And the link "Préférences utilisateur" should point to "/parametres/preferences-utilisateur" + + Scenario: Opens the account menu from the greeting and links to Mon compte + When I visit "/" + Then I should not see "Mon compte" + When I open the account menu + And I click the link "Mon compte" + Then the URL should include "/parametres/compte" + + Scenario: Redirects the old /foyer path to /parametres/foyer + Given the household request returns no household + When I visit "/foyer" + Then the URL should include "/parametres/foyer" + + Scenario: Collapses to an icon-only rail and back, persisting the choice across reloads + When I visit "/" + Then the sidebar should not be collapsed + And the nav link "Planning" should be visible + When I toggle the sidebar collapse + Then the sidebar should be collapsed + And the nav link "Planning"'s label should be hidden + And the nav link titled "Planning" should be visible + When I reload the page + Then the sidebar should be collapsed + When I toggle the sidebar collapse + Then the sidebar should not be collapsed + And the nav link "Planning" should be visible diff --git a/apps/web/cypress/e2e/sidebar.steps.ts b/apps/web/cypress/e2e/sidebar.steps.ts new file mode 100644 index 0000000..a135363 --- /dev/null +++ b/apps/web/cypress/e2e/sidebar.steps.ts @@ -0,0 +1,36 @@ +import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; + +Then("the sidebar's main nav should not mention {string}", (text: string) => { + cy.get(".app-sidebar__nav a").should("not.contain", text); +}); + +Then("the sidebar should be collapsed", () => { + cy.get(".app-sidebar").should("have.class", "collapsed"); +}); + +Then("the sidebar should not be collapsed", () => { + cy.get(".app-sidebar").should("not.have.class", "collapsed"); +}); + +When("I toggle the sidebar collapse", () => { + cy.get(".app-sidebar__collapse-toggle").click(); +}); + +Then("the nav link {string} should be visible", (text: string) => { + cy.contains("nav a", text).should("be.visible"); +}); + +// The label text stays in the DOM (still there for the `title` +// tooltip/accessibility) — it's hidden via CSS, not removed, hence +// `.should("exist").and("not.be.visible")` rather than `.should("not.exist")`. +Then("the nav link {string}'s label should be hidden", (text: string) => { + cy.contains("span.label", text).should("exist").and("not.be.visible"); +}); + +Then("the nav link titled {string} should be visible", (title: string) => { + cy.get(`nav a[title="${title}"]`).should("be.visible"); +}); + +When("I reload the page", () => { + cy.reload(); +}); diff --git a/apps/web/cypress/e2e/smoke.cy.ts b/apps/web/cypress/e2e/smoke.cy.ts deleted file mode 100644 index 5333aa7..0000000 --- a/apps/web/cypress/e2e/smoke.cy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ErrorCode } from "@batch-cooking/shared"; - -describe("smoke test", () => { - it("redirects an unauthenticated visitor to the login page", () => { - cy.intercept("GET", "**/auth/me", { - statusCode: 401, - body: { code: ErrorCode.NOT_AUTHENTICATED, message: "Not authenticated" }, - }); - cy.visit("/"); - cy.url().should("include", "/login"); - cy.contains("h1", "Se connecter").should("be.visible"); - }); -}); diff --git a/apps/web/cypress/e2e/smoke.feature b/apps/web/cypress/e2e/smoke.feature new file mode 100644 index 0000000..b1e7919 --- /dev/null +++ b/apps/web/cypress/e2e/smoke.feature @@ -0,0 +1,10 @@ +Feature: Unauthenticated access + As a visitor without a session + I want to be redirected to the login page + So that I can sign in before using the app + + Scenario: Visiting the app without a session redirects to login + Given I am not signed in + When I visit "/" + Then the URL should include "/login" + And I should see the heading "Se connecter" diff --git a/apps/web/cypress/e2e/user-preferences.cy.ts b/apps/web/cypress/e2e/user-preferences.cy.ts deleted file mode 100644 index 0c09b1c..0000000 --- a/apps/web/cypress/e2e/user-preferences.cy.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: null, - dietId: null, -}; - -describe("User preferences (/parametres/preferences-utilisateur)", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - }); - - it("shows SYSTEM selected by default", () => { - cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme: "SYSTEM" } }); - - cy.visit("/parametres/preferences-utilisateur"); - - cy.contains("label", "Système").find("input[type=radio]").should("be.checked"); - cy.contains("label", "Clair").find("input[type=radio]").should("not.be.checked"); - cy.contains("label", "Sombre").find("input[type=radio]").should("not.be.checked"); - // SYSTEM never sets an override — the OS/browser preference decides. - cy.get("html").should("not.have.attr", "data-theme"); - }); - - it("shows the previously saved theme selected, and applies it to the document", () => { - cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme: "DARK" } }); - - cy.visit("/parametres/preferences-utilisateur"); - - cy.contains("label", "Sombre").find("input[type=radio]").should("be.checked"); - cy.get("html").should("have.attr", "data-theme", "dark"); - }); - - it("switching theme autosaves and applies immediately, no explicit save button", () => { - cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme: "SYSTEM" } }); - cy.intercept("PATCH", "**/preferences", { statusCode: 200, body: { theme: "LIGHT" } }).as( - "updatePreferences", - ); - - cy.visit("/parametres/preferences-utilisateur"); - cy.contains("button", "Enregistrer").should("not.exist"); - - cy.contains("label", "Clair").click(); - - cy.wait("@updatePreferences").its("request.body").should("deep.equal", { theme: "LIGHT" }); - cy.contains("Enregistré ✓").should("be.visible"); - cy.get("html").should("have.attr", "data-theme", "light"); - }); -}); diff --git a/apps/web/cypress/e2e/user-preferences.feature b/apps/web/cypress/e2e/user-preferences.feature new file mode 100644 index 0000000..990af72 --- /dev/null +++ b/apps/web/cypress/e2e/user-preferences.feature @@ -0,0 +1,31 @@ +Feature: User preferences (theme) + As a signed-in user + I want to switch between system/light/dark theme + So that the app matches my preference, applied immediately + + Background: + Given I am signed in as "Alice" "Martin" + + Scenario: Shows SYSTEM selected by default + Given my saved theme preference is "SYSTEM" + When I visit "/parametres/preferences-utilisateur" + Then the radio "Système" should be checked + And the radio "Clair" should not be checked + And the radio "Sombre" should not be checked + And the page should have no theme override + + Scenario: Shows the previously saved theme selected, and applies it to the document + Given my saved theme preference is "DARK" + When I visit "/parametres/preferences-utilisateur" + Then the radio "Sombre" should be checked + And the page theme should be "dark" + + Scenario: Switching theme autosaves and applies immediately, no explicit save button + Given my saved theme preference is "SYSTEM" + And updating the theme preference will succeed + When I visit "/parametres/preferences-utilisateur" + Then I should not see "Enregistrer" + When I click the radio "Clair" + Then the theme update request should have been made with theme "LIGHT" + And I should see "Enregistré ✓" + And the page theme should be "light" diff --git a/apps/web/cypress/e2e/user-preferences.steps.ts b/apps/web/cypress/e2e/user-preferences.steps.ts new file mode 100644 index 0000000..e888bcd --- /dev/null +++ b/apps/web/cypress/e2e/user-preferences.steps.ts @@ -0,0 +1,15 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; + +Given("my saved theme preference is {string}", (theme: string) => { + cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme } }); +}); + +Given("updating the theme preference will succeed", () => { + cy.intercept("PATCH", "**/preferences", { statusCode: 200, body: { theme: "LIGHT" } }).as( + "updatePreferences", + ); +}); + +Then("the theme update request should have been made with theme {string}", (theme: string) => { + cy.wait("@updatePreferences").its("request.body").should("deep.equal", { theme }); +}); diff --git a/apps/web/cypress/support/profile.ts b/apps/web/cypress/support/profile.ts new file mode 100644 index 0000000..a0236ed --- /dev/null +++ b/apps/web/cypress/support/profile.ts @@ -0,0 +1,33 @@ +import type { SafeUserProfile } from "@batch-cooking/shared"; + +/** + * Mutable per-scenario signed-in profile, built up across several `Given` + * steps (see common.steps.ts's "I am signed in as .../my household id + * is.../my diet id is...") before the final `cy.visit` — each step + * re-registers the `GET **\/auth/me` intercept with the updated shape, so + * only the last one (i.e. the fully assembled profile) is ever actually + * requested by the app. Reset before every scenario by the `Before` hook in + * common.steps.ts, so scenarios never leak state into one another. + */ +export let currentProfile: SafeUserProfile | null = null; + +export function resetProfile() { + currentProfile = null; +} + +export function buildProfile(overrides: Partial = {}): SafeUserProfile { + return { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null, + dietId: null, + ...overrides, + }; +} + +export function setCurrentProfile(profile: SafeUserProfile) { + currentProfile = profile; +} diff --git a/apps/web/cypress/support/step_definitions/common.steps.ts b/apps/web/cypress/support/step_definitions/common.steps.ts new file mode 100644 index 0000000..10e38bd --- /dev/null +++ b/apps/web/cypress/support/step_definitions/common.steps.ts @@ -0,0 +1,184 @@ +import { Before, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; +import { buildProfile, currentProfile, resetProfile, setCurrentProfile } from "../profile"; + +// Steps shared across every feature — signing in/out, navigation, and +// generic UI assertions/interactions phrased the same way regardless of +// which page they happen to run against. Anything specific to one feature +// (its own API responses, its own DOM structure) lives in that feature's +// own `.steps.ts` instead — same split as apps/api's +// step-definitions/ (shared "profile already exists" vs. feature-specific +// steps). +// +// Mocks the API via `cy.intercept` — this job doesn't run a live backend +// (see .github/workflows/ci.yml); apps/api's own Mocha/Cucumber suites +// cover real API behavior against a real database. + +Before(() => { + resetProfile(); +}); + +Given("I am not signed in", () => { + cy.intercept("GET", "**/auth/me", { statusCode: 401 }); +}); + +Given("I am signed in as {string} {string}", (firstName: string, lastName: string) => { + setCurrentProfile( + buildProfile({ + firstName, + lastName, + email: `${firstName.toLowerCase()}@example.com`, + }), + ); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile }); +}); + +// Composable with the step above — re-registers the same intercept with an +// updated profile field. Order between these doesn't matter as long as they +// all run before the scenario's `visit`/`When` step: Cypress resolves +// multiple `cy.intercept` calls on the same route by giving the +// most-recently-registered one priority, so the final, fully-assembled +// profile is always what the app actually receives. +Given("my household id is {int}", (houseId: number) => { + if (!currentProfile) { + throw new Error('"my household id is" must follow "I am signed in as ..."'); + } + setCurrentProfile({ ...currentProfile, houseId }); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile }); +}); + +Given("my diet id is {int}", (dietId: number) => { + if (!currentProfile) { + throw new Error('"my diet id is" must follow "I am signed in as ..."'); + } + setCurrentProfile({ ...currentProfile, dietId }); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile }); +}); + +Given("my user id is {int}", (id: number) => { + if (!currentProfile) { + throw new Error('"my user id is" must follow "I am signed in as ..."'); + } + setCurrentProfile({ ...currentProfile, id }); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile }); +}); + +When("I visit {string}", (path: string) => { + cy.visit(path); +}); + +Then("the URL should include {string}", (fragment: string) => { + cy.url().should("include", fragment); +}); + +Then("the URL should not include {string}", (fragment: string) => { + cy.url().should("not.include", fragment); +}); + +Then("the URL should be the home page", () => { + cy.url().should("eq", `${Cypress.config().baseUrl}/`); +}); + +Then("I should see {string}", (text: string) => { + cy.contains(text).should("be.visible"); +}); + +Then("I should see the heading {string}", (text: string) => { + cy.contains("h1", text).should("be.visible"); +}); + +Then("I should not see {string}", (text: string) => { + cy.contains(text).should("not.exist"); +}); + +When("I click the button {string}", (text: string) => { + cy.contains("button", text).click(); +}); + +When("I click the link {string}", (text: string) => { + cy.contains("a", text).click(); +}); + +When("I fill in the {string} field with {string}", (fieldId: string, value: string) => { + cy.get(`#${fieldId}`).type(value); +}); + +When("I clear the {string} field", (fieldId: string) => { + cy.get(`#${fieldId}`).clear(); +}); + +Then("the {string} field should have the value {string}", (fieldId: string, value: string) => { + cy.get(`#${fieldId}`).should("have.value", value); +}); + +When("I select {string} from the {string} field", (value: string, fieldId: string) => { + cy.get(`#${fieldId}`).select(value); +}); + +Then("I should see the section {string}", (legend: string) => { + cy.contains("legend", legend).should("be.visible"); +}); + +Then("the checkbox {string} should be checked", (label: string) => { + cy.contains("label", label).find("input[type=checkbox]").should("be.checked"); +}); + +Then("the checkbox {string} should not be checked", (label: string) => { + cy.contains("label", label).find("input[type=checkbox]").should("not.be.checked"); +}); + +When("I check the checkbox {string}", (label: string) => { + cy.contains("label", label).find("input[type=checkbox]").check(); +}); + +Then("the radio {string} should be checked", (label: string) => { + cy.contains("label", label).find("input[type=radio]").should("be.checked"); +}); + +Then("the radio {string} should not be checked", (label: string) => { + cy.contains("label", label).find("input[type=radio]").should("not.be.checked"); +}); + +When("I click the radio {string}", (label: string) => { + cy.contains("label", label).click(); +}); + +Then("the page should have no theme override", () => { + cy.get("html").should("not.have.attr", "data-theme"); +}); + +Then("the page theme should be {string}", (theme: string) => { + cy.get("html").should("have.attr", "data-theme", theme); +}); + +// Freezes `Date` so "today"/"this week" assertions are deterministic +// instead of depending on the day the suite happens to run. +Given("today is frozen at {string}", (iso: string) => { + cy.clock(new Date(iso), ["Date"]); +}); + +Given("the viewport is {int} by {int}", (width: number, height: number) => { + cy.viewport(width, height); +}); + +Then("the {string} button should not be disabled", (text: string) => { + cy.contains("button", text).should("not.be.disabled"); +}); + +When("I open the account menu", () => { + cy.get(".app-sidebar__account-toggle").click(); +}); + +// Not "**/planning*" — that glob also matches the Vite dev request for +// planning-page.scss. Used by any feature that lands on the home page +// (Planning) but isn't itself testing the planning grid's content. +Given("the planning request returns nothing", () => { + cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); +}); + +Given("the household request returns no household", () => { + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); +}); + +Then("the link {string} should point to {string}", (text: string, href: string) => { + cy.contains("a", text).should("have.attr", "href", href); +}); diff --git a/apps/web/package.json b/apps/web/package.json index eb06d08..83fca96 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,6 +24,8 @@ "zod": "^3.25.76" }, "devDependencies": { + "@badeball/cypress-cucumber-preprocessor": "^26.0.0", + "@bahmutov/cypress-esbuild-preprocessor": "^2.2.8", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88b6a91..9f55992 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,12 @@ importers: specifier: ^3.25.76 version: 3.25.76 devDependencies: + '@badeball/cypress-cucumber-preprocessor': + specifier: ^26.0.0 + version: 26.0.0(@babel/core@7.29.7)(cypress@13.17.0)(typescript@5.9.3) + '@bahmutov/cypress-esbuild-preprocessor': + specifier: ^2.2.8 + version: 2.2.8(esbuild@0.28.2) '@types/node': specifier: ^22.9.0 version: 22.20.1 @@ -221,6 +227,18 @@ importers: packages: + '@actions/core@2.0.3': + resolution: {integrity: sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA==, tarball: https://registry.npmjs.org/@actions/core/-/core-2.0.3.tgz} + + '@actions/exec@2.0.0': + resolution: {integrity: sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==, tarball: https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz} + + '@actions/http-client@3.0.2': + resolution: {integrity: sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==, tarball: https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz} + + '@actions/io@2.0.0': + resolution: {integrity: sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==, tarball: https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, tarball: https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz} engines: {node: '>=6.9.0'} @@ -280,6 +298,12 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==, tarball: https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.29.7': resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==, tarball: https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz} engines: {node: '>=6.9.0'} @@ -308,6 +332,18 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, tarball: https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz} engines: {node: '>=6.9.0'} + '@badeball/cypress-cucumber-preprocessor@26.0.0': + resolution: {integrity: sha512-pAY1MrN/H0DztwQkLoiRA8GSduDiJOjCD17Tn525v3GxL5pzXSXsQoalzo77jCVXg8CAl75QoZ/5PsJF34WPsA==, tarball: https://registry.npmjs.org/@badeball/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-26.0.0.tgz} + engines: {node: ^20.12.0 || ^21.7.0 || >=22} + hasBin: true + peerDependencies: + cypress: ^12.0.0 || ^13.0.0 || ^14.0.0 || >=15.0.0 <=15.17.0 || ^15.18.0 + + '@bahmutov/cypress-esbuild-preprocessor@2.2.8': + resolution: {integrity: sha512-pN90es4T1DYcQbBuDEpa7yotC/VhE+BgfHIEGpS0uxibTIqt7g4ZITQZbTFcaQATZh3QmPIMMkeZyBE7PWf1LA==, tarball: https://registry.npmjs.org/@bahmutov/cypress-esbuild-preprocessor/-/cypress-esbuild-preprocessor-2.2.8.tgz} + peerDependencies: + esbuild: '>=0.17.0' + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==, tarball: https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz} engines: {node: '>=14.21.3'} @@ -394,6 +430,11 @@ packages: '@cucumber/gherkin@42.0.0': resolution: {integrity: sha512-kBcv+NV4FGQYX6NIsSCjsjaX8MsRdEH559BQ9xEFPgkLQX/Z//JZrY94fpjxW6quo4V5kxlzFiiVC5xouF9Akg==, tarball: https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-42.0.0.tgz} + '@cucumber/html-formatter@24.0.0': + resolution: {integrity: sha512-F+3Y2yXZBjj8kYhUg++8LjHhfne0ddm8KS9bYf29nF5f9D3g3elZjWnxa9eMKfkIQGlW5EcmzMYyu6iEF+dV5g==, tarball: https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-24.0.0.tgz} + peerDependencies: + '@cucumber/messages': '>=18' + '@cucumber/html-formatter@24.1.0': resolution: {integrity: sha512-5TBLnBT+tIvtamw/jtblBVgzQ2/ckd+md8I0rwkEk29vFMCF68jNwhyONGq6leUYMq4kVF+KdFhE8xGrSc3kJw==, tarball: https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-24.1.0.tgz} peerDependencies: @@ -430,6 +471,9 @@ packages: peerDependencies: '@cucumber/messages': '*' + '@cucumber/tag-expressions@10.0.0': + resolution: {integrity: sha512-uap3XSQFxj5HYAHQIShGeS2zotMEnUmnEVjyuhp39j7tDUvaU64ArHzRkLPSWRavEI8ycdeNdwef1pcM/n6pSQ==, tarball: https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-10.0.0.tgz} + '@cucumber/tag-expressions@11.0.0': resolution: {integrity: sha512-7Uo6dYST8xZbHwwemXCt7Kv80Yh/SE85DyOed1/FTwxRXs3NYVOCm6e/h8DxlhTGIpD6rYXfpY3nHnIXAFMBlw==, tarball: https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-11.0.0.tgz} @@ -440,6 +484,14 @@ packages: '@cypress/xvfb@1.2.4': resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==, tarball: https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz} + '@dependents/detective-less@5.0.3': + resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==, tarball: https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.3.tgz} + engines: {node: '>=18'} + + '@discoveryjs/json-ext@1.1.0': + resolution: {integrity: sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==, tarball: https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz} + engines: {node: '>=14.17.0'} + '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==, tarball: https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz} @@ -761,6 +813,10 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==, tarball: https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, tarball: https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} @@ -791,6 +847,18 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==, tarball: https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz} engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, tarball: https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, tarball: https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz} + engines: {node: '>= 8'} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==, tarball: https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz} @@ -874,6 +942,10 @@ packages: resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==, tarball: https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz} engines: {node: '>=10'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, tarball: https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz} + engines: {node: '>=14'} + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==, tarball: https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz} engines: {node: '>=16.13'} @@ -1136,12 +1208,53 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==, tarball: https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz} + '@typescript-eslint/project-service@8.67.0': + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==, tarball: https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.67.0': + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==, tarball: https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.67.0': + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.67.0': + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.67.0': + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==, tarball: https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==, tarball: https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==, tarball: https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==, tarball: https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==, tarball: https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==, tarball: https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==, tarball: https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz} @@ -1149,6 +1262,15 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, tarball: https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz} engines: {node: '>= 0.6'} + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==, tarball: https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz} + engines: {node: '>=0.4.0'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==, tarball: https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz} engines: {node: '>= 6.0.0'} @@ -1177,10 +1299,17 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, tarball: https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz} engines: {node: '>= 8'} + app-module-path@2.2.0: + resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==, tarball: https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz} + aproba@2.1.0: resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==, tarball: https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz} @@ -1202,6 +1331,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==, tarball: https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==, tarball: https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz} @@ -1222,6 +1355,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, tarball: https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz} engines: {node: '>=12'} + ast-module-types@6.0.2: + resolution: {integrity: sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==, tarball: https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.2.tgz} + engines: {node: '>=18'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==, tarball: https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz} engines: {node: '>=8'} @@ -1236,6 +1373,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==, tarball: https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz} engines: {node: '>= 4.0.0'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, tarball: https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz} + engines: {node: '>= 0.4'} + aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==, tarball: https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz} @@ -1248,6 +1389,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, tarball: https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, tarball: https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, tarball: https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz} @@ -1279,6 +1424,10 @@ packages: brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmjs.org/braces/-/braces-3.0.3.tgz} engines: {node: '>=8'} @@ -1315,10 +1464,18 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, tarball: https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==, tarball: https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, tarball: https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, tarball: https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz} + engines: {node: '>=6'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, tarball: https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz} engines: {node: '>=10'} @@ -1349,6 +1506,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz} + engines: {node: '>= 14.16.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz} engines: {node: '>= 20.19.0'} @@ -1373,6 +1534,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==, tarball: https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz} engines: {node: 10.* || >= 12.*} + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==, tarball: https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz} + engines: {node: '>= 0.2.0'} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==, tarball: https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz} engines: {node: '>=8'} @@ -1380,6 +1545,14 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==, tarball: https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, tarball: https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==, tarball: https://registry.npmjs.org/clone/-/clone-1.0.4.tgz} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, tarball: https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz} engines: {node: '>=7.0.0'} @@ -1394,10 +1567,18 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, tarball: https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz} + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==, tarball: https://registry.npmjs.org/colors/-/colors-1.0.3.tgz} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz} engines: {node: '>= 0.8'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==, tarball: https://registry.npmjs.org/commander/-/commander-12.1.0.tgz} + engines: {node: '>=18'} + commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==, tarball: https://registry.npmjs.org/commander/-/commander-15.0.0.tgz} engines: {node: '>=22.12.0'} @@ -1406,6 +1587,10 @@ packages: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==, tarball: https://registry.npmjs.org/commander/-/commander-6.2.1.tgz} engines: {node: '>= 6'} + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==, tarball: https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz} + engines: {node: '>= 18'} + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==, tarball: https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz} engines: {node: '>=4.0.0'} @@ -1419,6 +1604,10 @@ packages: console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==, tarball: https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz} + console.table@0.10.0: + resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==, tarball: https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz} + engines: {node: '> 0.10'} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, tarball: https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz} engines: {node: '>= 0.6'} @@ -1462,6 +1651,15 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, tarball: https://registry.npmjs.org/cors/-/cors-2.8.6.tgz} engines: {node: '>= 0.10'} + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==, tarball: https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cross-env@10.1.0: resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==, tarball: https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz} engines: {node: '>=20'} @@ -1502,6 +1700,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, tarball: https://registry.npmjs.org/debug/-/debug-4.3.4.tgz} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} engines: {node: '>=6.0'} @@ -1519,6 +1726,21 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, tarball: https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==, tarball: https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz} + engines: {node: '>= 0.4'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==, tarball: https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, tarball: https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, tarball: https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz} engines: {node: '>=0.4.0'} @@ -1530,6 +1752,11 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, tarball: https://registry.npmjs.org/depd/-/depd-2.0.0.tgz} engines: {node: '>= 0.8'} + dependency-tree@11.5.0: + resolution: {integrity: sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg==, tarball: https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.5.0.tgz} + engines: {node: '>=18'} + hasBin: true + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, tarball: https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1538,6 +1765,49 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz} engines: {node: '>=8'} + detective-amd@6.1.0: + resolution: {integrity: sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==, tarball: https://registry.npmjs.org/detective-amd/-/detective-amd-6.1.0.tgz} + engines: {node: '>=18'} + hasBin: true + + detective-cjs@6.1.1: + resolution: {integrity: sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==, tarball: https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.1.1.tgz} + engines: {node: '>=18'} + + detective-es6@5.0.2: + resolution: {integrity: sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==, tarball: https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.2.tgz} + engines: {node: '>=18'} + + detective-postcss@8.0.4: + resolution: {integrity: sha512-DZ7M/hWPZyr17ZUdoQ+TVXaPj70mYr4XXrAE+GeJbca44haCvZgb191L/jLJmFYewhxRJuBd4lUtNSu986TXag==, tarball: https://registry.npmjs.org/detective-postcss/-/detective-postcss-8.0.4.tgz} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4.47 + + detective-sass@6.0.2: + resolution: {integrity: sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==, tarball: https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.2.tgz} + engines: {node: '>=18'} + + detective-scss@5.0.2: + resolution: {integrity: sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==, tarball: https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.2.tgz} + engines: {node: '>=18'} + + detective-stylus@5.0.1: + resolution: {integrity: sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==, tarball: https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz} + engines: {node: '>=18'} + + detective-typescript@14.1.2: + resolution: {integrity: sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==, tarball: https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.1.2.tgz} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + + detective-vue2@2.3.0: + resolution: {integrity: sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==, tarball: https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.3.0.tgz} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==, tarball: https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz} @@ -1549,6 +1819,10 @@ packages: resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==, tarball: https://registry.npmjs.org/diff/-/diff-5.2.2.tgz} engines: {node: '>=0.3.1'} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==, tarball: https://registry.npmjs.org/diff/-/diff-7.0.0.tgz} + engines: {node: '>=0.3.1'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, tarball: https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz} engines: {node: '>=12'} @@ -1560,6 +1834,12 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==, tarball: https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, tarball: https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz} + + easy-table@1.1.0: + resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==, tarball: https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==, tarball: https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz} @@ -1575,6 +1855,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, tarball: https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz} engines: {node: '>= 0.8'} @@ -1582,10 +1865,25 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==, tarball: https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, tarball: https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz} engines: {node: '>=8.6'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==, tarball: https://registry.npmjs.org/entities/-/entities-7.0.1.tgz} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==, tarball: https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==, tarball: https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==, tarball: https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz} @@ -1597,6 +1895,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, tarball: https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==, tarball: https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==, tarball: https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz} engines: {node: '>= 0.4'} @@ -1630,6 +1931,31 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, tarball: https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==, tarball: https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz} + engines: {node: '>=6.0'} + hasBin: true + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, tarball: https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, tarball: https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, tarball: https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, tarball: https://registry.npmjs.org/etag/-/etag-1.8.1.tgz} engines: {node: '>= 0.6'} @@ -1668,12 +1994,28 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==, tarball: https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz} engines: {'0': node >=0.6.0} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz} + engines: {node: '>=8.6.0'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==, tarball: https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==, tarball: https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==, tarball: https://registry.npmjs.org/figures/-/figures-3.2.0.tgz} engines: {node: '>=8'} @@ -1682,6 +2024,11 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==, tarball: https://registry.npmjs.org/figures/-/figures-6.1.0.tgz} engines: {node: '>=18'} + filing-cabinet@5.5.1: + resolution: {integrity: sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==, tarball: https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.5.1.tgz} + engines: {node: '>=18'} + hasBin: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, tarball: https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz} engines: {node: '>=8'} @@ -1690,6 +2037,15 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==, tarball: https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz} engines: {node: '>= 0.8'} + find-cypress-specs@1.54.12: + resolution: {integrity: sha512-eTJi4qctSClNV5ZNveCq07D2cKdyreRXO6fPBx2zfknV+03WzJmxoOruNK7Ok41FvJNzf33wQyVygbO0XCvasQ==, tarball: https://registry.npmjs.org/find-cypress-specs/-/find-cypress-specs-1.54.12.tgz} + engines: {node: '>=18'} + hasBin: true + + find-test-names@1.29.19: + resolution: {integrity: sha512-fSO2GXgOU6dH+FdffmRXYN/kLdnd8zkBGIZrKsmAdfLSFUUDLpDFF7+F/h+wjmjDWQmMgD8hPfJZR+igiEUQHQ==, tarball: https://registry.npmjs.org/find-test-names/-/find-test-names-1.29.19.tgz} + hasBin: true + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==, tarball: https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz} engines: {node: '>=18'} @@ -1711,6 +2067,14 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==, tarball: https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, tarball: https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz} + engines: {node: '>=14'} + forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==, tarball: https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz} @@ -1726,6 +2090,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, tarball: https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz} engines: {node: '>= 0.6'} + fp-ts@2.16.11: + resolution: {integrity: sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w==, tarball: https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.11.tgz} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==, tarball: https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz} engines: {node: '>= 0.6'} @@ -1752,6 +2119,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, tarball: https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, tarball: https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz} + gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==, tarball: https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz} engines: {node: '>=10'} @@ -1761,6 +2131,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz} engines: {node: '>=6.9.0'} + get-amd-module-type@6.0.2: + resolution: {integrity: sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==, tarball: https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.2.tgz} + engines: {node: '>=18'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, tarball: https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz} engines: {node: 6.* || 8.* || >= 10.*} @@ -1769,6 +2143,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, tarball: https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz} engines: {node: '>= 0.4'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==, tarball: https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, tarball: https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz} engines: {node: '>= 0.4'} @@ -1791,6 +2168,15 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz} engines: {node: '>= 6'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==, tarball: https://registry.npmjs.org/glob/-/glob-10.5.0.tgz} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==, tarball: https://registry.npmjs.org/glob/-/glob-13.0.6.tgz} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, tarball: https://registry.npmjs.org/glob/-/glob-7.2.3.tgz} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -1808,6 +2194,11 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==, tarball: https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz} engines: {node: '>=10'} + gonzales-pe@4.3.0: + resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==, tarball: https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz} + engines: {node: '>=0.6.0'} + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, tarball: https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz} engines: {node: '>= 0.4'} @@ -1819,10 +2210,17 @@ packages: resolution: {integrity: sha512-vAyM+6+jAYwSwz0/M0jYKfU9AvAMCz0kH791RsUhvMKGUHXled/3FjcQB3YiQ4Astj5srHdb6B2FHGIfZkOQNg==, tarball: https://registry.npmjs.org/has-ansi/-/has-ansi-6.0.2.tgz} engines: {node: '>=18'} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==, tarball: https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, tarball: https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, tarball: https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, tarball: https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz} engines: {node: '>= 0.4'} @@ -1887,6 +2285,10 @@ packages: immutable@5.1.9: resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==, tarball: https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, tarball: https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz} + engines: {node: '>=6'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==, tarball: https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz} engines: {node: '>=8'} @@ -1914,14 +2316,54 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==, tarball: https://registry.npmjs.org/ini/-/ini-4.1.1.tgz} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==, tarball: https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz} + engines: {node: '>= 0.4'} + + io-ts@2.2.22: + resolution: {integrity: sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA==, tarball: https://registry.npmjs.org/io-ts/-/io-ts-2.2.22.tgz} + peerDependencies: + fp-ts: ^2.5.0 + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, tarball: https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz} engines: {node: '>= 0.10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==, tarball: https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==, tarball: https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, tarball: https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==, tarball: https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, tarball: https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==, tarball: https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, tarball: https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==, tarball: https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} engines: {node: '>=0.10.0'} @@ -1942,10 +2384,22 @@ packages: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==, tarball: https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz} engines: {node: '>=18'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, tarball: https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==, tarball: https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, tarball: https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==, tarball: https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz} + engines: {node: '>=0.10.0'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, tarball: https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz} engines: {node: '>=8'} @@ -1958,10 +2412,34 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==, tarball: https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz} engines: {node: '>=8'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, tarball: https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==, tarball: https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==, tarball: https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==, tarball: https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, tarball: https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz} engines: {node: '>=8'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==, tarball: https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==, tarball: https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz} + engines: {node: '>= 0.4'} + is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==, tarball: https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz} @@ -1973,12 +2451,30 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==, tarball: https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz} engines: {node: '>=18'} + is-url-superb@4.0.0: + resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==, tarball: https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz} + engines: {node: '>=10'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==, tarball: https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==, tarball: https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, tarball: https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==, tarball: https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, tarball: https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz} + joi@18.2.3: resolution: {integrity: sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==, tarball: https://registry.npmjs.org/joi/-/joi-18.2.3.tgz} engines: {node: '>= 20'} @@ -1998,6 +2494,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, tarball: https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} @@ -2033,6 +2532,13 @@ packages: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==, tarball: https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz} engines: {node: '> 0.8'} + lazy-ass@2.0.3: + resolution: {integrity: sha512-/O3/DoQmI1XAhklDvF1dAjFf/epE8u3lzOZegQfLZ8G7Ud5bTRSZiFOpukHCu6jODrCA4gtIdwUCC7htxcDACA==, tarball: https://registry.npmjs.org/lazy-ass/-/lazy-ass-2.0.3.tgz} + engines: {node: '> 0.8'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, tarball: https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz} + listr2@3.14.0: resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==, tarball: https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz} engines: {node: '>=10.0.0'} @@ -2094,6 +2600,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz} + lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz} engines: {node: 20 || >=22} @@ -2110,6 +2619,9 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==, tarball: https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz} engines: {node: '>=12'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==, tarball: https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz} engines: {node: '>=8'} @@ -2131,10 +2643,18 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, tarball: https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, tarball: https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz} + engines: {node: '>= 8'} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==, tarball: https://registry.npmjs.org/methods/-/methods-1.1.2.tgz} engines: {node: '>= 0.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, tarball: https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz} engines: {node: '>= 0.6'} @@ -2162,6 +2682,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, tarball: https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz} engines: {node: '>=6'} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz} @@ -2169,6 +2693,10 @@ packages: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz} engines: {node: '>=10'} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} @@ -2180,6 +2708,10 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==, tarball: https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz} engines: {node: '>=8'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==, tarball: https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz} engines: {node: '>= 8'} @@ -2194,9 +2726,27 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + mocha@11.8.0: + resolution: {integrity: sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==, tarball: https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + module-definition@6.0.2: + resolution: {integrity: sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==, tarball: https://registry.npmjs.org/module-definition/-/module-definition-6.0.2.tgz} + engines: {node: '>=18'} + hasBin: true + + module-lookup-amd@9.1.3: + resolution: {integrity: sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==, tarball: https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.1.3.tgz} + engines: {node: '>=18'} + hasBin: true + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==, tarball: https://registry.npmjs.org/ms/-/ms-2.0.0.tgz} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.2.tgz} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} @@ -2225,6 +2775,10 @@ packages: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==, tarball: https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz} engines: {node: '>=18'} + node-source-walk@7.0.2: + resolution: {integrity: sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==, tarball: https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.2.tgz} + engines: {node: '>=18'} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==, tarball: https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz} engines: {node: '>=6'} @@ -2254,6 +2808,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, tarball: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==, tarball: https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, tarball: https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==, tarball: https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, tarball: https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz} engines: {node: '>= 0.8'} @@ -2280,10 +2846,21 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==, tarball: https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, tarball: https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz} + pad-right@0.2.2: resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==, tarball: https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz} engines: {node: '>=0.10.0'} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, tarball: https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, tarball: https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz} + engines: {node: '>=8'} + parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==, tarball: https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz} engines: {node: '>=18'} @@ -2304,6 +2881,17 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, tarball: https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz} @@ -2335,10 +2923,29 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==, tarball: https://registry.npmjs.org/pify/-/pify-2.3.0.tgz} engines: {node: '>=0.10.0'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==, tarball: https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz} + engines: {node: '>=4'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==, tarball: https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz} + engines: {node: '>= 0.4'} + + postcss-values-parser@6.0.2: + resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==, tarball: https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.2.9 + postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz} engines: {node: ^10 || ^12 || >=14} + precinct@12.3.2: + resolution: {integrity: sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==, tarball: https://registry.npmjs.org/precinct/-/precinct-12.3.2.tgz} + engines: {node: '>=18'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==, tarball: https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz} engines: {node: '>=6'} @@ -2382,6 +2989,12 @@ packages: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==, tarball: https://registry.npmjs.org/qs/-/qs-6.15.3.tgz} engines: {node: '>=0.6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz} + + quote-unquote@1.0.0: + resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==, tarball: https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==, tarball: https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz} @@ -2455,6 +3068,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz} + engines: {node: '>= 14.18.0'} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz} engines: {node: '>= 20.19.0'} @@ -2466,6 +3083,10 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==, tarball: https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz} hasBin: true + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==, tarball: https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz} + engines: {node: '>= 0.4'} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==, tarball: https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz} engines: {node: '>=0.10'} @@ -2473,14 +3094,44 @@ packages: request-progress@3.0.0: resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==, tarball: https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz} + require-and-forget@1.0.1: + resolution: {integrity: sha512-Sea861D/seGo3cptxc857a34Df0oEijXit8Q3IDodiwZMzVmyXrRI9EgQQa3hjkhoEjNzCBvv0t/0fMgebmWLg==, tarball: https://registry.npmjs.org/require-and-forget/-/require-and-forget-1.0.1.tgz} + engines: {node: '>=6'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, tarball: https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz} engines: {node: '>=0.10.0'} + requirejs-config-file@4.0.0: + resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==, tarball: https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz} + engines: {node: '>=10.13.0'} + + requirejs@2.3.8: + resolution: {integrity: sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==, tarball: https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz} + engines: {node: '>=0.4.0'} + hasBin: true + + resolve-dependency-path@4.0.1: + resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==, tarball: https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz} + engines: {node: '>=18'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, tarball: https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==, tarball: https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==, tarball: https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz} engines: {node: '>=8'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, tarball: https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, tarball: https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz} @@ -2494,15 +3145,27 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, tarball: https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==, tarball: https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, tarball: https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, tarball: https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, tarball: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz} + sass-lookup@6.1.2: + resolution: {integrity: sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==, tarball: https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.2.tgz} + engines: {node: '>=18'} + hasBin: true + sass@1.102.0: resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==, tarball: https://registry.npmjs.org/sass/-/sass-1.102.0.tgz} engines: {node: '>=20.19.0'} @@ -2514,6 +3177,9 @@ packages: seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==, tarball: https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz} + seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==, tarball: https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmjs.org/semver/-/semver-6.3.1.tgz} hasBin: true @@ -2540,6 +3206,14 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==, tarball: https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, tarball: https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==, tarball: https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, tarball: https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz} @@ -2551,6 +3225,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} engines: {node: '>=8'} + shelljs@0.10.0: + resolution: {integrity: sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==, tarball: https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz} + engines: {node: '>=18'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, tarball: https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz} engines: {node: '>= 0.4'} @@ -2570,6 +3248,14 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz} + engines: {node: '>=14'} + + simple-bin-help@1.8.0: + resolution: {integrity: sha512-0LxHn+P1lF5r2WwVB/za3hLRIsYoLaNq1CXqjbrs3ZvLuvlWnRKrUjEWzV7umZL7hpQ7xULiQMV+0iXdRa5iFg==, tarball: https://registry.npmjs.org/simple-bin-help/-/simple-bin-help-1.8.0.tgz} + engines: {node: '>=14.16'} + slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==, tarball: https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz} engines: {node: '>=8'} @@ -2601,9 +3287,16 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==, tarball: https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz} + spec-change@1.11.21: + resolution: {integrity: sha512-87FZBOBjTyXF/R8juve+lHp+Q+9UlcwCF4rEM995jPGrJBYAMl19saWc9oRvB36VJqLI66D1FXsXTmetFJtQ9g==, tarball: https://registry.npmjs.org/spec-change/-/spec-change-1.11.21.tgz} + hasBin: true + split@0.3.3: resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==, tarball: https://registry.npmjs.org/split/-/split-0.3.3.tgz} + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==, tarball: https://registry.npmjs.org/split/-/split-1.0.1.tgz} + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==, tarball: https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz} engines: {node: '>=0.10.0'} @@ -2621,6 +3314,10 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, tarball: https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==, tarball: https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz} + engines: {node: '>= 0.4'} + stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==, tarball: https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz} @@ -2632,13 +3329,29 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, tarball: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, tarball: https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, tarball: https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz} + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==, tarball: https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, tarball: https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz} + engines: {node: '>=4'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==, tarball: https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz} engines: {node: '>=6'} @@ -2647,6 +3360,11 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, tarball: https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz} engines: {node: '>=8'} + stylus-lookup@6.1.2: + resolution: {integrity: sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==, tarball: https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.2.tgz} + engines: {node: '>=18'} + hasBin: true + superagent@10.3.0: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==, tarball: https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz} engines: {node: '>=14.18.0'} @@ -2667,10 +3385,18 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, tarball: https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz} engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, tarball: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz} + engines: {node: '>= 0.4'} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==, tarball: https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz} engines: {node: '>=20'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==, tarball: https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz} + engines: {node: '>=6'} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==, tarball: https://registry.npmjs.org/tar/-/tar-6.2.1.tgz} engines: {node: '>=10'} @@ -2685,6 +3411,10 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==, tarball: https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz} + engines: {node: '>=12.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==, tarball: https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz} @@ -2718,6 +3448,16 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==, tarball: https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz} hasBin: true + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, tarball: https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==, tarball: https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz} + engines: {node: '>=6'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} @@ -2729,6 +3469,10 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==, tarball: https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz} + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==, tarball: https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==, tarball: https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz} @@ -2760,6 +3504,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz} + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==, tarball: https://registry.npmjs.org/undici/-/undici-6.28.0.tgz} + engines: {node: '>=18.17'} + unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==, tarball: https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz} engines: {node: '>=20'} @@ -2797,6 +3545,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, tarball: https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz} engines: {node: '>= 0.4.0'} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==, tarball: https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==, tarball: https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -2849,12 +3601,27 @@ packages: engines: {node: '>=20.0.0'} hasBin: true + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, tarball: https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==, tarball: https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==, tarball: https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==, tarball: https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} engines: {node: '>= 8'} @@ -2866,6 +3633,9 @@ packages: workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==, tarball: https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz} + workerpool@9.3.4: + resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==, tarball: https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz} engines: {node: '>=8'} @@ -2874,6 +3644,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, tarball: https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz} @@ -2900,6 +3674,10 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==, tarball: https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, tarball: https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz} + engines: {node: '>=12'} + yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==, tarball: https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz} engines: {node: '>=10'} @@ -2908,6 +3686,10 @@ packages: resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==, tarball: https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz} engines: {node: '>=10'} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==, tarball: https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz} + engines: {node: '>=12'} + yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==, tarball: https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz} @@ -2923,6 +3705,22 @@ packages: snapshots: + '@actions/core@2.0.3': + dependencies: + '@actions/exec': 2.0.0 + '@actions/http-client': 3.0.2 + + '@actions/exec@2.0.0': + dependencies: + '@actions/io': 2.0.0 + + '@actions/http-client@3.0.2': + dependencies: + tunnel: 0.0.6 + undici: 6.28.0 + + '@actions/io@2.0.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -3002,6 +3800,11 @@ snapshots: dependencies: '@babel/types': 7.29.8 + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -3037,6 +3840,45 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@badeball/cypress-cucumber-preprocessor@26.0.0(@babel/core@7.29.7)(cypress@13.17.0)(typescript@5.9.3)': + dependencies: + '@cucumber/ci-environment': 14.0.0 + '@cucumber/cucumber': 13.2.1 + '@cucumber/cucumber-expressions': 20.0.0 + '@cucumber/gherkin': 41.0.0 + '@cucumber/html-formatter': 24.0.0(@cucumber/messages@33.0.4) + '@cucumber/message-streams': 5.0.1(@cucumber/messages@33.0.4) + '@cucumber/messages': 33.0.4 + '@cucumber/pretty-formatter': 4.0.0(@cucumber/messages@33.0.4) + '@cucumber/tag-expressions': 10.0.0 + '@jridgewell/trace-mapping': 0.3.31 + base64-js: 1.5.1 + cli-table: 0.3.11 + common-ancestor-path: 2.0.0 + cosmiconfig: 9.0.2(typescript@5.9.3) + cypress: 13.17.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + find-cypress-specs: 1.54.12(@babel/core@7.29.7) + fp-ts: 2.16.11 + glob: 13.0.6 + io-ts: 2.2.22(fp-ts@2.16.11) + mocha: 11.8.0 + seedrandom: 3.0.5 + split: 1.0.1 + uuid: 14.0.2 + transitivePeerDependencies: + - '@babel/core' + - supports-color + - typescript + + '@bahmutov/cypress-esbuild-preprocessor@2.2.8(esbuild@0.28.2)': + dependencies: + debug: 4.4.3 + esbuild: 0.28.2 + transitivePeerDependencies: + - supports-color + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -3140,6 +3982,10 @@ snapshots: dependencies: '@cucumber/messages': 34.2.0 + '@cucumber/html-formatter@24.0.0(@cucumber/messages@33.0.4)': + dependencies: + '@cucumber/messages': 33.0.4 + '@cucumber/html-formatter@24.1.0(@cucumber/messages@34.2.0)': dependencies: '@cucumber/messages': 34.2.0 @@ -3152,6 +3998,11 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 + '@cucumber/message-streams@5.0.1(@cucumber/messages@33.0.4)': + dependencies: + '@cucumber/messages': 33.0.4 + mime: 4.1.0 + '@cucumber/message-streams@5.0.1(@cucumber/messages@34.2.0)': dependencies: '@cucumber/messages': 34.2.0 @@ -3161,12 +4012,24 @@ snapshots: '@cucumber/messages@34.2.0': {} + '@cucumber/pretty-formatter@4.0.0(@cucumber/messages@33.0.4)': + dependencies: + '@cucumber/messages': 33.0.4 + '@cucumber/query': 16.0.0(@cucumber/messages@33.0.4) + luxon: 3.7.2 + '@cucumber/pretty-formatter@4.0.0(@cucumber/messages@34.2.0)': dependencies: '@cucumber/messages': 34.2.0 '@cucumber/query': 16.0.0(@cucumber/messages@34.2.0) luxon: 3.7.2 + '@cucumber/query@16.0.0(@cucumber/messages@33.0.4)': + dependencies: + '@cucumber/messages': 33.0.4 + '@teppeis/multimaps': 3.0.0 + lodash.sortby: 4.7.0 + '@cucumber/query@16.0.0(@cucumber/messages@34.2.0)': dependencies: '@cucumber/messages': 34.2.0 @@ -3179,6 +4042,8 @@ snapshots: '@teppeis/multimaps': 3.0.0 lodash.sortby: 4.7.0 + '@cucumber/tag-expressions@10.0.0': {} + '@cucumber/tag-expressions@11.0.0': {} '@cypress/request@3.0.10': @@ -3209,6 +4074,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@dependents/detective-less@5.0.3': + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + '@discoveryjs/json-ext@1.1.0': {} + '@epic-web/invariant@1.0.0': {} '@esbuild/aix-ppc64@0.21.5': @@ -3376,6 +4248,15 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3415,6 +4296,18 @@ snapshots: '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -3478,6 +4371,9 @@ snapshots: '@phc/format@1.0.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 @@ -3712,6 +4608,41 @@ snapshots: '@types/node': 22.20.1 optional: true + '@typescript-eslint/project-service@8.67.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/types@8.67.0': {} + + '@typescript-eslint/typescript-estree@8.67.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.67.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3) + '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/visitor-keys': 8.67.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.67.0': + dependencies: + '@typescript-eslint/types': 8.67.0 + eslint-visitor-keys: 5.0.1 + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.1)(sass@1.102.0))': dependencies: '@babel/core': 7.29.7 @@ -3724,6 +4655,38 @@ snapshots: transitivePeerDependencies: - supports-color + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.26 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + abbrev@1.1.1: {} accepts@1.3.8: @@ -3731,6 +4694,12 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-walk@8.3.5: + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -3756,11 +4725,15 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.2 + app-module-path@2.2.0: {} + aproba@2.1.0: {} arch@2.2.0: {} @@ -3783,6 +4756,11 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} asap@2.0.6: {} @@ -3801,6 +4779,8 @@ snapshots: assertion-error@2.0.1: {} + ast-module-types@6.0.2: {} + astral-regex@2.0.0: {} async@3.2.6: {} @@ -3809,6 +4789,10 @@ snapshots: at-least-node@1.0.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + aws-sign2@0.7.0: {} aws4@1.13.2: {} @@ -3825,6 +4809,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.11.14: {} @@ -3865,6 +4851,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3899,11 +4889,20 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + camelcase@6.3.0: {} caniuse-lite@1.0.30001809: {} @@ -3939,6 +4938,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -3959,6 +4962,10 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -3970,6 +4977,15 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: + optional: true + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3980,14 +4996,20 @@ snapshots: colorette@2.0.20: {} + colors@1.0.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@12.1.0: {} + commander@15.0.0: {} commander@6.2.1: {} + common-ancestor-path@2.0.0: {} + common-tags@1.8.2: {} component-emitter@1.3.1: {} @@ -3996,6 +5018,10 @@ snapshots: console-control-strings@1.1.0: {} + console.table@0.10.0: + dependencies: + easy-table: 1.1.0 + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -4028,6 +5054,15 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@9.0.2(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + cross-env@10.1.0: dependencies: '@epic-web/invariant': 1.0.0 @@ -4103,6 +5138,10 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -4123,16 +5162,120 @@ snapshots: deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + optional: true + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delayed-stream@1.0.0: {} delegates@1.0.0: {} depd@2.0.0: {} + dependency-tree@11.5.0: + dependencies: + '@discoveryjs/json-ext': 1.1.0 + commander: 12.1.0 + filing-cabinet: 5.5.1 + precinct: 12.3.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + destroy@1.2.0: {} detect-libc@2.1.2: {} + detective-amd@6.1.0: + dependencies: + ast-module-types: 6.0.2 + escodegen: 2.1.0 + get-amd-module-type: 6.0.2 + node-source-walk: 7.0.2 + + detective-cjs@6.1.1: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + detective-es6@5.0.2: + dependencies: + node-source-walk: 7.0.2 + + detective-postcss@8.0.4(postcss@8.5.26): + dependencies: + is-url-superb: 4.0.0 + postcss: 8.5.26 + postcss-values-parser: 6.0.2(postcss@8.5.26) + + detective-sass@6.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-scss@5.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-stylus@5.0.1: {} + + detective-typescript@14.1.2(typescript@5.9.3): + dependencies: + '@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3) + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + detective-vue2@2.3.0(typescript@5.9.3): + dependencies: + '@dependents/detective-less': 5.0.3 + '@vue/compiler-sfc': 3.5.41 + detective-es6: 5.0.2 + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -4142,6 +5285,8 @@ snapshots: diff@5.2.2: {} + diff@7.0.0: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -4152,6 +5297,12 @@ snapshots: duplexer@0.1.2: {} + eastasianwidth@0.2.0: {} + + easy-table@1.1.0: + optionalDependencies: + wcwidth: 1.0.1 + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -4167,17 +5318,32 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@7.0.1: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -4186,6 +5352,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -4260,6 +5438,24 @@ snapshots: escape-string-regexp@4.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-visitor-keys@5.0.1: {} + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + etag@1.8.1: {} event-stream@3.3.4: @@ -4352,12 +5548,28 @@ snapshots: extsprintf@1.3.0: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-safe-stringify@2.1.1: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -4366,6 +5578,20 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + filing-cabinet@5.5.1: + dependencies: + app-module-path: 2.2.0 + commander: 12.1.0 + enhanced-resolve: 5.24.5 + module-definition: 6.0.2 + module-lookup-amd: 9.1.3 + resolve: 1.22.12 + resolve-dependency-path: 4.0.1 + sass-lookup: 6.1.2 + stylus-lookup: 6.1.2 + tsconfig-paths: 4.2.0 + typescript: 5.9.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4382,6 +5608,36 @@ snapshots: transitivePeerDependencies: - supports-color + find-cypress-specs@1.54.12(@babel/core@7.29.7): + dependencies: + '@actions/core': 2.0.3 + arg: 5.0.2 + console.table: 0.10.0 + debug: 4.4.3 + find-test-names: 1.29.19(@babel/core@7.29.7) + minimatch: 10.2.6 + pluralize: 8.0.0 + require-and-forget: 1.0.1 + shelljs: 0.10.0 + spec-change: 1.11.21 + tinyglobby: 0.2.17 + tsx: 4.23.12 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + find-test-names@1.29.19(@babel/core@7.29.7): + dependencies: + '@babel/parser': 7.29.8 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + acorn-walk: 8.3.5 + debug: 4.4.3 + simple-bin-help: 1.8.0 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - '@babel/core' + - supports-color + find-up-simple@1.0.1: {} find-up@5.0.0: @@ -4395,6 +5651,15 @@ snapshots: optionalDependencies: debug: 4.4.3 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + forever-agent@0.6.1: {} form-data@4.0.6: @@ -4413,6 +5678,8 @@ snapshots: forwarded@0.2.0: {} + fp-ts@2.16.11: {} + fresh@0.5.2: {} from@0.1.7: {} @@ -4435,6 +5702,8 @@ snapshots: function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gauge@3.0.2: dependencies: aproba: 2.1.0 @@ -4449,6 +5718,11 @@ snapshots: gensync@1.0.0-beta.2: {} + get-amd-module-type@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -4464,6 +5738,8 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 + get-own-enumerable-property-symbols@3.0.2: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -4487,6 +5763,21 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -4512,6 +5803,10 @@ snapshots: dependencies: ini: 2.0.0 + gonzales-pe@4.3.0: + dependencies: + minimist: 1.2.8 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4520,8 +5815,14 @@ snapshots: dependencies: ansi-regex: 6.3.0 + has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -4579,6 +5880,11 @@ snapshots: immutable@5.1.9: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + indent-string@4.0.0: {} indent-string@5.0.0: {} @@ -4596,12 +5902,55 @@ snapshots: ini@4.1.1: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + io-ts@2.2.22(fp-ts@2.16.11): + dependencies: + fp-ts: 2.16.11 + ipaddr.js@1.9.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4620,26 +5969,78 @@ snapshots: global-directory: 4.0.1 is-path-inside: 4.0.0 + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-obj@1.0.1: {} + is-path-inside@3.0.3: {} is-path-inside@4.0.0: {} is-plain-obj@2.1.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-regexp@1.0.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-typedarray@1.0.0: {} is-unicode-supported@0.1.0: {} is-unicode-supported@2.1.0: {} + is-url-superb@4.0.0: {} + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} isstream@0.1.2: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + joi@18.2.3: dependencies: '@hapi/address': 5.1.1 @@ -4660,6 +6061,8 @@ snapshots: jsesc@3.1.0: {} + json-parse-even-better-errors@2.3.1: {} + json-schema@0.4.0: {} json-stringify-safe@5.0.1: {} @@ -4709,6 +6112,10 @@ snapshots: lazy-ass@1.6.0: {} + lazy-ass@2.0.3: {} + + lines-and-columns@1.2.4: {} + listr2@3.14.0(enquirer@2.4.1): dependencies: cli-truncate: 2.1.0 @@ -4766,6 +6173,8 @@ snapshots: loupe@3.2.1: {} + lru-cache@10.4.3: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -4778,6 +6187,10 @@ snapshots: luxon@3.7.2: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -4792,8 +6205,15 @@ snapshots: merge-stream@2.0.0: {} + merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -4808,6 +6228,10 @@ snapshots: mimic-fn@2.1.0: {} + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.18 @@ -4816,6 +6240,10 @@ snapshots: dependencies: brace-expansion: 2.1.4 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + minimist@1.2.8: {} minipass@3.3.6: @@ -4824,6 +6252,8 @@ snapshots: minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -4854,8 +6284,45 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mocha@11.8.0: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.5.0 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.3.1 + log-symbols: 4.1.0 + minimatch: 9.0.9 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.3 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + module-definition@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + module-lookup-amd@9.1.3: + dependencies: + commander: 12.1.0 + requirejs: 2.3.8 + requirejs-config-file: 4.0.0 + ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} nanoid@3.3.18: {} @@ -4870,6 +6337,10 @@ snapshots: node-releases@2.0.53: {} + node-source-walk@7.0.2: + dependencies: + '@babel/parser': 7.29.8 + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -4897,6 +6368,22 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4923,10 +6410,23 @@ snapshots: dependencies: aggregate-error: 3.1.0 + package-json-from-dist@1.0.1: {} + pad-right@0.2.2: dependencies: repeat-string: 1.6.1 + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse-json@8.3.0: dependencies: '@babel/code-frame': 7.29.7 @@ -4941,6 +6441,18 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-to-regexp@0.1.13: {} pathval@2.0.1: {} @@ -4957,17 +6469,47 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.5: - optional: true + picomatch@4.0.5: {} pify@2.3.0: {} + pluralize@8.0.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss-values-parser@6.0.2(postcss@8.5.26): + dependencies: + color-name: 1.1.4 + is-url-superb: 4.0.0 + postcss: 8.5.26 + quote-unquote: 1.0.0 + postcss@8.5.26: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 + precinct@12.3.2: + dependencies: + '@dependents/detective-less': 5.0.3 + commander: 12.1.0 + detective-amd: 6.1.0 + detective-cjs: 6.1.1 + detective-es6: 5.0.2 + detective-postcss: 8.0.4(postcss@8.5.26) + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + detective-vue2: 2.3.0(typescript@5.9.3) + module-definition: 6.0.2 + node-source-walk: 7.0.2 + postcss: 8.5.26 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + pretty-bytes@5.6.0: {} prisma@5.22.0: @@ -5007,6 +6549,10 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 + queue-microtask@1.2.3: {} + + quote-unquote@1.0.0: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -5081,6 +6627,8 @@ snapshots: dependencies: picomatch: 2.3.2 + readdirp@4.1.2: {} + readdirp@5.1.1: {} regexp-match-indices@1.0.2: @@ -5089,19 +6637,54 @@ snapshots: regexp-tree@0.1.27: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + repeat-string@1.6.1: {} request-progress@3.0.0: dependencies: throttleit: 1.0.1 + require-and-forget@1.0.1: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + require-directory@2.1.1: {} + requirejs-config-file@4.0.0: + dependencies: + esprima: 4.0.1 + stringify-object: 3.3.0 + + requirejs@2.3.8: {} + + resolve-dependency-path@4.0.1: {} + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 + reusify@1.1.0: {} + rfdc@1.4.1: {} rimraf@3.0.2: @@ -5140,14 +6723,29 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.4 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + rxjs@7.8.2: dependencies: tslib: 2.8.1 safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sass-lookup@6.1.2: + dependencies: + commander: 12.1.0 + enhanced-resolve: 5.24.5 + sass@1.102.0: dependencies: chokidar: 5.0.0 @@ -5162,6 +6760,8 @@ snapshots: seed-random@2.2.0: {} + seedrandom@3.0.5: {} + semver@6.3.1: {} semver@7.8.5: {} @@ -5201,6 +6801,22 @@ snapshots: set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -5209,6 +6825,11 @@ snapshots: shebang-regex@3.0.0: {} + shelljs@0.10.0: + dependencies: + execa: 5.1.1 + fast-glob: 3.3.3 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -5239,6 +6860,10 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + + simple-bin-help@1.8.0: {} + slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -5274,10 +6899,25 @@ snapshots: spdx-license-ids@3.0.23: {} + spec-change@1.11.21: + dependencies: + arg: 5.0.2 + debug: 4.4.3 + deep-equal: 2.2.3 + dependency-tree: 11.5.0 + lazy-ass: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + split@0.3.3: dependencies: through: 2.3.8 + split@1.0.1: + dependencies: + through: 2.3.8 + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -5307,6 +6947,11 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stream-combiner@0.0.4: dependencies: duplexer: 0.1.2 @@ -5319,18 +6964,40 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-bom@3.0.0: {} + strip-final-newline@2.0.0: {} strip-json-comments@3.1.1: {} + stylus-lookup@6.1.2: + dependencies: + commander: 12.1.0 + superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -5363,8 +7030,12 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + tagged-tag@1.0.0: {} + tapable@2.3.3: {} + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -5380,6 +7051,11 @@ snapshots: tiny-case@1.0.3: {} + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tldts-core@6.1.86: {} tldts@6.1.86: @@ -5404,6 +7080,16 @@ snapshots: tree-kill@1.2.2: {} + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} tsx@4.23.12: @@ -5416,6 +7102,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tunnel@0.0.6: {} + tweetnacl@0.14.5: {} type-fest@0.21.3: {} @@ -5437,6 +7125,8 @@ snapshots: undici-types@6.21.0: {} + undici@6.28.0: {} + unicorn-magic@0.4.0: {} universalify@2.0.1: {} @@ -5461,6 +7151,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@14.0.2: {} + uuid@8.3.2: {} validate-npm-package-license@3.0.4: @@ -5497,6 +7189,11 @@ snapshots: - debug - supports-color + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + optional: true + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -5504,6 +7201,31 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5514,6 +7236,8 @@ snapshots: workerpool@6.5.1: {} + workerpool@9.3.4: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -5526,6 +7250,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} xmlbuilder@15.1.1: {} @@ -5540,6 +7270,8 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs-unparser@2.0.0: dependencies: camelcase: 6.3.0 @@ -5557,6 +7289,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13