diff --git a/apps/api/.gitignore b/apps/api/.gitignore index 149049a..4b56acf 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -54,6 +54,3 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -/docker-imported-files-data -/docker-volumes diff --git a/apps/webapp/.gitignore b/apps/webapp/.gitignore index 5018944..391bfac 100644 --- a/apps/webapp/.gitignore +++ b/apps/webapp/.gitignore @@ -24,4 +24,10 @@ dist-ssr *.sw? public/dsfr -vite.config.ts.* \ No newline at end of file +vite.config.ts.* + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 959583e..21201cd 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -14,7 +14,8 @@ "test": "DEBUG_PRINT_LIMIT=10000 vitest", "test:all": "vitest run", "postinstall": "pnpm dsfr:build", - "dsfr:build": "react-dsfr update-icons" + "dsfr:build": "react-dsfr update-icons", + "test:ct": "playwright test -c playwright-ct.config.ts" }, "dependencies": { "@codegouvfr/react-dsfr": "^1.15.0", @@ -22,6 +23,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "file-type": "^19.6.0", + "localforage": "^1.10.0", "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -31,11 +33,13 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@playwright/experimental-ct-react": "^1.49.1", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/blob-stream": "^0.1.33", "@types/lodash": "^4.17.13", + "@types/node": "^20.3.1", "@types/pdfkit": "^0.13.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/apps/webapp/playwright-ct.config.ts b/apps/webapp/playwright-ct.config.ts new file mode 100644 index 0000000..56ac357 --- /dev/null +++ b/apps/webapp/playwright-ct.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from "@playwright/experimental-ct-react"; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./", + // Glob patterns or regular expressions that match test files. + testMatch: "*src/**/*.ct-spec.tsx", + /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ + snapshotDir: "./__snapshots__", + /* Maximum time one test can run for. */ + timeout: 10 * 1000, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? "html" : "list", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + + /* Port to use for Playwright component endpoint. */ + ctPort: 3100, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], +}); diff --git a/apps/webapp/playwright/index.html b/apps/webapp/playwright/index.html new file mode 100644 index 0000000..2032be5 --- /dev/null +++ b/apps/webapp/playwright/index.html @@ -0,0 +1,12 @@ + + + + + + Testing Page + + +
+ + + diff --git a/apps/webapp/playwright/index.tsx b/apps/webapp/playwright/index.tsx new file mode 100644 index 0000000..ac6de14 --- /dev/null +++ b/apps/webapp/playwright/index.tsx @@ -0,0 +1,2 @@ +// Import styles, initialize component theme here. +// import '../src/common.css'; diff --git a/apps/webapp/src/authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts b/apps/webapp/src/authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts index 4f7f800..f97f429 100644 --- a/apps/webapp/src/authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts +++ b/apps/webapp/src/authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts @@ -4,18 +4,17 @@ import { AuthenticationStorageProvider } from "../../../core-logic/providers/aut export class AuthenticationSessionStorageProvider implements AuthenticationStorageProvider { - storeAuthentication(payload: AuthenticatedUserSM): void { + async storeAuthentication(payload: AuthenticatedUserSM) { sessionStorage.setItem("authenticated", "true"); if (payload) sessionStorage.setItem("user", JSON.stringify(payload)); } - storeDisconnection(): void { + async storeDisconnection() { sessionStorage.setItem("authenticated", "false"); } - isAuthenticated() { + async isAuthenticated() { return sessionStorage.getItem("authenticated") === "true"; } - - getUser(): AuthenticatedUserSM { + async getUser() { return JSON.parse(sessionStorage.getItem("user") || "null"); } } diff --git a/apps/webapp/src/authentication/adapters/secondary/providers/fakeAuthenticationStorage.provider.ts b/apps/webapp/src/authentication/adapters/secondary/providers/fakeAuthenticationStorage.provider.ts index 6e6b033..5addeb7 100644 --- a/apps/webapp/src/authentication/adapters/secondary/providers/fakeAuthenticationStorage.provider.ts +++ b/apps/webapp/src/authentication/adapters/secondary/providers/fakeAuthenticationStorage.provider.ts @@ -7,19 +7,18 @@ export class FakeAuthenticationStorageProvider _isAuthenticated: boolean = false; _user: AuthenticatedUserSM | null = null; - storeAuthentication(payload: AuthenticatedUserSM) { + async storeAuthentication(payload: AuthenticatedUserSM) { this._isAuthenticated = true; this._user = payload; } - storeDisconnection() { + async storeDisconnection() { this._isAuthenticated = false; this._user = null; } - isAuthenticated() { + async isAuthenticated() { return this._isAuthenticated; } - - getUser() { + async getUser() { return this._user; } } diff --git a/apps/webapp/src/authentication/adapters/secondary/providers/indexedDbAuthenticationStorage.provider.ts b/apps/webapp/src/authentication/adapters/secondary/providers/indexedDbAuthenticationStorage.provider.ts new file mode 100644 index 0000000..5697bae --- /dev/null +++ b/apps/webapp/src/authentication/adapters/secondary/providers/indexedDbAuthenticationStorage.provider.ts @@ -0,0 +1,103 @@ +import { AuthenticatedUserSM } from "../../../core-logic/gateways/Authentication.gateway"; +import { AuthenticationStorageProvider } from "../../../core-logic/providers/authenticationStorage.provider"; + +export class IndexedDbAuthenticationStorageProvider + implements AuthenticationStorageProvider +{ + private readonly dbName = "fondation"; + private readonly storeName = "authentication"; + private db: IDBDatabase | null = null; + + constructor() { + if (!IndexedDbAuthenticationStorageProvider.browserSupportsIndexedDB()) { + console.warn("Your browser doesn't support IndexedDB."); + return; + } + + this.initDB(); + } + + private async initDB() { + const request = indexedDB.open(this.dbName, 1); + this.db = await this.requestDbToPromise(request); + } + + async storeAuthentication(payload: AuthenticatedUserSM): Promise { + if (!this.db) return; + + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + const requestAuthenticated = store.put({ + key: "authenticated", + value: "true", + }); + const requestUser = store.put({ + key: "user", + value: JSON.stringify(payload), + }); + + await this.requestToPromise(requestAuthenticated); + await this.requestToPromise(requestUser); + } + + async storeDisconnection() { + if (!this.db) return; + + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + store.put({ key: "authenticated", value: "false" }); + const request = store.delete("user"); + + await this.requestToPromise(request); + } + + async isAuthenticated() { + if (!this.db) return false; + + const transaction = this.db.transaction(this.storeName, "readonly"); + const store = transaction.objectStore(this.storeName); + const request = store.get("authenticated"); + const value = await this.requesToPromiseValue(request); + + return value === "true"; + } + + async getUser() { + if (!this.db) { + return null; + } + + const transaction = this.db.transaction(this.storeName, "readonly"); + const store = transaction.objectStore(this.storeName); + const request = store.get("user"); + const value = await this.requesToPromiseValue(request); + + return value ? JSON.parse(value) : null; + } + + private requestDbToPromise( + request: IDBRequest, + ): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = (event) => + resolve((event.target as IDBOpenDBRequest).result); + request.onerror = () => reject(request.error); + }); + } + + private async requesToPromiseValue(request: IDBRequest) { + const result = await this.requestToPromise(request); + return result?.value; + } + + private requestToPromise(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + static browserSupportsIndexedDB() { + return !!indexedDB; + } +} diff --git a/apps/webapp/src/authentication/core-logic/listeners/authentication.listeners.ts b/apps/webapp/src/authentication/core-logic/listeners/authentication.listeners.ts index d946772..5614232 100644 --- a/apps/webapp/src/authentication/core-logic/listeners/authentication.listeners.ts +++ b/apps/webapp/src/authentication/core-logic/listeners/authentication.listeners.ts @@ -1,4 +1,5 @@ import { Listener } from "../../../store/listeners"; +import { authenticationStateInitFromStore } from "../reducers/authentication.slice"; import { authenticate } from "../use-cases/authentication/authenticate"; export const storeAuthenticationOnLoginSuccess: Listener = ( @@ -6,7 +7,7 @@ export const storeAuthenticationOnLoginSuccess: Listener = ( ) => startAppListening({ actionCreator: authenticate.fulfilled, - effect: ( + effect: async ( action, { extra: { @@ -14,6 +15,34 @@ export const storeAuthenticationOnLoginSuccess: Listener = ( }, }, ) => { - authenticationStorageProvider?.storeAuthentication(action.payload); + await authenticationStorageProvider?.storeAuthentication(action.payload); + }, + }); + +export const initializeAuthenticationState: Listener = (startAppListening) => + startAppListening({ + predicate: (_, state) => + state.authentication.initializedFromStore === false, + effect: async ( + _, + { + dispatch, + extra: { + providers: { authenticationStorageProvider }, + }, + }, + ) => { + // There is no retry mechanism here, but it could be useful + // because the IndexedDB instance access could take some time. + const authenticated = + await authenticationStorageProvider?.isAuthenticated(); + const user = await authenticationStorageProvider?.getUser(); + + dispatch( + authenticationStateInitFromStore({ + authenticated: !!authenticated, + user: user || null, + }), + ); }, }); diff --git a/apps/webapp/src/authentication/core-logic/listeners/logout.listeners.ts b/apps/webapp/src/authentication/core-logic/listeners/logout.listeners.ts index f4f1e90..42ae536 100644 --- a/apps/webapp/src/authentication/core-logic/listeners/logout.listeners.ts +++ b/apps/webapp/src/authentication/core-logic/listeners/logout.listeners.ts @@ -4,7 +4,7 @@ import { logout } from "../use-cases/logout/logout"; export const storeDisconnectionOnLogout: Listener = (startAppListening) => startAppListening({ actionCreator: logout.fulfilled, - effect: ( + effect: async ( _, { extra: { @@ -12,6 +12,6 @@ export const storeDisconnectionOnLogout: Listener = (startAppListening) => }, }, ) => { - authenticationStorageProvider?.storeDisconnection(); + await authenticationStorageProvider?.storeDisconnection(); }, }); diff --git a/apps/webapp/src/authentication/core-logic/providers/authenticationStorage.provider.ts b/apps/webapp/src/authentication/core-logic/providers/authenticationStorage.provider.ts index 10b5275..2809ab1 100644 --- a/apps/webapp/src/authentication/core-logic/providers/authenticationStorage.provider.ts +++ b/apps/webapp/src/authentication/core-logic/providers/authenticationStorage.provider.ts @@ -1,8 +1,8 @@ import { AuthenticatedUserSM } from "../gateways/Authentication.gateway"; export interface AuthenticationStorageProvider { - storeAuthentication(payload: AuthenticatedUserSM): void; - storeDisconnection(): void; - isAuthenticated: () => boolean; - getUser: () => AuthenticatedUserSM | null; + storeAuthentication(payload: AuthenticatedUserSM): Promise; + storeDisconnection(): Promise; + isAuthenticated: () => Promise; + getUser: () => Promise; } diff --git a/apps/webapp/src/authentication/core-logic/reducers/authentication.slice.ts b/apps/webapp/src/authentication/core-logic/reducers/authentication.slice.ts index 7027788..8aeca12 100644 --- a/apps/webapp/src/authentication/core-logic/reducers/authentication.slice.ts +++ b/apps/webapp/src/authentication/core-logic/reducers/authentication.slice.ts @@ -1,22 +1,24 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createAction, createSlice } from "@reduxjs/toolkit"; import { AppState } from "../../../store/appState"; -import { PartialAppDependencies } from "../../../store/reduxStore"; import { authenticate } from "../use-cases/authentication/authenticate"; import { logout } from "../use-cases/logout/logout"; -export const createAuthenticationSlice = ({ - authenticationStorageProvider, -}: Pick< - PartialAppDependencies["providers"], - "authenticationStorageProvider" ->) => +const initialState: AppState["authentication"] = { + authenticated: false, + user: null, + initializedFromStore: false, +}; + +export const createAuthenticationSlice = () => createSlice({ name: "authentication", - initialState: (): AppState["authentication"] => ({ - authenticated: !!authenticationStorageProvider?.isAuthenticated(), - user: authenticationStorageProvider?.getUser() || null, - }), - reducers: {}, + initialState, + reducers: { + setAuthentication: (state, action) => { + state.authenticated = action.payload.authenticated; + state.user = action.payload.user; + }, + }, extraReducers: (builder) => { builder.addCase(authenticate.fulfilled, (state, action) => { state.authenticated = true; @@ -26,5 +28,14 @@ export const createAuthenticationSlice = ({ state.authenticated = false; state.user = null; }); + builder.addCase(authenticationStateInitFromStore, (state, action) => { + state.authenticated = action.payload.authenticated; + state.user = action.payload.user; + state.initializedFromStore = true; + }); }, }); + +export const authenticationStateInitFromStore = createAction< + Omit +>("AUTHENTICATION_STATE_INIT_FROM_STORE"); diff --git a/apps/webapp/src/authentication/core-logic/use-cases/authentication/authenticate.spec.ts b/apps/webapp/src/authentication/core-logic/use-cases/authentication/authenticate.spec.ts index 1ba88ef..f137d2a 100644 --- a/apps/webapp/src/authentication/core-logic/use-cases/authentication/authenticate.spec.ts +++ b/apps/webapp/src/authentication/core-logic/use-cases/authentication/authenticate.spec.ts @@ -51,8 +51,7 @@ describe("Authenticate", () => { it("persists the authenticated state", async () => { store.dispatch(authenticate.fulfilled(user, "", userCredentials)); - - expect(authenticationStorageProvider.isAuthenticated()).toBe(true); + expect(await authenticationStorageProvider.isAuthenticated()).toBe(true); }); }); diff --git a/apps/webapp/src/authentication/core-logic/use-cases/logout/logout.spec.ts b/apps/webapp/src/authentication/core-logic/use-cases/logout/logout.spec.ts index 985819d..b0c23a2 100644 --- a/apps/webapp/src/authentication/core-logic/use-cases/logout/logout.spec.ts +++ b/apps/webapp/src/authentication/core-logic/use-cases/logout/logout.spec.ts @@ -47,6 +47,6 @@ describe("Logout", () => { store.dispatch(logout.fulfilled(undefined, "", undefined)); - expect(authenticationStorageProvider.isAuthenticated()).toBe(false); + expect(await authenticationStorageProvider.isAuthenticated()).toBe(false); }); }); diff --git a/apps/webapp/src/main.tsx b/apps/webapp/src/main.tsx index 1fa6246..c855004 100644 --- a/apps/webapp/src/main.tsx +++ b/apps/webapp/src/main.tsx @@ -2,14 +2,21 @@ import { startReactDsfr } from "@codegouvfr/react-dsfr/spa"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; +import { allRulesMap } from "shared-models"; import App from "./App.tsx"; -import { AuthenticationSessionStorageProvider } from "./authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts"; -import { storeAuthenticationOnLoginSuccess } from "./authentication/core-logic/listeners/authentication.listeners.ts"; +import { ApiAuthenticationGateway } from "./authentication/adapters/secondary/gateways/ApiAuthentication.gateway.ts"; +import { FetchAuthenticationApiClient } from "./authentication/adapters/secondary/gateways/FetchAuthentication.client.ts"; +import { IndexedDbAuthenticationStorageProvider } from "./authentication/adapters/secondary/providers/indexedDbAuthenticationStorage.provider.ts"; +import { + initializeAuthenticationState, + storeAuthenticationOnLoginSuccess, +} from "./authentication/core-logic/listeners/authentication.listeners.ts"; import { storeDisconnectionOnLogout } from "./authentication/core-logic/listeners/logout.listeners.ts"; import "./index.css"; import { ApiReportGateway } from "./reports/adapters/secondary/gateways/ApiReport.gateway.ts"; import { FetchReportApiClient } from "./reports/adapters/secondary/gateways/FetchReport.client.ts"; -import { initReduxStore } from "./store/reduxStore.ts"; +import { reportFileAttached } from "./reports/core-logic/listeners/report-file-attached.listeners.ts"; +import { routeToReactComponentMap } from "./router/adapters/routeToReactComponentMap.tsx"; import { RouteProvider, TypeRouterProvider, @@ -19,11 +26,8 @@ import { useRouteToComponentFactory } from "./router/adapters/type-route/useRout import { redirectOnLogin } from "./router/core-logic/listeners/redirectOnLogin.listeners.ts"; import { redirectOnLogout } from "./router/core-logic/listeners/redirectOnLogout.listeners.ts"; import { redirectOnRouteChange } from "./router/core-logic/listeners/redirectOnRouteChange.listeners.ts"; -import { reportFileAttached } from "./reports/core-logic/listeners/report-file-attached.listeners.ts"; -import { routeToReactComponentMap } from "./router/adapters/routeToReactComponentMap.tsx"; -import { allRulesMap } from "shared-models"; -import { ApiAuthenticationGateway } from "./authentication/adapters/secondary/gateways/ApiAuthentication.gateway.ts"; -import { FetchAuthenticationApiClient } from "./authentication/adapters/secondary/gateways/FetchAuthentication.client.ts"; +import { initReduxStore } from "./store/reduxStore.ts"; +import { AuthenticationSessionStorageProvider } from "./authentication/adapters/secondary/providers/authenticationSessionStorage.provider.ts"; startReactDsfr({ defaultColorScheme: "light" }); @@ -38,7 +42,9 @@ const reportApiClient = new FetchReportApiClient(import.meta.env.VITE_API_URL); const reportGateway = new ApiReportGateway(reportApiClient); const authenticationStorageProvider = - new AuthenticationSessionStorageProvider(); + IndexedDbAuthenticationStorageProvider.browserSupportsIndexedDB() + ? new IndexedDbAuthenticationStorageProvider() + : new AuthenticationSessionStorageProvider(); const store = initReduxStore( { reportGateway, authenticationGateway }, @@ -50,6 +56,7 @@ const store = initReduxStore( { storeAuthenticationOnLoginSuccess, + initializeAuthenticationState, storeDisconnectionOnLogout, redirectOnRouteChange, redirectOnLogout, diff --git a/apps/webapp/src/router/core-logic/listeners/redirectOnRouteChange.listeners.ts b/apps/webapp/src/router/core-logic/listeners/redirectOnRouteChange.listeners.ts index 133fad6..046a099 100644 --- a/apps/webapp/src/router/core-logic/listeners/redirectOnRouteChange.listeners.ts +++ b/apps/webapp/src/router/core-logic/listeners/redirectOnRouteChange.listeners.ts @@ -1,9 +1,12 @@ +import { authenticationStateInitFromStore } from "../../../authentication/core-logic/reducers/authentication.slice"; import { Listener } from "../../../store/listeners"; import { routeChanged } from "../reducers/router.slice"; export const redirectOnRouteChange: Listener = (startAppListening) => startAppListening({ - actionCreator: routeChanged, + predicate: (action) => + action.type === routeChanged.type || + action.type === authenticationStateInitFromStore.type, effect: ( action, { @@ -14,18 +17,29 @@ export const redirectOnRouteChange: Listener = (startAppListening) => }, ) => { if (!routerProvider) throw new Error("routerProvider is not defined"); - const { authenticated } = getState().authentication; + const state = getState(); + const { authenticated } = state.authentication; + const { current: currentHref } = state.router.hrefs; - switch (action.payload) { - case "/": - if (authenticated) routerProvider.goToReportList(); - else routerProvider.goToLogin(); - break; - case routerProvider.getLoginHref(): - if (authenticated) routerProvider.goToReportList(); - break; - default: - if (!authenticated) routerProvider.goToLogin(); + if (action.type === authenticationStateInitFromStore.type) { + const userOnLoginPage = currentHref === routerProvider.getLoginHref(); + if (authenticated && userOnLoginPage) routerProvider.goToReportList(); + return; + } + + if (action.type === routeChanged.type) { + switch (action.payload) { + case "/": + if (authenticated) routerProvider.goToReportList(); + else routerProvider.goToLogin(); + break; + case routerProvider.getLoginHref(): + if (authenticated) routerProvider.goToReportList(); + break; + default: + if (!authenticated) routerProvider.goToLogin(); + } + return; } }, }); diff --git a/apps/webapp/src/store/appState.ts b/apps/webapp/src/store/appState.ts index 964117c..d9fba4b 100644 --- a/apps/webapp/src/store/appState.ts +++ b/apps/webapp/src/store/appState.ts @@ -59,6 +59,7 @@ export interface AppState { }; }; authentication: { + initializedFromStore: boolean; authenticated: boolean; user: AuthenticatedUserSM | null; }; diff --git a/apps/webapp/src/store/listeners.ts b/apps/webapp/src/store/listeners.ts index 54c9cee..ec9b3b5 100644 --- a/apps/webapp/src/store/listeners.ts +++ b/apps/webapp/src/store/listeners.ts @@ -6,6 +6,7 @@ export type Listener = (startAppListening: StartAppListening) => void; export type AppListeners = { storeAuthenticationOnLoginSuccess: Listener; + initializeAuthenticationState: Listener; storeDisconnectionOnLogout: Listener; redirectOnRouteChange: Listener; redirectOnLogout: Listener; diff --git a/apps/webapp/src/store/reduxStore.ts b/apps/webapp/src/store/reduxStore.ts index cbbccc3..3ce6fc3 100644 --- a/apps/webapp/src/store/reduxStore.ts +++ b/apps/webapp/src/store/reduxStore.ts @@ -107,9 +107,7 @@ export const initReduxStore = ( rulesTuple, ).reducer, reportList, - authentication: createAuthenticationSlice({ - authenticationStorageProvider: providers?.authenticationStorageProvider, - }).reducer, + authentication: createAuthenticationSlice().reducer, router: createRouterSlice({ routerProvider: providers.routerProvider, routeToComponent: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32e5ceb..1f2c63d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: file-type: specifier: ^19.6.0 version: 19.6.0 + localforage: + specifier: ^1.10.0 + version: 1.10.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -232,6 +235,9 @@ importers: '@eslint/js': specifier: ^9.9.0 version: 9.12.0 + '@playwright/experimental-ct-react': + specifier: ^1.49.1 + version: 1.49.1(@types/node@20.16.10)(terser@5.34.1)(vite@5.4.8(@types/node@20.16.10)(terser@5.34.1)) '@testing-library/jest-dom': specifier: ^6.5.0 version: 6.5.0 @@ -247,6 +253,9 @@ importers: '@types/lodash': specifier: ^4.17.13 version: 4.17.13 + '@types/node': + specifier: ^20.3.1 + version: 20.16.10 '@types/pdfkit': specifier: ^0.13.5 version: 0.13.5 @@ -556,32 +565,62 @@ packages: resolution: {integrity: sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.5': + resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.25.7': resolution: {integrity: sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==} engines: {node: '>=6.9.0'} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.25.7': resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} engines: {node: '>=6.9.0'} + '@babel/generator@7.26.5': + resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.25.7': resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.26.5': + resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.7': resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.25.7': resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-plugin-utils@7.25.7': resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + '@babel/helper-simple-access@7.25.7': resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} engines: {node: '>=6.9.0'} @@ -590,6 +629,10 @@ packages: resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.7': resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} @@ -602,10 +645,18 @@ packages: resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.25.7': resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.25.7': resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} engines: {node: '>=6.9.0'} @@ -615,6 +666,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.26.5': + resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -706,6 +762,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.25.7': resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} engines: {node: '>=6.9.0'} @@ -718,14 +786,26 @@ packages: resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.25.7': resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.5': + resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.25.7': resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.5': + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} + engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} @@ -1596,6 +1676,15 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/experimental-ct-core@1.49.1': + resolution: {integrity: sha512-MZ0by8hLo/2qGHoo3SSbdxTNwKXrw06rqqA64TEiwOBjzpbYAaCuA3+BemXpMFfa0nU6ivO4h/sFdA8Zv9ns+g==} + engines: {node: '>=18'} + + '@playwright/experimental-ct-react@1.49.1': + resolution: {integrity: sha512-kQTSzVkFd05x1kY8Q/XaF3xg3dd3AH31976N0zCVVQ3pw/qpa3jtdjoj7/30eblU5eVlg0ELMV7pjL5lUjJzzw==} + engines: {node: '>=18'} + hasBin: true + '@reduxjs/toolkit@2.2.7': resolution: {integrity: sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==} peerDependencies: @@ -2235,6 +2324,12 @@ packages: peerDependencies: vite: ^4 || ^5 + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/expect@2.1.2': resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==} @@ -3531,6 +3626,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3719,6 +3819,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.1: resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} @@ -4156,6 +4259,9 @@ packages: libpq@1.8.13: resolution: {integrity: sha512-t1wpnGVgwRIFSKoe4RFUllAFj953kNMcdXhGvFJwI0r6lJQqgSwTeiIciaCinjOmHk0HnFeWQSMC6Uw2591G4A==} + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4174,6 +4280,9 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -4659,6 +4768,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4909,6 +5028,10 @@ packages: redux: optional: true + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6433,6 +6556,8 @@ snapshots: '@babel/compat-data@7.25.7': {} + '@babel/compat-data@7.26.5': {} + '@babel/core@7.25.7': dependencies: '@ampproject/remapping': 2.3.0 @@ -6453,6 +6578,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.5 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.5 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.25.7': dependencies: '@babel/types': 7.25.7 @@ -6460,6 +6605,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.0.2 + '@babel/generator@7.26.5': + dependencies: + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + '@babel/helper-compilation-targets@7.25.7': dependencies: '@babel/compat-data': 7.25.7 @@ -6468,6 +6621,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.26.5': + dependencies: + '@babel/compat-data': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.0 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-module-imports@7.25.7': dependencies: '@babel/traverse': 7.25.7 @@ -6475,6 +6636,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 @@ -6485,8 +6653,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + '@babel/helper-plugin-utils@7.25.7': {} + '@babel/helper-plugin-utils@7.26.5': {} + '@babel/helper-simple-access@7.25.7': dependencies: '@babel/traverse': 7.25.7 @@ -6496,17 +6675,26 @@ snapshots: '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-validator-identifier@7.25.7': {} '@babel/helper-validator-identifier@7.25.9': {} '@babel/helper-validator-option@7.25.7': {} + '@babel/helper-validator-option@7.25.9': {} + '@babel/helpers@7.25.7': dependencies: '@babel/template': 7.25.7 '@babel/types': 7.25.7 + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.5 + '@babel/highlight@7.25.7': dependencies: '@babel/helper-validator-identifier': 7.25.7 @@ -6518,6 +6706,10 @@ snapshots: dependencies: '@babel/types': 7.25.7 + '@babel/parser@7.26.5': + dependencies: + '@babel/types': 7.26.5 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.7)': dependencies: '@babel/core': 7.25.7 @@ -6603,6 +6795,16 @@ snapshots: '@babel/core': 7.25.7 '@babel/helper-plugin-utils': 7.25.7 + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/runtime@7.25.7': dependencies: regenerator-runtime: 0.14.1 @@ -6613,13 +6815,19 @@ snapshots: '@babel/template@7.25.7': dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 '@babel/parser': 7.25.7 '@babel/types': 7.25.7 + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + '@babel/traverse@7.25.7': dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 '@babel/generator': 7.25.7 '@babel/parser': 7.25.7 '@babel/template': 7.25.7 @@ -6629,12 +6837,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.26.5': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.5 + '@babel/parser': 7.26.5 + '@babel/template': 7.25.9 + '@babel/types': 7.26.5 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.25.7': dependencies: '@babel/helper-string-parser': 7.25.7 '@babel/helper-validator-identifier': 7.25.7 to-fast-properties: 2.0.0 + '@babel/types@7.26.5': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@balena/dockerignore@1.0.2': {} '@bcoe/v8-coverage@0.2.3': {} @@ -7415,6 +7640,37 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/experimental-ct-core@1.49.1(@types/node@20.16.10)(terser@5.34.1)': + dependencies: + playwright: 1.49.1 + playwright-core: 1.49.1 + vite: 5.4.8(@types/node@20.16.10)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + + '@playwright/experimental-ct-react@1.49.1(@types/node@20.16.10)(terser@5.34.1)(vite@5.4.8(@types/node@20.16.10)(terser@5.34.1))': + dependencies: + '@playwright/experimental-ct-core': 1.49.1(@types/node@20.16.10)(terser@5.34.1) + '@vitejs/plugin-react': 4.3.4(vite@5.4.8(@types/node@20.16.10)(terser@5.34.1)) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - vite + '@reduxjs/toolkit@2.2.7(react-redux@9.1.2(@types/react@18.3.11)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': dependencies: immer: 10.1.1 @@ -8251,6 +8507,17 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitejs/plugin-react@4.3.4(vite@5.4.8(@types/node@20.16.10)(terser@5.34.1))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.8(@types/node@20.16.10)(terser@5.34.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.2': dependencies: '@vitest/spy': 2.1.2 @@ -9708,6 +9975,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9892,6 +10162,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + immer@10.1.1: {} import-fresh@3.3.0: @@ -10531,6 +10803,10 @@ snapshots: bindings: 1.5.0 nan: 2.19.0 + lie@3.1.1: + dependencies: + immediate: 3.0.6 + lilconfig@2.1.0: {} lilconfig@3.1.2: {} @@ -10544,6 +10820,10 @@ snapshots: loader-runner@4.3.0: {} + localforage@1.10.0: + dependencies: + lie: 3.1.1 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -10971,6 +11251,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} png-js@1.0.0: {} @@ -11140,6 +11428,8 @@ snapshots: '@types/react': 18.3.11 redux: 5.0.1 + react-refresh@0.14.2: {} + react@18.3.1: dependencies: loose-envify: 1.4.0