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