✨ feat: implement new feature for enhanced user experience
This commit is contained in:
1
.env
1
.env
@@ -19,5 +19,6 @@ STAGING_PATH=/staging/__ORG__/__PROJECT__/dev
|
|||||||
|
|
||||||
LIVE_URL=https://www
|
LIVE_URL=https://www
|
||||||
STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online
|
STAGING_URL=https://dev-__PROJECT_NAME__.staging.testversion.online
|
||||||
|
CODING_URL=https://__PROJECT_NAME__.code.testversion.online
|
||||||
|
|
||||||
#START_SCRIPT=:ssr
|
#START_SCRIPT=:ssr
|
||||||
|
|||||||
32
.github/copilot-instructions.md
vendored
32
.github/copilot-instructions.md
vendored
@@ -1,25 +1,25 @@
|
|||||||
# Copilot Instructions
|
# Copilot Instructions
|
||||||
|
|
||||||
## Common Instructions
|
This workspace uses scoped instructions with YAML front matter in `.github/instructions/*.instructions.md`.
|
||||||
|
Keep this file minimal to avoid duplicate or conflicting guidance.
|
||||||
|
|
||||||
- Look in the problems tab for any errors or warnings in the code
|
## Quick Reference
|
||||||
- Follow the existing code style and conventions used in the project
|
|
||||||
- Write clear and concise comments where necessary to explain complex logic
|
- **General workflow**: See `.github/instructions/general.instructions.md`
|
||||||
- Ensure code is modular and reusable where possible
|
- **Frontend (Svelte)**: See `.github/instructions/frontend.instructions.md`
|
||||||
- Write unit tests for new functionality and ensure existing tests pass, but only if there is a configured testing framework
|
- **API Hooks (tibi-server)**: See `.github/instructions/api-hooks.instructions.md`
|
||||||
- Avoid introducing new dependencies unless absolutely necessary, but ask the user if there is a specific library they want to use
|
- **SSR/Caching**: See `.github/instructions/ssr.instructions.md`
|
||||||
- If you are unsure about any requirements or details, ask the user for clarification before proceeding
|
- **Testing (Playwright)**: See `.github/instructions/testing.instructions.md`
|
||||||
- Respect a11y and localization best practices if applicable, optimize for WCAG AA standards
|
|
||||||
|
|
||||||
## Toolchain
|
## Toolchain
|
||||||
|
|
||||||
- See .env in root for project specific environment variables
|
- See `.env` in root for project-specific environment variables
|
||||||
- See Makefile for starting up the development environment with Docker
|
- See `Makefile` for starting up the development environment with Docker
|
||||||
- If development environment is running, access the website at: https://${PROJECT_NAME}.code.testversion.online/ or ask the user for the correct URL
|
- If development environment is running, access the website at: `https://${PROJECT_NAME}.code.testversion.online/` or ask the user for the correct URL
|
||||||
- You can also use Browser MCP, so ask user to connect if needed
|
- You can also use Browser MCP, so ask user to connect if needed
|
||||||
- Esbuild is used, watching for changes in files to rebuild automatically
|
- Esbuild is used, watching for changes in files to rebuild automatically
|
||||||
- To force a restart of the frontend build and dev-server run: `make restart-frontend`
|
- To force a restart of the frontend build and dev-server run: `make docker-restart-frontend`
|
||||||
- Backend is tibi-server configured in /api/ folder and also restarted if changes are detected in this folder
|
- Backend is tibi-server configured in `/api/` folder and also restarted if changes are detected in this folder
|
||||||
- To show last X lines of docker logs run: `make docker-logs-X` where X is the number
|
- To show last X lines of docker logs run: `make docker-logs-X` where X is the number of lines you want to see
|
||||||
of lines you want to see
|
|
||||||
- For a11y testing use the MCP a11y tools if available
|
- For a11y testing use the MCP a11y tools if available
|
||||||
|
- For testing run: `yarn test` (all), `yarn test:e2e`, `yarn test:api`, `yarn test:visual`
|
||||||
|
|||||||
51
.github/instructions/api-hooks.instructions.md
vendored
Normal file
51
.github/instructions/api-hooks.instructions.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: API Hooks
|
||||||
|
description: tibi-server hook conventions and typing.
|
||||||
|
applyTo: "api/hooks/**"
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Hooks (tibi-server)
|
||||||
|
|
||||||
|
- Wrap hook files in an IIFE: `;(function () { ... })()`.
|
||||||
|
- Always return a HookResponse type or throw a HookException type.
|
||||||
|
- Use inline type casting with `/** @type {TypeName} */ (value)` and typed collection entries from `types/global.d.ts`.
|
||||||
|
- Avoid `@ts-ignore`; use proper casting instead.
|
||||||
|
- Use `const` and `let` instead of `var`. The tibi-server runtime supports modern JS declarations.
|
||||||
|
|
||||||
|
## context.filter – Go object quirk
|
||||||
|
|
||||||
|
`context.filter` is not a regular JS object but a Go object. Even when empty, it is **truthy**.
|
||||||
|
Always check with `Object.keys()`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const requestedFilter =
|
||||||
|
context.filter &&
|
||||||
|
typeof context.filter === "object" &&
|
||||||
|
!Array.isArray(context.filter) &&
|
||||||
|
Object.keys(context.filter).length > 0
|
||||||
|
? context.filter
|
||||||
|
: null
|
||||||
|
```
|
||||||
|
|
||||||
|
**Never** use `context.filter || null` – it is always truthy and results in an empty filter object inside `$and`, which crashes the Go server.
|
||||||
|
|
||||||
|
## Single-item vs. list retrieval
|
||||||
|
|
||||||
|
For single-item retrieval (`GET /:collection/:id`), the Go server sets `_id` automatically from the URL parameter.
|
||||||
|
GET read hooks should therefore **not set their own `_id` filter** for `req.param("id")`;
|
||||||
|
instead, only add authorization filters (e.g. `{ userId: userId }`).
|
||||||
|
|
||||||
|
## HookResponse fields for GET hooks
|
||||||
|
|
||||||
|
- `filter` – MongoDB filter (list retrieval only, or to restrict single-item retrieval)
|
||||||
|
- `selector` – MongoDB projection (`{ fieldName: 0 }` to exclude, `{ fieldName: 1 }` to include)
|
||||||
|
- `offset`, `limit`, `sort` – pagination/sorting
|
||||||
|
- `pipelineMod` – function to manipulate the aggregation pipeline
|
||||||
|
|
||||||
|
## API Tests (Playwright)
|
||||||
|
|
||||||
|
- When creating or modifying collections/hooks: extend or create corresponding API tests in `tests/api/`.
|
||||||
|
- Test files live under `tests/api/` and use fixtures from `tests/api/fixtures.ts`.
|
||||||
|
- Helpers: `ensureTestUser()` (`tests/api/helpers/test-user.ts`), Admin API (`tests/api/helpers/admin-api.ts`), MailDev (`tests/api/helpers/maildev.ts`).
|
||||||
|
- After hook changes, run only the affected API tests: `npx playwright test tests/api/filename.spec.ts`.
|
||||||
|
- When tests fail, clarify whether the hook or the test needs adjustment – coordinate with the user.
|
||||||
28
.github/instructions/frontend.instructions.md
vendored
Normal file
28
.github/instructions/frontend.instructions.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: Frontend
|
||||||
|
description: Svelte SPA structure and conventions.
|
||||||
|
applyTo: "frontend/src/**"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
|
||||||
|
- SPA entry point is `frontend/src/index.ts`, main component is `frontend/src/App.svelte`.
|
||||||
|
- Component organization: `lib/` for utilities and stores, keep route components in a `routes/` folder when needed, `css/` for styles.
|
||||||
|
- Use PascalCase component names and export props at the top of the `<script>` tag; keep code/comments in English.
|
||||||
|
- SSR safety: guard browser-only code with `typeof window !== "undefined"`.
|
||||||
|
- API behavior: PUT responses return only changed fields; filter by id uses `_id`; API requests reject non-2xx with `{ response, data }` and error payload in `error.data.error`.
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
- `svelte-i18n` is configured in `frontend/src/lib/i18n/index.ts` with lazy loading for locale files.
|
||||||
|
- Locale files live in `frontend/src/lib/i18n/locales/{lang}.json`.
|
||||||
|
- URL-based language routing: `/{lang}/...` (e.g. `/de/`, `/en/about`).
|
||||||
|
- Language utilities in `frontend/src/lib/i18n.ts`: `extractLanguageFromPath()`, `localizedPath()`, `getLanguageSwitchUrl()`.
|
||||||
|
- Use `$_("key")` from `svelte-i18n` for translations in components.
|
||||||
|
|
||||||
|
## E2E Tests (Playwright)
|
||||||
|
|
||||||
|
- When developing frontend features: extend or create corresponding E2E tests in `tests/e2e/`.
|
||||||
|
- Shared fixtures and helpers in `tests/e2e/fixtures.ts`: `waitForSpaReady(page)`, `navigateToRoute(page, path)`, `clickSpaLink(page, selector)`, `authedPage` fixture.
|
||||||
|
- After frontend changes, run only the affected E2E tests: `npx playwright test tests/e2e/filename.spec.ts` or `-g "test name"`.
|
||||||
|
- When tests fail, clarify whether the frontend or the test needs adjustment – coordinate with the user.
|
||||||
38
.github/instructions/general.instructions.md
vendored
Normal file
38
.github/instructions/general.instructions.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: General
|
||||||
|
description: Workspace-wide guidance and workflows.
|
||||||
|
applyTo: "**/*"
|
||||||
|
---
|
||||||
|
|
||||||
|
# General
|
||||||
|
|
||||||
|
## Code Style & Conventions
|
||||||
|
|
||||||
|
- Look in the problems tab for any errors or warnings in the code
|
||||||
|
- Follow the existing code style and conventions used in the project
|
||||||
|
- Write clear and concise comments where necessary to explain complex logic
|
||||||
|
- Ensure code is modular and reusable where possible
|
||||||
|
- Avoid introducing new dependencies unless absolutely necessary, but ask the user if there is a specific library they want to use
|
||||||
|
- If you are unsure about any requirements or details, ask the user for clarification before proceeding
|
||||||
|
- Respect a11y and localization best practices if applicable, optimize for WCAG AA standards
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
- Default dev flow is Docker/Makefile: `make docker-up`, `make docker-start`, `make docker-logs`, `make docker-restart-frontend` (see Makefile).
|
||||||
|
- Local dev is secondary: `yarn dev` for watch, `yarn build` and `yarn build:server` for production builds (see package.json).
|
||||||
|
- Frontend code is automatically built by watcher and browser-sync; backend code is automatically built and reloaded by extension, so no manual restarts needed during development.
|
||||||
|
- Read `.env` for environment URLs and secrets.
|
||||||
|
- Keep `tibi-types/` read-only unless explicitly asked.
|
||||||
|
- `webserver/` is for staging/ops only; use BrowserSync/esbuild for day-to-day dev.
|
||||||
|
|
||||||
|
## API Access
|
||||||
|
|
||||||
|
- API access to collections uses the reverse proxy: `CODING_URL/api/<collection>` (e.g. `CODING_URL/api/content`).
|
||||||
|
- Auth via `Token` header with ADMIN_TOKEN from `api/config.yml.env`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Write unit tests for new functionality and ensure existing tests pass.
|
||||||
|
- Playwright is configured for E2E, API, mobile, and visual regression tests.
|
||||||
|
- Run tests via `yarn test` (all), `yarn test:e2e`, `yarn test:api`, `yarn test:visual`.
|
||||||
|
- After code changes, run only affected spec files: `npx playwright test tests/e2e/filename.spec.ts`.
|
||||||
12
.github/instructions/ssr.instructions.md
vendored
Normal file
12
.github/instructions/ssr.instructions.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: SSR
|
||||||
|
description: Server-side rendering flow and caching.
|
||||||
|
applyTo: "api/hooks/ssr/**"
|
||||||
|
---
|
||||||
|
|
||||||
|
# SSR and Caching
|
||||||
|
|
||||||
|
- SSR request flow: `api/hooks/ssr/get_read.js` calls `api/hooks/lib/ssr-server.js` and injects `window.__SSR_CACHE__` used by `api/hooks/lib/ssr.js` on the client.
|
||||||
|
- SSR cache HTML is stored in the `ssr` collection.
|
||||||
|
- SSR builds output to `api/hooks/lib/app.server.js` via `yarn build:server`.
|
||||||
|
- SSR route validation is currently disabled and returns -1 in `api/hooks/config.js`; update this when enabling SSR per route.
|
||||||
41
.github/instructions/testing.instructions.md
vendored
Normal file
41
.github/instructions/testing.instructions.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Testing
|
||||||
|
description: Playwright test conventions, fixtures, and visual regression.
|
||||||
|
applyTo: "tests/**"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
- Playwright for API tests (`tests/api/`), E2E tests (`tests/e2e/`), mobile tests (`tests/e2e-mobile/`), and Visual Regression tests (`tests/e2e-visual/`). Config in `playwright.config.ts`.
|
||||||
|
- Self-test after code changes: run only affected spec files (`npx playwright test tests/e2e/filename.spec.ts` or `-g "test name"`), not a full test suite run – saves time.
|
||||||
|
- Always coordinate test adjustments with the user: do not fix broken tests to match buggy frontend or vice versa. When tests fail, first clarify whether the test or the code is wrong.
|
||||||
|
|
||||||
|
## BrowserSync Workaround
|
||||||
|
|
||||||
|
BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `load` from resolving. All fixture files override `page.goto()` and `page.reload()` to use `waitUntil: "domcontentloaded"` by default. Always use `domcontentloaded` for navigation waits.
|
||||||
|
|
||||||
|
## Fixtures & Helpers
|
||||||
|
|
||||||
|
- `tests/e2e/fixtures.ts` – Shared fixtures (`authedPage`, `testUser`) and helpers (`waitForSpaReady`, `navigateToRoute`, `clickSpaLink`).
|
||||||
|
- `tests/e2e-visual/fixtures.ts` – Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`).
|
||||||
|
- `tests/e2e-mobile/fixtures.ts` – Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
|
||||||
|
- `tests/api/fixtures.ts` – API fixtures (`api`, `authedApi`, `accessToken`).
|
||||||
|
- `tests/api/helpers/` – API test utilities (`test-user.ts`, `admin-api.ts`, `maildev.ts`).
|
||||||
|
- `tests/fixtures/test-constants.ts` – Central constants (`TEST_USER`, `ADMIN_TOKEN`, `API_BASE`).
|
||||||
|
|
||||||
|
## Visual Regression Tests
|
||||||
|
|
||||||
|
- Visual regression tests live in `tests/e2e-visual/` with separate Playwright projects (`visual-desktop`, `visual-iphonese`, `visual-ipad`).
|
||||||
|
- Run: `yarn test:visual`. Update baselines: `yarn test:visual:update`.
|
||||||
|
- Screenshots are stored in `tests/e2e-visual/__screenshots__/{projectName}/` and MUST be committed to the repo.
|
||||||
|
- Tolerance: `maxDiffPixelRatio: 0.02` (2%) for cross-OS/hardware rendering differences.
|
||||||
|
- Always call `prepareForScreenshot(page)` before `expectScreenshot()`.
|
||||||
|
- Use `waitForVisualReady(page)` instead of `waitForSpaReady()` – it additionally waits for skeleton loaders and CSS settling.
|
||||||
|
- Dynamic content: `hideDynamicContent(page)` disables BrowserSync overlay and animations; `getDynamicMasks(page)` returns locators for non-deterministic elements.
|
||||||
|
- For AI review of screenshots, run `./scripts/export-visual-screenshots.sh`.
|
||||||
|
|
||||||
|
## API Tests
|
||||||
|
|
||||||
|
- API tests use `tests/api/fixtures.ts` with `api` (unauthenticated) and `authedApi` (with Bearer token) fixtures.
|
||||||
|
- Helpers: `ensureTestUser()` (`tests/api/helpers/test-user.ts`), Admin API (`tests/api/helpers/admin-api.ts`), MailDev (`tests/api/helpers/maildev.ts`).
|
||||||
|
- After hook changes, run only affected API tests: `npx playwright test tests/api/filename.spec.ts`.
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,10 @@ tmp
|
|||||||
_temp
|
_temp
|
||||||
frontend/dist
|
frontend/dist
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
visual-review/
|
||||||
.yarn/*
|
.yarn/*
|
||||||
!.yarn/cache
|
!.yarn/cache
|
||||||
!.yarn/patches
|
!.yarn/patches
|
||||||
|
|||||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -21,13 +21,19 @@
|
|||||||
"event": "onFileChange"
|
"event": "onFileChange"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"i18n-ally.localesPaths": ["frontend/locales"],
|
"i18n-ally.localesPaths": ["frontend/src/lib/i18n/locales"],
|
||||||
"i18n-ally.sourceLanguage": "de",
|
"i18n-ally.sourceLanguage": "de",
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.enabledFrameworks": ["svelte"],
|
||||||
|
"i18n-ally.displayLanguage": "de",
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
},
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"css": "tailwindcss"
|
"css": "tailwindcss"
|
||||||
}
|
},
|
||||||
|
"css.validate": true,
|
||||||
|
"css.lint.unknownAtRules": "ignore",
|
||||||
|
"playwright.reuseBrowser": false,
|
||||||
|
"playwright.showTrace": true
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
.yarn/cache/@esbuild-linux-x64-npm-0.19.12-59062fdb38-10.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@esbuild-linux-x64-npm-0.19.12-59062fdb38-10.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@formatjs-ecma402-abstract-npm-2.3.6-b28618e55c-30b1b5cd6b.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@formatjs-ecma402-abstract-npm-2.3.6-b28618e55c-30b1b5cd6b.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@formatjs-fast-memoize-npm-2.2.7-bc909b3b5a-e7e6efc677.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@formatjs-fast-memoize-npm-2.2.7-bc909b3b5a-e7e6efc677.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@formatjs-icu-messageformat-parser-npm-2.11.4-b051248584-2acb100c06.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@formatjs-icu-messageformat-parser-npm-2.11.4-b051248584-2acb100c06.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@formatjs-icu-skeleton-parser-npm-1.8.16-e9d6e923fd-428001e5be.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@formatjs-icu-skeleton-parser-npm-1.8.16-e9d6e923fd-428001e5be.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@formatjs-intl-localematcher-npm-0.6.2-984821923e-eb12a7f536.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@formatjs-intl-localematcher-npm-0.6.2-984821923e-eb12a7f536.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@playwright-test-npm-1.58.2-03a96deb3c-58bf901392.zip
LFS
vendored
Normal file
BIN
.yarn/cache/@playwright-test-npm-1.58.2-03a96deb3c-58bf901392.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/cli-color-npm-2.0.4-d35494cfd7-6706fbb98f.zip
LFS
vendored
Normal file
BIN
.yarn/cache/cli-color-npm-2.0.4-d35494cfd7-6706fbb98f.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/d-npm-1.0.2-7abbb6ae36-a3f45ef964.zip
LFS
vendored
Normal file
BIN
.yarn/cache/d-npm-1.0.2-7abbb6ae36-a3f45ef964.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/decimal.js-npm-10.6.0-a72c1b8a2f-c0d45842d4.zip
LFS
vendored
Normal file
BIN
.yarn/cache/decimal.js-npm-10.6.0-a72c1b8a2f-c0d45842d4.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/deepmerge-npm-4.3.1-4f751a0844-058d9e1b0f.zip
LFS
vendored
Normal file
BIN
.yarn/cache/deepmerge-npm-4.3.1-4f751a0844-058d9e1b0f.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/es5-ext-npm-0.10.64-c30cdc3d60-0c5d865770.zip
LFS
vendored
Normal file
BIN
.yarn/cache/es5-ext-npm-0.10.64-c30cdc3d60-0c5d865770.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/es6-iterator-npm-2.0.3-4dadb0ccc1-dbadecf3d0.zip
LFS
vendored
Normal file
BIN
.yarn/cache/es6-iterator-npm-2.0.3-4dadb0ccc1-dbadecf3d0.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/es6-symbol-npm-3.1.4-7d67ac432c-3743119fe6.zip
LFS
vendored
Normal file
BIN
.yarn/cache/es6-symbol-npm-3.1.4-7d67ac432c-3743119fe6.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/es6-weak-map-npm-2.0.3-5e57e0b4e6-5958a321cf.zip
LFS
vendored
Normal file
BIN
.yarn/cache/es6-weak-map-npm-2.0.3-5e57e0b4e6-5958a321cf.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/esbuild-npm-0.19.12-fb5a3a4313-861fa8eb24.zip
LFS
vendored
Normal file
BIN
.yarn/cache/esbuild-npm-0.19.12-fb5a3a4313-861fa8eb24.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/esniff-npm-2.0.1-26cea8766c-f6a2abd2f8.zip
LFS
vendored
Normal file
BIN
.yarn/cache/esniff-npm-2.0.1-26cea8766c-f6a2abd2f8.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip
LFS
vendored
Normal file
BIN
.yarn/cache/estree-walker-npm-2.0.2-dfab42f65c-b02109c5d4.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/event-emitter-npm-0.3.5-f1e8b8edb5-a7f5ea8002.zip
LFS
vendored
Normal file
BIN
.yarn/cache/event-emitter-npm-0.3.5-f1e8b8edb5-a7f5ea8002.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/ext-npm-1.7.0-580588ab93-666a135980.zip
LFS
vendored
Normal file
BIN
.yarn/cache/ext-npm-1.7.0-580588ab93-666a135980.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip
LFS
vendored
Normal file
BIN
.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/globalyzer-npm-0.1.0-3982d25961-419a0f95ba.zip
LFS
vendored
Normal file
BIN
.yarn/cache/globalyzer-npm-0.1.0-3982d25961-419a0f95ba.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/globrex-npm-0.1.2-ddda94f2d0-81ce62ee6f.zip
LFS
vendored
Normal file
BIN
.yarn/cache/globrex-npm-0.1.2-ddda94f2d0-81ce62ee6f.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/intl-messageformat-npm-10.7.18-b63c15bddc-96650d6739.zip
LFS
vendored
Normal file
BIN
.yarn/cache/intl-messageformat-npm-10.7.18-b63c15bddc-96650d6739.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/is-promise-npm-2.2.2-afbf94db67-18bf7d1c59.zip
LFS
vendored
Normal file
BIN
.yarn/cache/is-promise-npm-2.2.2-afbf94db67-18bf7d1c59.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/lru-queue-npm-0.1.0-8e1c90dde8-55b08ee3a7.zip
LFS
vendored
Normal file
BIN
.yarn/cache/lru-queue-npm-0.1.0-8e1c90dde8-55b08ee3a7.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/memoizee-npm-0.4.17-95e9fda366-b7abda74d1.zip
LFS
vendored
Normal file
BIN
.yarn/cache/memoizee-npm-0.4.17-95e9fda366-b7abda74d1.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/next-tick-npm-1.1.0-e0eb60d6a4-83b5cf3602.zip
LFS
vendored
Normal file
BIN
.yarn/cache/next-tick-npm-1.1.0-e0eb60d6a4-83b5cf3602.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/playwright-core-npm-1.58.2-7d85ddc78a-8a98fcf122.zip
LFS
vendored
Normal file
BIN
.yarn/cache/playwright-core-npm-1.58.2-7d85ddc78a-8a98fcf122.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/playwright-npm-1.58.2-0c12daad27-d89d6c8a32.zip
LFS
vendored
Normal file
BIN
.yarn/cache/playwright-npm-1.58.2-0c12daad27-d89d6c8a32.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/svelte-i18n-npm-4.0.1-e8d73fe51f-683f921429.zip
LFS
vendored
Normal file
BIN
.yarn/cache/svelte-i18n-npm-4.0.1-e8d73fe51f-683f921429.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/timers-ext-npm-0.1.8-1fa0ad5365-8abd168c57.zip
LFS
vendored
Normal file
BIN
.yarn/cache/timers-ext-npm-0.1.8-1fa0ad5365-8abd168c57.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/tiny-glob-npm-0.2.9-068f4ab3f8-5fb773747f.zip
LFS
vendored
Normal file
BIN
.yarn/cache/tiny-glob-npm-0.2.9-068f4ab3f8-5fb773747f.zip
LFS
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/type-npm-2.7.3-509458c133-82e99e7795.zip
LFS
vendored
Normal file
BIN
.yarn/cache/type-npm-2.7.3-509458c133-82e99e7795.zip
LFS
vendored
Normal file
Binary file not shown.
Binary file not shown.
17
Makefile
17
Makefile
@@ -2,7 +2,7 @@ DOCKER_COMPOSE=docker compose -f docker-compose-local.yml
|
|||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: init docker-up docker-restart-frontend docker-up-tibi-dev docker-start docker-start-tibi-dev docker-down docker-ps docker-logs yarn-upgrade fix-permissions mongo-sync-master-to-local media-sync-master-to-local mongo-sync-local-to-staging media-sync-local-to-staging
|
.PHONY: init docker-up docker-restart-frontend docker-up-tibi-dev docker-start docker-start-tibi-dev docker-down docker-ps docker-logs yarn-upgrade fix-permissions mongo-sync-master-to-local media-sync-master-to-local mongo-sync-local-to-staging media-sync-local-to-staging test test-e2e test-api test-visual
|
||||||
|
|
||||||
include ./.env
|
include ./.env
|
||||||
|
|
||||||
@@ -49,6 +49,21 @@ docker-pull: ## pull docker images
|
|||||||
docker-%:
|
docker-%:
|
||||||
$(DOCKER_COMPOSE) $*
|
$(DOCKER_COMPOSE) $*
|
||||||
|
|
||||||
|
test: ## run all Playwright tests
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
test-e2e: ## run E2E Playwright tests
|
||||||
|
yarn test:e2e
|
||||||
|
|
||||||
|
test-api: ## run API Playwright tests
|
||||||
|
yarn test:api
|
||||||
|
|
||||||
|
test-visual: ## run visual regression tests
|
||||||
|
yarn test:visual
|
||||||
|
|
||||||
|
test-visual-update: ## update visual regression baselines
|
||||||
|
yarn test:visual:update
|
||||||
|
|
||||||
yarn-upgrade: ## interactive yarn upgrade
|
yarn-upgrade: ## interactive yarn upgrade
|
||||||
$(DOCKER_COMPOSE) run --rm yarnstart yarn upgrade-interactive
|
$(DOCKER_COMPOSE) run --rm yarnstart yarn upgrade-interactive
|
||||||
$(DOCKER_COMPOSE) restart yarnstart
|
$(DOCKER_COMPOSE) restart yarnstart
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { metricCall } from "./config"
|
import { metricCall } from "./config"
|
||||||
import { location } from "./lib/store"
|
import { location } from "./lib/store"
|
||||||
|
import { _, locale } from "./lib/i18n/index"
|
||||||
|
import {
|
||||||
|
SUPPORTED_LANGUAGES,
|
||||||
|
LANGUAGE_LABELS,
|
||||||
|
currentLanguage,
|
||||||
|
localizedPath,
|
||||||
|
getLanguageSwitchUrl,
|
||||||
|
getBrowserLanguage,
|
||||||
|
extractLanguageFromPath,
|
||||||
|
DEFAULT_LANGUAGE,
|
||||||
|
getRoutePath,
|
||||||
|
} from "./lib/i18n"
|
||||||
export let url = ""
|
export let url = ""
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -13,8 +25,19 @@
|
|||||||
push: false,
|
push: false,
|
||||||
pop: false,
|
pop: false,
|
||||||
}
|
}
|
||||||
|
// Set svelte-i18n locale from URL for SSR rendering
|
||||||
|
const lang = extractLanguageFromPath(l[0]) || DEFAULT_LANGUAGE
|
||||||
|
$locale = lang
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect root "/" to default language
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window !== "undefined" && $location.path === "/") {
|
||||||
|
const lang = getBrowserLanguage()
|
||||||
|
history.replaceState(null, "", `/${lang}/`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// metrics
|
// metrics
|
||||||
let oldPath = $state("")
|
let oldPath = $state("")
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -38,14 +61,47 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="text-white p-4 bg-red-900">
|
<header class="text-white p-4 bg-red-900">
|
||||||
<div class="container mx-auto flex justify-between items-center">
|
<div class="container mx-auto flex flex-wrap items-center justify-between gap-2">
|
||||||
<a href="/" class="text-xl font-bold">Tibi Svelte Starter</a>
|
<a href={localizedPath("/")} class="text-xl font-bold shrink-0">Tibi Svelte Starter</a>
|
||||||
<nav>
|
<nav class="flex items-center gap-4">
|
||||||
<ul class="flex space-x-4">
|
<ul class="hidden sm:flex space-x-4">
|
||||||
<li><a href="/" class="hover:underline">Home</a></li>
|
<li><a href={localizedPath("/")} class="hover:underline">{$_("nav.home")}</a></li>
|
||||||
<li><a href="/about" class="hover:underline">About</a></li>
|
<li><a href={localizedPath("/about")} class="hover:underline">{$_("nav.about")}</a></li>
|
||||||
<li><a href="/contact" class="hover:underline">Contact</a></li>
|
<li><a href={localizedPath("/contact")} class="hover:underline">{$_("nav.contact")}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="flex space-x-2 text-sm sm:border-l sm:border-white/30 sm:pl-4">
|
||||||
|
{#each SUPPORTED_LANGUAGES as lang}
|
||||||
|
<a
|
||||||
|
href={getLanguageSwitchUrl(lang)}
|
||||||
|
class="hover:underline px-1"
|
||||||
|
class:font-bold={$currentLanguage === lang}
|
||||||
|
class:opacity-60={$currentLanguage !== lang}
|
||||||
|
>
|
||||||
|
{LANGUAGE_LABELS[lang]}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Mobile nav -->
|
||||||
|
<nav class="sm:hidden mt-2 border-t border-white/20 pt-2">
|
||||||
|
<ul class="flex space-x-4 text-sm">
|
||||||
|
<li><a href={localizedPath("/")} class="hover:underline">{$_("nav.home")}</a></li>
|
||||||
|
<li><a href={localizedPath("/about")} class="hover:underline">{$_("nav.about")}</a></li>
|
||||||
|
<li><a href={localizedPath("/contact")} class="hover:underline">{$_("nav.contact")}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<main class="container mx-auto p-4">
|
||||||
|
{#if getRoutePath($location.path) === "/about"}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">{$_("page.about.title")}</h1>
|
||||||
|
<p>{$_("page.about.text")}</p>
|
||||||
|
{:else if getRoutePath($location.path) === "/contact"}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">{$_("page.contact.title")}</h1>
|
||||||
|
<p>{$_("page.contact.text")}</p>
|
||||||
|
{:else}
|
||||||
|
<h1 class="text-2xl font-bold mb-4">{$_("page.home.title")}</h1>
|
||||||
|
<p>{$_("page.home.text")}</p>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import "./css/style.css"
|
import "./css/style.css"
|
||||||
import App from "./App.svelte"
|
import App from "./App.svelte"
|
||||||
import { hydrate } from "svelte"
|
import { hydrate } from "svelte"
|
||||||
|
import { setupI18n } from "./lib/i18n/index"
|
||||||
|
|
||||||
let appContainer = document?.getElementById("appContainer")
|
// Initialize i18n before mounting the app
|
||||||
|
setupI18n().then(() => {
|
||||||
|
let appContainer = document?.getElementById("appContainer")
|
||||||
|
hydrate(App, { target: appContainer })
|
||||||
|
})
|
||||||
|
|
||||||
const app = hydrate(App, { target: appContainer })
|
|
||||||
|
|
||||||
export default app
|
|
||||||
|
|||||||
168
frontend/src/lib/i18n.ts
Normal file
168
frontend/src/lib/i18n.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
import { location } from "./store"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported languages configuration.
|
||||||
|
* Add more languages as needed for your project.
|
||||||
|
*/
|
||||||
|
export const SUPPORTED_LANGUAGES = ["de", "en"] as const
|
||||||
|
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||||
|
|
||||||
|
export const DEFAULT_LANGUAGE: SupportedLanguage = "de"
|
||||||
|
|
||||||
|
export const LANGUAGE_LABELS: Record<SupportedLanguage, string> = {
|
||||||
|
de: "Deutsch",
|
||||||
|
en: "English",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route translations for localized URLs.
|
||||||
|
* Add entries for routes that need translated slugs.
|
||||||
|
* Example: { about: { de: "ueber-uns", en: "about" } }
|
||||||
|
*/
|
||||||
|
export const ROUTE_TRANSLATIONS: Record<string, Record<SupportedLanguage, string>> = {
|
||||||
|
// Add your route translations here:
|
||||||
|
// about: { de: "ueber-uns", en: "about" },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLocalizedRoute = (canonicalRoute: string, lang: SupportedLanguage): string => {
|
||||||
|
const translations = ROUTE_TRANSLATIONS[canonicalRoute]
|
||||||
|
if (translations && translations[lang]) {
|
||||||
|
return translations[lang]
|
||||||
|
}
|
||||||
|
return canonicalRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCanonicalRoute = (localizedSegment: string): string => {
|
||||||
|
for (const [canonical, translations] of Object.entries(ROUTE_TRANSLATIONS)) {
|
||||||
|
for (const translated of Object.values(translations)) {
|
||||||
|
if (translated === localizedSegment) {
|
||||||
|
return canonical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localizedSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the language code from a URL path.
|
||||||
|
* Returns null if no valid language prefix is found.
|
||||||
|
*/
|
||||||
|
export const extractLanguageFromPath = (path: string): SupportedLanguage | null => {
|
||||||
|
const match = path.match(/^\/([a-z]{2})(\/|$)/)
|
||||||
|
if (match && SUPPORTED_LANGUAGES.includes(match[1] as SupportedLanguage)) {
|
||||||
|
return match[1] as SupportedLanguage
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stripLanguageFromPath = (path: string): string => {
|
||||||
|
const lang = extractLanguageFromPath(path)
|
||||||
|
if (lang) {
|
||||||
|
const stripped = path.slice(3)
|
||||||
|
return stripped || "/"
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRoutePath = (fullPath: string): string => {
|
||||||
|
const stripped = stripLanguageFromPath(fullPath)
|
||||||
|
if (stripped === "/" || stripped === "") {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
const segments = stripped.split("/").filter(Boolean)
|
||||||
|
if (segments.length > 0) {
|
||||||
|
const canonicalFirst = getCanonicalRoute(segments[0])
|
||||||
|
if (canonicalFirst !== segments[0]) {
|
||||||
|
segments[0] = canonicalFirst
|
||||||
|
return "/" + segments.join("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived store: current language based on URL.
|
||||||
|
*/
|
||||||
|
export const currentLanguage = derived(location, ($location) => {
|
||||||
|
const path = $location.path || "/"
|
||||||
|
return extractLanguageFromPath(path) || DEFAULT_LANGUAGE
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writable store for the selected language.
|
||||||
|
*/
|
||||||
|
export const selectedLanguage = writable<SupportedLanguage>(DEFAULT_LANGUAGE)
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const initialLang = extractLanguageFromPath(window.location.pathname)
|
||||||
|
if (initialLang) {
|
||||||
|
selectedLanguage.set(initialLang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
location.subscribe(($loc) => {
|
||||||
|
const lang = extractLanguageFromPath($loc.path)
|
||||||
|
if (lang) {
|
||||||
|
selectedLanguage.set(lang)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the localized path for a given route and language.
|
||||||
|
*/
|
||||||
|
export const localizedPath = (path: string, lang?: SupportedLanguage): string => {
|
||||||
|
const language = lang || get(currentLanguage)
|
||||||
|
if (path === "/" || path === "") {
|
||||||
|
return `/${language}`
|
||||||
|
}
|
||||||
|
let cleanPath = stripLanguageFromPath(path)
|
||||||
|
cleanPath = cleanPath.startsWith("/") ? cleanPath : `/${cleanPath}`
|
||||||
|
const segments = cleanPath.split("/").filter(Boolean)
|
||||||
|
if (segments.length > 0) {
|
||||||
|
const canonicalFirst = getCanonicalRoute(segments[0])
|
||||||
|
const localizedFirst = getLocalizedRoute(canonicalFirst, language)
|
||||||
|
segments[0] = localizedFirst
|
||||||
|
}
|
||||||
|
const translatedPath = "/" + segments.join("/")
|
||||||
|
return `/${language}${translatedPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived store for localized href.
|
||||||
|
*/
|
||||||
|
export const localizedHref = (path: string) => {
|
||||||
|
return derived(currentLanguage, ($lang) => localizedPath(path, $lang))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidLanguage = (lang: string): lang is SupportedLanguage => {
|
||||||
|
return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the browser's preferred language, falling back to default.
|
||||||
|
*/
|
||||||
|
export const getBrowserLanguage = (): SupportedLanguage => {
|
||||||
|
if (typeof navigator === "undefined") {
|
||||||
|
return DEFAULT_LANGUAGE
|
||||||
|
}
|
||||||
|
const browserLangs = navigator.languages || [navigator.language]
|
||||||
|
for (const lang of browserLangs) {
|
||||||
|
const code = lang.split("-")[0].toLowerCase()
|
||||||
|
if (isValidLanguage(code)) {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_LANGUAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get URL for switching to a different language.
|
||||||
|
*/
|
||||||
|
export const getLanguageSwitchUrl = (newLang: SupportedLanguage): string => {
|
||||||
|
const currentPath = typeof window !== "undefined" ? window.location.pathname : "/"
|
||||||
|
const canonicalPath = getRoutePath(currentPath)
|
||||||
|
return localizedPath(canonicalPath, newLang)
|
||||||
|
}
|
||||||
75
frontend/src/lib/i18n/index.ts
Normal file
75
frontend/src/lib/i18n/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { register, init, locale, waitLocale, getLocaleFromNavigator } from "svelte-i18n"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import {
|
||||||
|
SUPPORTED_LANGUAGES,
|
||||||
|
DEFAULT_LANGUAGE,
|
||||||
|
extractLanguageFromPath,
|
||||||
|
selectedLanguage,
|
||||||
|
type SupportedLanguage,
|
||||||
|
} from "../i18n"
|
||||||
|
|
||||||
|
// Re-export svelte-i18n utilities for convenience
|
||||||
|
export { _, format, time, date, number, json } from "svelte-i18n"
|
||||||
|
export { locale, isLoading, addMessages } from "svelte-i18n"
|
||||||
|
|
||||||
|
// Register locale files for lazy loading
|
||||||
|
register("de", () => import("./locales/de.json"))
|
||||||
|
register("en", () => import("./locales/en.json"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the initial locale from URL, browser, or fallback.
|
||||||
|
*/
|
||||||
|
function getInitialLocale(url?: string): SupportedLanguage {
|
||||||
|
// 1. Try URL path
|
||||||
|
if (url) {
|
||||||
|
const langFromUrl = extractLanguageFromPath(url)
|
||||||
|
if (langFromUrl) return langFromUrl
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const langFromPath = extractLanguageFromPath(window.location.pathname)
|
||||||
|
if (langFromPath) return langFromPath
|
||||||
|
}
|
||||||
|
// 2. Try browser settings
|
||||||
|
if (typeof navigator !== "undefined") {
|
||||||
|
const browserLocale = getLocaleFromNavigator()
|
||||||
|
if (browserLocale) {
|
||||||
|
const langCode = browserLocale.split("-")[0] as SupportedLanguage
|
||||||
|
if (SUPPORTED_LANGUAGES.includes(langCode)) {
|
||||||
|
return langCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. Fallback
|
||||||
|
return DEFAULT_LANGUAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize i18n for client-side rendering.
|
||||||
|
* Call this before mounting the Svelte app.
|
||||||
|
*/
|
||||||
|
export async function setupI18n(url?: string): Promise<void> {
|
||||||
|
const initialLocale = getInitialLocale(url)
|
||||||
|
|
||||||
|
init({
|
||||||
|
fallbackLocale: DEFAULT_LANGUAGE,
|
||||||
|
initialLocale,
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedLanguage.set(initialLocale)
|
||||||
|
|
||||||
|
// Keep svelte-i18n locale and selectedLanguage store in sync
|
||||||
|
locale.subscribe((newLocale) => {
|
||||||
|
if (newLocale && SUPPORTED_LANGUAGES.includes(newLocale as SupportedLanguage)) {
|
||||||
|
selectedLanguage.set(newLocale as SupportedLanguage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedLanguage.subscribe((newLang) => {
|
||||||
|
const currentLocale = get(locale)
|
||||||
|
if (newLang && newLang !== currentLocale) {
|
||||||
|
locale.set(newLang)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitLocale()
|
||||||
|
}
|
||||||
23
frontend/src/lib/i18n/locales/de.json
Normal file
23
frontend/src/lib/i18n/locales/de.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Startseite",
|
||||||
|
"about": "Über uns",
|
||||||
|
"contact": "Kontakt"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"home": {
|
||||||
|
"title": "Willkommen",
|
||||||
|
"text": "Dies ist der Tibi Svelte Starter. Passe diese Seite an dein Projekt an."
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Über uns",
|
||||||
|
"text": "Hier kannst du Informationen über dein Projekt darstellen."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Kontakt",
|
||||||
|
"text": "Hier kannst du ein Kontaktformular oder Kontaktdaten anzeigen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"welcome": "Willkommen",
|
||||||
|
"language": "Sprache"
|
||||||
|
}
|
||||||
23
frontend/src/lib/i18n/locales/en.json
Normal file
23
frontend/src/lib/i18n/locales/en.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About",
|
||||||
|
"contact": "Contact"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"home": {
|
||||||
|
"title": "Welcome",
|
||||||
|
"text": "This is the Tibi Svelte Starter. Customise this page for your project."
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About",
|
||||||
|
"text": "Use this page to present information about your project."
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Contact",
|
||||||
|
"text": "Use this page to display a contact form or contact details."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"welcome": "Welcome",
|
||||||
|
"language": "Language"
|
||||||
|
}
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
|
import { addMessages, init } from "svelte-i18n"
|
||||||
|
import { DEFAULT_LANGUAGE } from "./lib/i18n"
|
||||||
|
import deLocale from "./lib/i18n/locales/de.json"
|
||||||
|
import enLocale from "./lib/i18n/locales/en.json"
|
||||||
import App from "./App.svelte"
|
import App from "./App.svelte"
|
||||||
|
|
||||||
|
// SSR: load messages synchronously (Babel transforms import → require)
|
||||||
|
addMessages("de", deLocale)
|
||||||
|
addMessages("en", enLocale)
|
||||||
|
|
||||||
|
init({
|
||||||
|
fallbackLocale: DEFAULT_LANGUAGE,
|
||||||
|
initialLocale: DEFAULT_LANGUAGE,
|
||||||
|
})
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -14,12 +14,19 @@
|
|||||||
"build:admin": "node scripts/esbuild-wrapper.js build esbuild.config.admin.js",
|
"build:admin": "node scripts/esbuild-wrapper.js build esbuild.config.admin.js",
|
||||||
"build:legacy": "node scripts/esbuild-wrapper.js build esbuild.config.legacy.js && babel _temp/index.js -o _temp/index.babeled.js && esbuild _temp/index.babeled.js --outfile=frontend/dist/index.es5.js --target=es5 --bundle --minify --sourcemap",
|
"build:legacy": "node scripts/esbuild-wrapper.js build esbuild.config.legacy.js && babel _temp/index.js -o _temp/index.babeled.js && esbuild _temp/index.babeled.js --outfile=frontend/dist/index.es5.js --target=es5 --bundle --minify --sourcemap",
|
||||||
"build:server": "node scripts/esbuild-wrapper.js build esbuild.config.server.js && babel --config-file ./babel.config.server.json _temp/app.server.js -o _temp/app.server.babeled.js && esbuild _temp/app.server.babeled.js --outfile=api/hooks/lib/app.server.js --bundle --sourcemap --platform=node",
|
"build:server": "node scripts/esbuild-wrapper.js build esbuild.config.server.js && babel --config-file ./babel.config.server.json _temp/app.server.js -o _temp/app.server.babeled.js && esbuild _temp/app.server.babeled.js --outfile=api/hooks/lib/app.server.js --bundle --sourcemap --platform=node",
|
||||||
"build:test": "node scripts/esbuild-wrapper.js build esbuild.config.test.js && babel --config-file ./babel.config.test.json _temp/hook.test.js -o _temp/hook.test.babeled.js && esbuild _temp/hook.test.babeled.js --outfile=api/hooks/lib/hook.test.js --target=es5 --bundle --sourcemap --platform=node"
|
"build:test": "node scripts/esbuild-wrapper.js build esbuild.config.test.js && babel --config-file ./babel.config.test.json _temp/hook.test.js -o _temp/hook.test.babeled.js && esbuild _temp/hook.test.babeled.js --outfile=api/hooks/lib/hook.test.js --target=es5 --bundle --sourcemap --platform=node",
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:e2e": "playwright test tests/e2e",
|
||||||
|
"test:api": "playwright test tests/api",
|
||||||
|
"test:visual": "playwright test --project=visual-desktop --project=visual-iphonese --project=visual-ipad",
|
||||||
|
"test:visual:update": "playwright test --project=visual-desktop --project=visual-iphonese --project=visual-ipad --update-snapshots",
|
||||||
|
"test:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.28.6",
|
"@babel/cli": "^7.28.6",
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.29.0",
|
"@babel/preset-env": "^7.29.0",
|
||||||
|
"@playwright/test": "^1.50.0",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tsconfig/svelte": "^5.0.7",
|
"@tsconfig/svelte": "^5.0.7",
|
||||||
"browser-sync": "^3.0.4",
|
"browser-sync": "^3.0.4",
|
||||||
@@ -47,7 +54,8 @@
|
|||||||
"@sentry/cli": "^3.2.0",
|
"@sentry/cli": "^3.2.0",
|
||||||
"@sentry/svelte": "^10.38.0",
|
"@sentry/svelte": "^10.38.0",
|
||||||
"core-js": "3.48.0",
|
"core-js": "3.48.0",
|
||||||
"cryptcha": "ssh://git@gitbase.de:2222/cms/cryptcha.git"
|
"cryptcha": "ssh://git@gitbase.de:2222/cms/cryptcha.git",
|
||||||
|
"svelte-i18n": "^4.0.1"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.7.0"
|
"packageManager": "yarn@4.7.0"
|
||||||
}
|
}
|
||||||
113
playwright.config.ts
Normal file
113
playwright.config.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright configuration for tibi-svelte projects.
|
||||||
|
*
|
||||||
|
* Run against the CODING_URL (externally reachable via HTTPS).
|
||||||
|
* Override with: CODING_URL=https://... yarn test
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traefik skips basic-auth for requests whose User-Agent contains
|
||||||
|
* "Playwright" (see docker-compose-local.yml, priority-100 router).
|
||||||
|
* Append the marker to every device preset so the original UA is preserved
|
||||||
|
* (important for mobile/responsive detection) while bypassing auth.
|
||||||
|
*/
|
||||||
|
function withPlaywrightUA(device: (typeof devices)[string]) {
|
||||||
|
return { ...device, userAgent: `${device.userAgent} Playwright` }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : 16,
|
||||||
|
reporter: [["html", { open: "never" }]],
|
||||||
|
globalSetup: "./tests/global-setup.ts",
|
||||||
|
globalTeardown: "./tests/global-teardown.ts",
|
||||||
|
|
||||||
|
/* ── Visual Regression defaults ──────────────────────────────────── */
|
||||||
|
snapshotPathTemplate: "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}",
|
||||||
|
expect: {
|
||||||
|
toHaveScreenshot: {
|
||||||
|
/* 2 % pixel tolerance – accounts for cross-OS font rendering */
|
||||||
|
maxDiffPixelRatio: 0.02,
|
||||||
|
/* per-pixel colour distance threshold (0 = exact, 1 = any) */
|
||||||
|
threshold: 0.2,
|
||||||
|
/* animation settling time before capture */
|
||||||
|
animations: "disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
use: {
|
||||||
|
/* Read from .env PROJECT_NAME or override via CODING_URL env var */
|
||||||
|
baseURL: process.env.CODING_URL || "https://localhost:3000",
|
||||||
|
headless: true,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
trace: "on",
|
||||||
|
/* BrowserSync keeps a WebSocket open permanently, preventing
|
||||||
|
"networkidle" and "load" from resolving reliably.
|
||||||
|
Default all navigations to "domcontentloaded". */
|
||||||
|
navigationTimeout: 30000,
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
/* ── Desktop E2E ───────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
use: { ...withPlaywrightUA(devices["Desktop Chrome"]) },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── API Tests ─────────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
name: "api",
|
||||||
|
testDir: "./tests/api",
|
||||||
|
use: {
|
||||||
|
...withPlaywrightUA(devices["Desktop Chrome"]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Mobile E2E ────────────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
name: "mobile-iphonese",
|
||||||
|
testDir: "./tests/e2e-mobile",
|
||||||
|
use: {
|
||||||
|
...withPlaywrightUA(devices["iPhone SE"]),
|
||||||
|
defaultBrowserType: "chromium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mobile-ipad",
|
||||||
|
testDir: "./tests/e2e-mobile",
|
||||||
|
use: {
|
||||||
|
...withPlaywrightUA(devices["iPad Mini"]),
|
||||||
|
defaultBrowserType: "chromium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Visual Regression ─────────────────────────────────────────── */
|
||||||
|
{
|
||||||
|
name: "visual-desktop",
|
||||||
|
testDir: "./tests/e2e-visual",
|
||||||
|
use: { ...withPlaywrightUA(devices["Desktop Chrome"]) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "visual-iphonese",
|
||||||
|
testDir: "./tests/e2e-visual",
|
||||||
|
use: {
|
||||||
|
...withPlaywrightUA(devices["iPhone SE"]),
|
||||||
|
defaultBrowserType: "chromium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "visual-ipad",
|
||||||
|
testDir: "./tests/e2e-visual",
|
||||||
|
use: {
|
||||||
|
...withPlaywrightUA(devices["iPad Mini"]),
|
||||||
|
defaultBrowserType: "chromium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
109
scripts/export-visual-screenshots.sh
Executable file
109
scripts/export-visual-screenshots.sh
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# export-visual-screenshots.sh
|
||||||
|
#
|
||||||
|
# Collects visual regression screenshots and test-results (diffs) into a
|
||||||
|
# structured folder for AI review.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/export-visual-screenshots.sh [output-dir]
|
||||||
|
#
|
||||||
|
# Default output: visual-review/
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
OUTPUT_DIR="${1:-$PROJECT_ROOT/visual-review}"
|
||||||
|
|
||||||
|
# Read project name from .env
|
||||||
|
PROJECT_NAME="tibi-project"
|
||||||
|
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||||
|
PROJECT_NAME=$(grep -E '^PROJECT_NAME=' "$PROJECT_ROOT/.env" | cut -d= -f2 || echo "tibi-project")
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR="$PROJECT_ROOT/tests/e2e-visual/__screenshots__"
|
||||||
|
TEST_RESULTS_DIR="$PROJECT_ROOT/test-results"
|
||||||
|
|
||||||
|
rm -rf "$OUTPUT_DIR"
|
||||||
|
mkdir -p "$OUTPUT_DIR/baselines" "$OUTPUT_DIR/diffs"
|
||||||
|
|
||||||
|
echo "📸 Exporting visual regression data to $OUTPUT_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ -d "$SCREENSHOTS_DIR" ]]; then
|
||||||
|
echo " ✓ Copying baseline screenshots..."
|
||||||
|
cp -r "$SCREENSHOTS_DIR"/* "$OUTPUT_DIR/baselines/" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo " ⚠ No baseline screenshots found at $SCREENSHOTS_DIR"
|
||||||
|
echo " Run: yarn test:visual:update to generate baseline screenshots first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIFF_COUNT=0
|
||||||
|
if [[ -d "$TEST_RESULTS_DIR" ]]; then
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
rel="${file#$TEST_RESULTS_DIR/}"
|
||||||
|
dest_dir="$OUTPUT_DIR/diffs/$(dirname "$rel")"
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
cp "$file" "$dest_dir/"
|
||||||
|
((DIFF_COUNT++)) || true
|
||||||
|
done < <(find "$TEST_RESULTS_DIR" \( -name "*-actual.png" -o -name "*-expected.png" -o -name "*-diff.png" \) -print0 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ✓ Generating manifest.json..."
|
||||||
|
|
||||||
|
MANIFEST="$OUTPUT_DIR/manifest.json"
|
||||||
|
echo "{" > "$MANIFEST"
|
||||||
|
echo ' "generated": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'",' >> "$MANIFEST"
|
||||||
|
echo ' "project": "'"$PROJECT_NAME"'",' >> "$MANIFEST"
|
||||||
|
echo ' "viewports": {' >> "$MANIFEST"
|
||||||
|
echo ' "visual-desktop": { "width": 1280, "height": 720, "device": "Desktop Chrome" },' >> "$MANIFEST"
|
||||||
|
echo ' "visual-iphonese": { "width": 375, "height": 667, "device": "iPhone SE" },' >> "$MANIFEST"
|
||||||
|
echo ' "visual-ipad": { "width": 768, "height": 1024, "device": "iPad Mini" }' >> "$MANIFEST"
|
||||||
|
echo ' },' >> "$MANIFEST"
|
||||||
|
|
||||||
|
echo ' "baselines": [' >> "$MANIFEST"
|
||||||
|
FIRST=true
|
||||||
|
if [[ -d "$OUTPUT_DIR/baselines" ]]; then
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
rel="${file#$OUTPUT_DIR/}"
|
||||||
|
project=$(echo "$rel" | cut -d'/' -f2)
|
||||||
|
if [[ "$FIRST" == "true" ]]; then
|
||||||
|
FIRST=false
|
||||||
|
else
|
||||||
|
echo "," >> "$MANIFEST"
|
||||||
|
fi
|
||||||
|
printf ' { "path": "%s", "project": "%s" }' "$rel" "$project" >> "$MANIFEST"
|
||||||
|
done < <(find "$OUTPUT_DIR/baselines" -name "*.png" -print0 2>/dev/null | sort -z)
|
||||||
|
fi
|
||||||
|
echo "" >> "$MANIFEST"
|
||||||
|
echo ' ],' >> "$MANIFEST"
|
||||||
|
|
||||||
|
echo ' "diffs": [' >> "$MANIFEST"
|
||||||
|
FIRST=true
|
||||||
|
if [[ -d "$OUTPUT_DIR/diffs" ]] && [[ $DIFF_COUNT -gt 0 ]]; then
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
rel="${file#$OUTPUT_DIR/}"
|
||||||
|
if [[ "$FIRST" == "true" ]]; then
|
||||||
|
FIRST=false
|
||||||
|
else
|
||||||
|
echo "," >> "$MANIFEST"
|
||||||
|
fi
|
||||||
|
printf ' { "path": "%s" }' "$rel" >> "$MANIFEST"
|
||||||
|
done < <(find "$OUTPUT_DIR/diffs" -name "*.png" -print0 2>/dev/null | sort -z)
|
||||||
|
fi
|
||||||
|
echo "" >> "$MANIFEST"
|
||||||
|
echo ' ],' >> "$MANIFEST"
|
||||||
|
|
||||||
|
echo " \"diffCount\": $DIFF_COUNT" >> "$MANIFEST"
|
||||||
|
echo "}" >> "$MANIFEST"
|
||||||
|
|
||||||
|
BASELINE_COUNT=$(find "$OUTPUT_DIR/baselines" -name "*.png" 2>/dev/null | wc -l)
|
||||||
|
echo ""
|
||||||
|
echo " 📊 Summary:"
|
||||||
|
echo " Baselines: $BASELINE_COUNT screenshots"
|
||||||
|
echo " Diffs: $DIFF_COUNT images (actual/expected/diff)"
|
||||||
|
echo " Manifest: $MANIFEST"
|
||||||
|
echo ""
|
||||||
|
echo " 💡 Review files in $OUTPUT_DIR/"
|
||||||
|
echo " Or attach screenshots to a Copilot Chat for AI review."
|
||||||
5
tailwind.config.js
Normal file
5
tailwind.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./frontend/src/**/*.{html,js,svelte,ts}", "./frontend/spa.html"],
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
57
tests/api/fixtures.ts
Normal file
57
tests/api/fixtures.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { test as base, expect, APIRequestContext } from "@playwright/test"
|
||||||
|
import { ensureTestUser, type TestUserCredentials } from "./helpers/test-user"
|
||||||
|
|
||||||
|
const API_BASE = "/api"
|
||||||
|
|
||||||
|
interface ActionSuccess {
|
||||||
|
success: true
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionError {
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiWorkerFixtures = {
|
||||||
|
testUser: TestUserCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiFixtures = {
|
||||||
|
api: APIRequestContext
|
||||||
|
authedApi: APIRequestContext
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<ApiFixtures, ApiWorkerFixtures>({
|
||||||
|
testUser: [
|
||||||
|
async ({ playwright }, use) => {
|
||||||
|
const baseURL = process.env.CODING_URL || "https://localhost:3000"
|
||||||
|
const user = await ensureTestUser(baseURL)
|
||||||
|
await use(user)
|
||||||
|
},
|
||||||
|
{ scope: "worker" },
|
||||||
|
],
|
||||||
|
|
||||||
|
api: async ({ request }, use) => {
|
||||||
|
await use(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
accessToken: async ({ testUser }, use) => {
|
||||||
|
await use(testUser.accessToken)
|
||||||
|
},
|
||||||
|
|
||||||
|
authedApi: async ({ playwright, baseURL, accessToken }, use) => {
|
||||||
|
const ctx = await playwright.request.newContext({
|
||||||
|
baseURL: baseURL!,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await use(ctx)
|
||||||
|
await ctx.dispose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { expect, API_BASE }
|
||||||
|
export type { ActionSuccess, ActionError }
|
||||||
26
tests/api/health.spec.ts
Normal file
26
tests/api/health.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { test, expect, API_BASE } from "./fixtures"
|
||||||
|
|
||||||
|
test.describe("API Health", () => {
|
||||||
|
test("should respond to API base endpoint", async ({ api }) => {
|
||||||
|
// tibi-server responds to the base API path
|
||||||
|
const res = await api.get(`${API_BASE}/`)
|
||||||
|
// Accept any successful response (200-299), 401 (auth required), or 404 (no root handler)
|
||||||
|
expect([200, 204, 401, 404]).toContain(res.status())
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should respond to SSR collection", async ({ api }) => {
|
||||||
|
// The ssr collection is part of the starter kit
|
||||||
|
const res = await api.get(`${API_BASE}/ssr`)
|
||||||
|
expect(res.status()).toBeLessThan(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should reject invalid action commands", async ({ api }) => {
|
||||||
|
const res = await api.post(`${API_BASE}/action`, {
|
||||||
|
params: { cmd: "nonexistent_action" },
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
// Should return an error, not crash
|
||||||
|
expect(res.status()).toBeGreaterThanOrEqual(400)
|
||||||
|
expect(res.status()).toBeLessThan(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
76
tests/api/helpers/admin-api.ts
Normal file
76
tests/api/helpers/admin-api.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { APIRequestContext, request } from "@playwright/test"
|
||||||
|
import { ADMIN_TOKEN, API_BASE } from "../../fixtures/test-constants"
|
||||||
|
|
||||||
|
let adminContext: APIRequestContext | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a singleton admin API context with the ADMIN_TOKEN.
|
||||||
|
*/
|
||||||
|
async function getAdminContext(baseURL: string): Promise<APIRequestContext> {
|
||||||
|
if (!adminContext) {
|
||||||
|
adminContext = await request.newContext({
|
||||||
|
baseURL,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
Token: ADMIN_TOKEN,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return adminContext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user by ID via admin API.
|
||||||
|
*/
|
||||||
|
export async function deleteUser(baseURL: string, userId: string): Promise<boolean> {
|
||||||
|
const ctx = await getAdminContext(baseURL)
|
||||||
|
const res = await ctx.delete(`${API_BASE}/user/${userId}`)
|
||||||
|
return res.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup test users matching a pattern in their email.
|
||||||
|
*/
|
||||||
|
export async function cleanupTestUsers(baseURL: string, emailPattern: RegExp | string): Promise<number> {
|
||||||
|
const ctx = await getAdminContext(baseURL)
|
||||||
|
const res = await ctx.get(`${API_BASE}/user`)
|
||||||
|
if (!res.ok()) return 0
|
||||||
|
|
||||||
|
const users: { id?: string; _id?: string; email?: string }[] = await res.json()
|
||||||
|
if (!Array.isArray(users)) return 0
|
||||||
|
|
||||||
|
let deleted = 0
|
||||||
|
for (const user of users) {
|
||||||
|
const email = user.email || ""
|
||||||
|
const matches = typeof emailPattern === "string" ? email.includes(emailPattern) : emailPattern.test(email)
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
const userId = user.id || user._id
|
||||||
|
if (userId) {
|
||||||
|
const ok = await deleteUser(baseURL, userId)
|
||||||
|
if (ok) deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup all test data (users and tokens matching @test.example.com).
|
||||||
|
*/
|
||||||
|
export async function cleanupAllTestData(baseURL: string): Promise<{ users: number }> {
|
||||||
|
const testPattern = "@test.example.com"
|
||||||
|
const users = await cleanupTestUsers(baseURL, testPattern)
|
||||||
|
return { users }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the admin API context. Call in globalTeardown.
|
||||||
|
*/
|
||||||
|
export async function disposeAdminApi(): Promise<void> {
|
||||||
|
if (adminContext) {
|
||||||
|
await adminContext.dispose()
|
||||||
|
adminContext = null
|
||||||
|
}
|
||||||
|
}
|
||||||
162
tests/api/helpers/maildev.ts
Normal file
162
tests/api/helpers/maildev.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { APIRequestContext, request } from "@playwright/test"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MailDev API helper for testing email flows.
|
||||||
|
*
|
||||||
|
* Configure via environment variables:
|
||||||
|
* - MAILDEV_URL: MailDev web UI URL (default: https://${PROJECT_NAME}-maildev.code.testversion.online)
|
||||||
|
* - MAILDEV_USER: Basic auth username (default: code)
|
||||||
|
* - MAILDEV_PASS: Basic auth password
|
||||||
|
*/
|
||||||
|
const MAILDEV_URL = process.env.MAILDEV_URL || "http://localhost:1080"
|
||||||
|
const MAILDEV_USER = process.env.MAILDEV_USER || "code"
|
||||||
|
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
|
||||||
|
|
||||||
|
export interface MailDevEmail {
|
||||||
|
id: string
|
||||||
|
subject: string
|
||||||
|
html: string
|
||||||
|
to: { address: string; name: string }[]
|
||||||
|
from: { address: string; name: string }[]
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
read: boolean
|
||||||
|
headers: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
let maildevContext: APIRequestContext | null = null
|
||||||
|
|
||||||
|
async function getContext(): Promise<APIRequestContext> {
|
||||||
|
if (!maildevContext) {
|
||||||
|
const opts: any = {
|
||||||
|
baseURL: MAILDEV_URL,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
}
|
||||||
|
if (MAILDEV_PASS) {
|
||||||
|
opts.httpCredentials = {
|
||||||
|
username: MAILDEV_USER,
|
||||||
|
password: MAILDEV_PASS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maildevContext = await request.newContext(opts)
|
||||||
|
}
|
||||||
|
return maildevContext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all emails from MailDev.
|
||||||
|
*/
|
||||||
|
export async function getAllEmails(): Promise<MailDevEmail[]> {
|
||||||
|
const ctx = await getContext()
|
||||||
|
const res = await ctx.get("/email")
|
||||||
|
if (!res.ok()) {
|
||||||
|
throw new Error(`MailDev API error: ${res.status()} ${res.statusText()}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all emails in MailDev.
|
||||||
|
*/
|
||||||
|
export async function deleteAllEmails(): Promise<void> {
|
||||||
|
const ctx = await getContext()
|
||||||
|
await ctx.delete("/email/all")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific email by ID.
|
||||||
|
*/
|
||||||
|
export async function deleteEmail(id: string): Promise<void> {
|
||||||
|
const ctx = await getContext()
|
||||||
|
await ctx.delete(`/email/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for an email to arrive for a specific recipient.
|
||||||
|
*/
|
||||||
|
export async function waitForEmail(
|
||||||
|
toEmail: string,
|
||||||
|
options: {
|
||||||
|
subject?: string | RegExp
|
||||||
|
timeout?: number
|
||||||
|
pollInterval?: number
|
||||||
|
} = {}
|
||||||
|
): Promise<MailDevEmail> {
|
||||||
|
const { subject, timeout = 5000, pollInterval = 250 } = options
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
const emails = await getAllEmails()
|
||||||
|
const match = emails.find((e) => {
|
||||||
|
const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase())
|
||||||
|
if (!toMatch) return false
|
||||||
|
if (!subject) return true
|
||||||
|
if (typeof subject === "string") return e.subject.includes(subject)
|
||||||
|
return subject.test(e.subject)
|
||||||
|
})
|
||||||
|
if (match) return match
|
||||||
|
await new Promise((r) => setTimeout(r, pollInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timeout: No email to ${toEmail} received within ${timeout}ms`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a 6-digit verification code from an email's HTML body.
|
||||||
|
*/
|
||||||
|
export function extractCode(email: MailDevEmail): string {
|
||||||
|
const match = email.html.match(/>(\d{6})<\//)
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`No 6-digit code found in email "${email.subject}"`)
|
||||||
|
}
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an action and capture the verification code from the resulting email.
|
||||||
|
* Filters out emails that existed before the action was triggered.
|
||||||
|
*/
|
||||||
|
export async function captureCode(
|
||||||
|
toEmail: string,
|
||||||
|
action: () => Promise<void>,
|
||||||
|
options: { subject?: string | RegExp; timeout?: number } = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const existingEmails = await getAllEmails()
|
||||||
|
const existingIds = new Set(
|
||||||
|
existingEmails
|
||||||
|
.filter((e) => e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase()))
|
||||||
|
.map((e) => e.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await action()
|
||||||
|
|
||||||
|
const { timeout = 5000, subject } = options
|
||||||
|
const pollInterval = 250
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
const emails = await getAllEmails()
|
||||||
|
const match = emails.find((e) => {
|
||||||
|
if (existingIds.has(e.id)) return false
|
||||||
|
const toMatch = e.to.some((t) => t.address.toLowerCase() === toEmail.toLowerCase())
|
||||||
|
if (!toMatch) return false
|
||||||
|
if (!subject) return true
|
||||||
|
if (typeof subject === "string") return e.subject.includes(subject)
|
||||||
|
return subject.test(e.subject)
|
||||||
|
})
|
||||||
|
if (match) return extractCode(match)
|
||||||
|
await new Promise((r) => setTimeout(r, pollInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timeout: No new email to ${toEmail} received within ${timeout}ms`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the MailDev API context. Call in globalTeardown.
|
||||||
|
*/
|
||||||
|
export async function disposeMailDev(): Promise<void> {
|
||||||
|
if (maildevContext) {
|
||||||
|
await maildevContext.dispose()
|
||||||
|
maildevContext = null
|
||||||
|
}
|
||||||
|
}
|
||||||
72
tests/api/helpers/test-user.ts
Normal file
72
tests/api/helpers/test-user.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { APIRequestContext, request } from "@playwright/test"
|
||||||
|
import { TEST_USER } from "../../fixtures/test-constants"
|
||||||
|
|
||||||
|
const API_BASE = "/api"
|
||||||
|
|
||||||
|
export interface TestUserCredentials {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
accessToken: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login a user via the tibi action endpoint.
|
||||||
|
* Returns the access token or null on failure.
|
||||||
|
*/
|
||||||
|
export async function loginUser(baseURL: string, email: string, password: string): Promise<string | null> {
|
||||||
|
const ctx = await request.newContext({
|
||||||
|
baseURL,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ctx.post(`${API_BASE}/action`, {
|
||||||
|
params: { cmd: "login" },
|
||||||
|
data: { email, password },
|
||||||
|
})
|
||||||
|
if (!res.ok()) return null
|
||||||
|
const body = await res.json()
|
||||||
|
return body.accessToken || null
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the test user exists and is logged in.
|
||||||
|
* Tries login first; if that fails, logs a warning (registration must be
|
||||||
|
* implemented per project or the user must be seeded via globalSetup).
|
||||||
|
*/
|
||||||
|
export async function ensureTestUser(baseURL: string): Promise<TestUserCredentials> {
|
||||||
|
const email = TEST_USER.email
|
||||||
|
const password = TEST_USER.password
|
||||||
|
|
||||||
|
const token = await loginUser(baseURL, email, password)
|
||||||
|
if (token) {
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
accessToken: token,
|
||||||
|
firstName: TEST_USER.firstName,
|
||||||
|
lastName: TEST_USER.lastName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If login fails, the test user doesn't exist yet.
|
||||||
|
// Implement project-specific registration here, or seed via globalSetup.
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Test user ${email} could not be logged in. ` +
|
||||||
|
`Either implement registration in test-user.ts or seed the user via globalSetup.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return a placeholder – tests requiring auth will fail gracefully
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
accessToken: "",
|
||||||
|
firstName: TEST_USER.firstName,
|
||||||
|
lastName: TEST_USER.lastName,
|
||||||
|
}
|
||||||
|
}
|
||||||
109
tests/e2e-mobile/fixtures.ts
Normal file
109
tests/e2e-mobile/fixtures.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { test as base, expect as pwExpect } from "@playwright/test"
|
||||||
|
|
||||||
|
export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures"
|
||||||
|
import { expect } from "../e2e/fixtures"
|
||||||
|
|
||||||
|
type MobileFixtures = {}
|
||||||
|
|
||||||
|
export const test = base.extend<MobileFixtures>({
|
||||||
|
/**
|
||||||
|
* Override page fixture: BrowserSync domcontentloaded workaround.
|
||||||
|
*/
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
const origGoto = page.goto.bind(page)
|
||||||
|
const origReload = page.reload.bind(page)
|
||||||
|
|
||||||
|
page.goto = ((url: string, opts?: any) =>
|
||||||
|
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
|
||||||
|
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
|
||||||
|
|
||||||
|
await use(page)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the SPA to be ready in mobile viewport.
|
||||||
|
*/
|
||||||
|
export async function waitForSpaReady(page: Page): Promise<string> {
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||||
|
const url = page.url()
|
||||||
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||||
|
return match?.[1] || "de"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a route with language prefix.
|
||||||
|
*/
|
||||||
|
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
|
||||||
|
const url = page.url()
|
||||||
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||||
|
const lang = match?.[1] || "de"
|
||||||
|
const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}`
|
||||||
|
await page.goto(fullPath, { waitUntil: "domcontentloaded" })
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if viewport width is mobile (<768px) */
|
||||||
|
export function isMobileViewport(page: Page): boolean {
|
||||||
|
return (page.viewportSize()?.width ?? 0) < 768
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if viewport width is tablet (768-1023px) */
|
||||||
|
export function isTabletViewport(page: Page): boolean {
|
||||||
|
const w = page.viewportSize()?.width ?? 0
|
||||||
|
return w >= 768 && w < 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if viewport width is below lg breakpoint (<1024px) */
|
||||||
|
export function isBelowLg(page: Page): boolean {
|
||||||
|
return (page.viewportSize()?.width ?? 0) < 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the hamburger/mobile navigation menu.
|
||||||
|
* Finds the first visible button with aria-expanded in the header.
|
||||||
|
*/
|
||||||
|
export async function openHamburgerMenu(page: Page): Promise<void> {
|
||||||
|
const hamburgers = page.locator("header button[aria-expanded]")
|
||||||
|
const count = await hamburgers.count()
|
||||||
|
let clicked = false
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const btn = hamburgers.nth(i)
|
||||||
|
if (await btn.isVisible()) {
|
||||||
|
await btn.click()
|
||||||
|
clicked = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!clicked) {
|
||||||
|
throw new Error("No visible hamburger button found in header")
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await page
|
||||||
|
.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const btn = document.querySelector<HTMLButtonElement>("header button[aria-expanded='true']")
|
||||||
|
return btn !== null
|
||||||
|
},
|
||||||
|
{ timeout: 5000 }
|
||||||
|
)
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the hamburger menu via Escape key.
|
||||||
|
*/
|
||||||
|
export async function closeHamburgerMenuViaEscape(page: Page): Promise<void> {
|
||||||
|
await page.keyboard.press("Escape")
|
||||||
|
await page
|
||||||
|
.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const btn = document.querySelector<HTMLButtonElement>("header button[aria-expanded='true']")
|
||||||
|
return btn === null
|
||||||
|
},
|
||||||
|
{ timeout: 5000 }
|
||||||
|
)
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
28
tests/e2e-mobile/home.mobile.spec.ts
Normal file
28
tests/e2e-mobile/home.mobile.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { test, expect, waitForSpaReady, isMobileViewport } from "./fixtures"
|
||||||
|
|
||||||
|
test.describe("Home Page (Mobile)", () => {
|
||||||
|
test("should load the start page on mobile", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForSpaReady(page)
|
||||||
|
|
||||||
|
expect(isMobileViewport(page) || true).toBeTruthy()
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should have a visible header", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForSpaReady(page)
|
||||||
|
|
||||||
|
const header = page.locator("header")
|
||||||
|
await expect(header).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Uncomment when your project has a hamburger menu:
|
||||||
|
// test("should open hamburger menu", async ({ page }) => {
|
||||||
|
// await page.goto("/de/")
|
||||||
|
// await waitForSpaReady(page)
|
||||||
|
// await openHamburgerMenu(page)
|
||||||
|
// const expandedBtn = page.locator("header button[aria-expanded='true']")
|
||||||
|
// await expect(expandedBtn).toBeVisible()
|
||||||
|
// })
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
113
tests/e2e-visual/fixtures.ts
Normal file
113
tests/e2e-visual/fixtures.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { Page, Locator } from "@playwright/test"
|
||||||
|
import { test as base, expect as pwExpect } from "@playwright/test"
|
||||||
|
|
||||||
|
export { expect, type Page, API_BASE, clickSpaLink } from "../e2e/fixtures"
|
||||||
|
import { expect } from "../e2e/fixtures"
|
||||||
|
|
||||||
|
type VisualFixtures = {}
|
||||||
|
|
||||||
|
export const test = base.extend<VisualFixtures>({
|
||||||
|
/**
|
||||||
|
* Override page fixture: BrowserSync domcontentloaded workaround.
|
||||||
|
*/
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
const origGoto = page.goto.bind(page)
|
||||||
|
const origReload = page.reload.bind(page)
|
||||||
|
|
||||||
|
page.goto = ((url: string, opts?: any) =>
|
||||||
|
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
|
||||||
|
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
|
||||||
|
|
||||||
|
await use(page)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the SPA to be fully rendered and stable for visual comparison.
|
||||||
|
* Waits for skeleton loaders to disappear and CSS to settle.
|
||||||
|
*/
|
||||||
|
export async function waitForVisualReady(page: Page, opts?: { timeout?: number }): Promise<void> {
|
||||||
|
const timeout = opts?.timeout ?? 15000
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout })
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => document.querySelectorAll(".animate-pulse").length === 0, { timeout: 10000 })
|
||||||
|
} catch {
|
||||||
|
// Skeleton loaders may not exist on every page
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(800)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a route with visual readiness wait.
|
||||||
|
*/
|
||||||
|
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
|
||||||
|
const url = page.url()
|
||||||
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||||
|
const lang = match?.[1] || "de"
|
||||||
|
const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}`
|
||||||
|
await page.goto(fullPath, { waitUntil: "domcontentloaded" })
|
||||||
|
await waitForVisualReady(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide dynamic content that would cause screenshot flakiness:
|
||||||
|
* - BrowserSync overlay
|
||||||
|
* - All animations and transitions
|
||||||
|
* - Blinking cursor
|
||||||
|
*/
|
||||||
|
export async function hideDynamicContent(page: Page): Promise<void> {
|
||||||
|
await page.addStyleTag({
|
||||||
|
content: `
|
||||||
|
#__bs_notify__, #__bs_notify__:before, #__bs_notify__:after { display: none !important; }
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0s !important;
|
||||||
|
animation-delay: 0s !important;
|
||||||
|
transition-duration: 0s !important;
|
||||||
|
transition-delay: 0s !important;
|
||||||
|
}
|
||||||
|
* { caret-color: transparent !important; }
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns locators for non-deterministic elements that should be masked
|
||||||
|
* in screenshots (e.g. cart badges, timestamps).
|
||||||
|
* Customize this for your project.
|
||||||
|
*/
|
||||||
|
export function getDynamicMasks(page: Page): Locator[] {
|
||||||
|
return [
|
||||||
|
// Add project-specific dynamic element selectors here:
|
||||||
|
// page.locator('[data-testid="cart-badge"]'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the page for a screenshot: hide dynamic content and scroll to top.
|
||||||
|
*/
|
||||||
|
export async function prepareForScreenshot(page: Page): Promise<void> {
|
||||||
|
await hideDynamicContent(page)
|
||||||
|
await page.evaluate(() => window.scrollTo(0, 0))
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a screenshot and compare against baseline.
|
||||||
|
*/
|
||||||
|
export async function expectScreenshot(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
opts?: {
|
||||||
|
fullPage?: boolean
|
||||||
|
mask?: Locator[]
|
||||||
|
maxDiffPixelRatio?: number
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const masks = [...getDynamicMasks(page), ...(opts?.mask ?? [])]
|
||||||
|
await pwExpect(page).toHaveScreenshot(name, {
|
||||||
|
fullPage: opts?.fullPage ?? false,
|
||||||
|
mask: masks,
|
||||||
|
maxDiffPixelRatio: opts?.maxDiffPixelRatio ?? 0.02,
|
||||||
|
})
|
||||||
|
}
|
||||||
11
tests/e2e-visual/home.visual.spec.ts
Normal file
11
tests/e2e-visual/home.visual.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { test, expect } from "./fixtures"
|
||||||
|
import { waitForVisualReady, prepareForScreenshot, expectScreenshot } from "./fixtures"
|
||||||
|
|
||||||
|
test.describe("Home Page (Visual)", () => {
|
||||||
|
test("homepage screenshot", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForVisualReady(page)
|
||||||
|
await prepareForScreenshot(page)
|
||||||
|
await expectScreenshot(page, "homepage.png", { fullPage: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
139
tests/e2e/fixtures.ts
Normal file
139
tests/e2e/fixtures.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { test as base, expect, type Page } from "@playwright/test"
|
||||||
|
import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user"
|
||||||
|
|
||||||
|
const API_BASE = "/api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared E2E test fixtures.
|
||||||
|
*
|
||||||
|
* Worker-scoped:
|
||||||
|
* - `testUser` – persistent test user (created/reused once per worker)
|
||||||
|
*
|
||||||
|
* Test-scoped:
|
||||||
|
* - `authedPage` – Page with logged-in user (token injection via sessionStorage)
|
||||||
|
* - `accessToken` – raw JWT access token
|
||||||
|
*/
|
||||||
|
type E2eWorkerFixtures = {
|
||||||
|
testUser: TestUserCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
type E2eFixtures = {
|
||||||
|
authedPage: Page
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
|
||||||
|
/**
|
||||||
|
* Override page fixture: BrowserSync keeps a WebSocket open permanently,
|
||||||
|
* preventing "load" and "networkidle" from resolving. We default all
|
||||||
|
* navigation methods to "domcontentloaded".
|
||||||
|
*/
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
const origGoto = page.goto.bind(page)
|
||||||
|
const origReload = page.reload.bind(page)
|
||||||
|
const origGoBack = page.goBack.bind(page)
|
||||||
|
const origGoForward = page.goForward.bind(page)
|
||||||
|
|
||||||
|
page.goto = ((url: string, opts?: any) =>
|
||||||
|
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
|
||||||
|
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
|
||||||
|
page.goBack = ((opts?: any) => origGoBack({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goBack
|
||||||
|
page.goForward = ((opts?: any) =>
|
||||||
|
origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward
|
||||||
|
|
||||||
|
await use(page)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Worker-scoped: create/reuse test user once per worker
|
||||||
|
testUser: [
|
||||||
|
async ({ playwright }, use) => {
|
||||||
|
const baseURL = process.env.CODING_URL || "https://localhost:3000"
|
||||||
|
const user = await ensureTestUser(baseURL)
|
||||||
|
await use(user)
|
||||||
|
},
|
||||||
|
{ scope: "worker" },
|
||||||
|
],
|
||||||
|
|
||||||
|
accessToken: async ({ testUser }, use) => {
|
||||||
|
await use(testUser.accessToken)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test-scoped: Page with logged-in user via sessionStorage token injection
|
||||||
|
authedPage: async ({ page, testUser, baseURL }, use) => {
|
||||||
|
// Navigate to home so domain is set for sessionStorage
|
||||||
|
await page.goto("/", { waitUntil: "domcontentloaded" })
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
|
||||||
|
// Inject auth token into sessionStorage (adapt key names to your app)
|
||||||
|
await page.evaluate(
|
||||||
|
({ token, user }) => {
|
||||||
|
sessionStorage.setItem("auth_token", token)
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"auth_user",
|
||||||
|
JSON.stringify({
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: testUser.accessToken,
|
||||||
|
user: testUser,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reload so the app reads the token from sessionStorage
|
||||||
|
await page.reload({ waitUntil: "domcontentloaded" })
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||||
|
|
||||||
|
await use(page)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the SPA to be ready (appContainer rendered).
|
||||||
|
* Returns the detected language prefix from the URL.
|
||||||
|
*/
|
||||||
|
export async function waitForSpaReady(page: Page): Promise<string> {
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||||
|
const url = page.url()
|
||||||
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||||
|
return match?.[1] || "de"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a route, prepending the current language prefix.
|
||||||
|
*/
|
||||||
|
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
|
||||||
|
const url = page.url()
|
||||||
|
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||||
|
const lang = match?.[1] || "de"
|
||||||
|
const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}`
|
||||||
|
await page.goto(fullPath)
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click an SPA link and verify no full page reload occurred.
|
||||||
|
*/
|
||||||
|
export async function clickSpaLink(page: Page, linkSelector: string): Promise<void> {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
;(window as any).__spa_navigation_marker = true
|
||||||
|
})
|
||||||
|
await page.locator(linkSelector).first().click()
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
const markerExists = await page.evaluate(() => {
|
||||||
|
return (window as any).__spa_navigation_marker === true
|
||||||
|
})
|
||||||
|
if (!markerExists) {
|
||||||
|
throw new Error(`SPA navigation failed: full page reload detected when clicking "${linkSelector}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { expect, API_BASE, type Page }
|
||||||
39
tests/e2e/home.spec.ts
Normal file
39
tests/e2e/home.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { test, expect, waitForSpaReady } from "./fixtures"
|
||||||
|
|
||||||
|
test.describe("Home Page", () => {
|
||||||
|
test("should load the start page", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
const lang = await waitForSpaReady(page)
|
||||||
|
expect(lang).toBe("de")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should have a visible header with navigation", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForSpaReady(page)
|
||||||
|
|
||||||
|
const header = page.locator("header")
|
||||||
|
await expect(header).toBeVisible()
|
||||||
|
|
||||||
|
const nav = header.locator("nav")
|
||||||
|
await expect(nav).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should have language prefix in URL", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForSpaReady(page)
|
||||||
|
expect(page.url()).toContain("/de")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should switch language to English", async ({ page }) => {
|
||||||
|
await page.goto("/de/")
|
||||||
|
await waitForSpaReady(page)
|
||||||
|
|
||||||
|
// Click on English language link
|
||||||
|
const enLink = page.locator('a[href*="/en"]').first()
|
||||||
|
if (await enLink.isVisible()) {
|
||||||
|
await enLink.click()
|
||||||
|
await page.waitForLoadState("domcontentloaded")
|
||||||
|
expect(page.url()).toContain("/en")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
23
tests/fixtures/test-constants.ts
vendored
Normal file
23
tests/fixtures/test-constants.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Zentrale Test-Konstanten für alle Tests.
|
||||||
|
*
|
||||||
|
* Passe diese Werte an dein Projekt an:
|
||||||
|
* - TEST_USER: E-Mail/Passwort für den E2E-Test-User
|
||||||
|
* - ADMIN_TOKEN: Token aus api/config.yml.env
|
||||||
|
* - API_BASE: API-Pfad (Standard: /api)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TEST_USER = {
|
||||||
|
email: "playwright-e2e@test.example.com",
|
||||||
|
password: "PlaywrightTest1",
|
||||||
|
firstName: "Playwright",
|
||||||
|
lastName: "E2E",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "CHANGE_ME"
|
||||||
|
export const API_BASE = "/api"
|
||||||
|
|
||||||
|
export const BASIC_AUTH = {
|
||||||
|
username: process.env.BASIC_AUTH_USER || "web",
|
||||||
|
password: process.env.BASIC_AUTH_PASS || "web",
|
||||||
|
} as const
|
||||||
49
tests/global-setup.ts
Normal file
49
tests/global-setup.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Playwright Global Setup
|
||||||
|
*
|
||||||
|
* Runs once before all tests. Use this to:
|
||||||
|
* - Seed test data via API action hooks
|
||||||
|
* - Create test users
|
||||||
|
* - Verify the dev environment is accessible
|
||||||
|
*
|
||||||
|
* Customize the setup_testing action call for your project,
|
||||||
|
* or remove it if not needed.
|
||||||
|
*/
|
||||||
|
import { request } from "@playwright/test"
|
||||||
|
import { API_BASE } from "./fixtures/test-constants"
|
||||||
|
|
||||||
|
async function globalSetup() {
|
||||||
|
const baseURL = process.env.CODING_URL || "https://localhost:3000"
|
||||||
|
|
||||||
|
// Verify dev environment is reachable
|
||||||
|
const ctx = await request.newContext({
|
||||||
|
baseURL,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
"User-Agent": "Playwright",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ctx.get("/")
|
||||||
|
if (!res.ok()) {
|
||||||
|
console.warn(`⚠️ Dev environment at ${baseURL} returned ${res.status()}`)
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Dev environment reachable at ${baseURL}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment and adapt for project-specific test data seeding:
|
||||||
|
// const seedRes = await ctx.post(`${API_BASE}/action`, {
|
||||||
|
// params: { cmd: "setup_testing" },
|
||||||
|
// data: { scope: "all" },
|
||||||
|
// headers: { Token: ADMIN_TOKEN },
|
||||||
|
// })
|
||||||
|
// if (seedRes.ok()) {
|
||||||
|
// console.log("✅ Test data seeded")
|
||||||
|
// }
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup
|
||||||
37
tests/global-teardown.ts
Normal file
37
tests/global-teardown.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Playwright Global Teardown
|
||||||
|
*
|
||||||
|
* Runs once after all tests. Use this to:
|
||||||
|
* - Clean up test data
|
||||||
|
* - Dispose singleton API contexts
|
||||||
|
*
|
||||||
|
* Customize for your project's cleanup needs.
|
||||||
|
*/
|
||||||
|
import { cleanupAllTestData, disposeAdminApi } from "./api/helpers/admin-api"
|
||||||
|
import { deleteAllEmails, disposeMailDev } from "./api/helpers/maildev"
|
||||||
|
|
||||||
|
async function globalTeardown() {
|
||||||
|
const baseURL = process.env.CODING_URL || "https://localhost:3000"
|
||||||
|
|
||||||
|
// Clean up test users
|
||||||
|
try {
|
||||||
|
const result = await cleanupAllTestData(baseURL)
|
||||||
|
if (result.users > 0) {
|
||||||
|
console.log(`🧹 Cleanup: ${result.users} test users deleted`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("⚠️ Test data cleanup failed:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up MailDev emails (optional)
|
||||||
|
try {
|
||||||
|
await deleteAllEmails()
|
||||||
|
} catch {
|
||||||
|
// MailDev cleanup is optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose singleton API contexts
|
||||||
|
await Promise.all([disposeAdminApi(), disposeMailDev()])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalTeardown
|
||||||
8
tsconfig.test.json
Normal file
8
tsconfig.test.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["tests/**/*", "playwright.config.ts"]
|
||||||
|
}
|
||||||
583
yarn.lock
583
yarn.lock
@@ -1394,6 +1394,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/aix-ppc64@npm:0.19.12"
|
||||||
|
conditions: os=aix & cpu=ppc64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@npm:0.27.3":
|
"@esbuild/aix-ppc64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/aix-ppc64@npm:0.27.3"
|
resolution: "@esbuild/aix-ppc64@npm:0.27.3"
|
||||||
@@ -1401,6 +1408,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/android-arm64@npm:0.19.12"
|
||||||
|
conditions: os=android & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/android-arm64@npm:0.27.3":
|
"@esbuild/android-arm64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/android-arm64@npm:0.27.3"
|
resolution: "@esbuild/android-arm64@npm:0.27.3"
|
||||||
@@ -1408,6 +1422,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/android-arm@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/android-arm@npm:0.19.12"
|
||||||
|
conditions: os=android & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/android-arm@npm:0.27.3":
|
"@esbuild/android-arm@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/android-arm@npm:0.27.3"
|
resolution: "@esbuild/android-arm@npm:0.27.3"
|
||||||
@@ -1415,6 +1436,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/android-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/android-x64@npm:0.19.12"
|
||||||
|
conditions: os=android & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/android-x64@npm:0.27.3":
|
"@esbuild/android-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/android-x64@npm:0.27.3"
|
resolution: "@esbuild/android-x64@npm:0.27.3"
|
||||||
@@ -1422,6 +1450,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/darwin-arm64@npm:0.19.12"
|
||||||
|
conditions: os=darwin & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@npm:0.27.3":
|
"@esbuild/darwin-arm64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/darwin-arm64@npm:0.27.3"
|
resolution: "@esbuild/darwin-arm64@npm:0.27.3"
|
||||||
@@ -1429,6 +1464,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/darwin-x64@npm:0.19.12"
|
||||||
|
conditions: os=darwin & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/darwin-x64@npm:0.27.3":
|
"@esbuild/darwin-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/darwin-x64@npm:0.27.3"
|
resolution: "@esbuild/darwin-x64@npm:0.27.3"
|
||||||
@@ -1436,6 +1478,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/freebsd-arm64@npm:0.19.12"
|
||||||
|
conditions: os=freebsd & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@npm:0.27.3":
|
"@esbuild/freebsd-arm64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/freebsd-arm64@npm:0.27.3"
|
resolution: "@esbuild/freebsd-arm64@npm:0.27.3"
|
||||||
@@ -1443,6 +1492,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/freebsd-x64@npm:0.19.12"
|
||||||
|
conditions: os=freebsd & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@npm:0.27.3":
|
"@esbuild/freebsd-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/freebsd-x64@npm:0.27.3"
|
resolution: "@esbuild/freebsd-x64@npm:0.27.3"
|
||||||
@@ -1450,6 +1506,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-arm64@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-arm64@npm:0.27.3":
|
"@esbuild/linux-arm64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-arm64@npm:0.27.3"
|
resolution: "@esbuild/linux-arm64@npm:0.27.3"
|
||||||
@@ -1457,6 +1520,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-arm@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-arm@npm:0.27.3":
|
"@esbuild/linux-arm@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-arm@npm:0.27.3"
|
resolution: "@esbuild/linux-arm@npm:0.27.3"
|
||||||
@@ -1464,6 +1534,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-ia32@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-ia32@npm:0.27.3":
|
"@esbuild/linux-ia32@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-ia32@npm:0.27.3"
|
resolution: "@esbuild/linux-ia32@npm:0.27.3"
|
||||||
@@ -1471,6 +1548,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-loong64@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=loong64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-loong64@npm:0.27.3":
|
"@esbuild/linux-loong64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-loong64@npm:0.27.3"
|
resolution: "@esbuild/linux-loong64@npm:0.27.3"
|
||||||
@@ -1478,6 +1562,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-mips64el@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=mips64el
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@npm:0.27.3":
|
"@esbuild/linux-mips64el@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-mips64el@npm:0.27.3"
|
resolution: "@esbuild/linux-mips64el@npm:0.27.3"
|
||||||
@@ -1485,6 +1576,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-ppc64@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=ppc64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@npm:0.27.3":
|
"@esbuild/linux-ppc64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-ppc64@npm:0.27.3"
|
resolution: "@esbuild/linux-ppc64@npm:0.27.3"
|
||||||
@@ -1492,6 +1590,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-riscv64@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=riscv64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@npm:0.27.3":
|
"@esbuild/linux-riscv64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-riscv64@npm:0.27.3"
|
resolution: "@esbuild/linux-riscv64@npm:0.27.3"
|
||||||
@@ -1499,6 +1604,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-s390x@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=s390x
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-s390x@npm:0.27.3":
|
"@esbuild/linux-s390x@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-s390x@npm:0.27.3"
|
resolution: "@esbuild/linux-s390x@npm:0.27.3"
|
||||||
@@ -1506,6 +1618,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/linux-x64@npm:0.19.12"
|
||||||
|
conditions: os=linux & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/linux-x64@npm:0.27.3":
|
"@esbuild/linux-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/linux-x64@npm:0.27.3"
|
resolution: "@esbuild/linux-x64@npm:0.27.3"
|
||||||
@@ -1520,6 +1639,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/netbsd-x64@npm:0.19.12"
|
||||||
|
conditions: os=netbsd & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@npm:0.27.3":
|
"@esbuild/netbsd-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/netbsd-x64@npm:0.27.3"
|
resolution: "@esbuild/netbsd-x64@npm:0.27.3"
|
||||||
@@ -1534,6 +1660,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/openbsd-x64@npm:0.19.12"
|
||||||
|
conditions: os=openbsd & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@npm:0.27.3":
|
"@esbuild/openbsd-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/openbsd-x64@npm:0.27.3"
|
resolution: "@esbuild/openbsd-x64@npm:0.27.3"
|
||||||
@@ -1548,6 +1681,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/sunos-x64@npm:0.19.12"
|
||||||
|
conditions: os=sunos & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/sunos-x64@npm:0.27.3":
|
"@esbuild/sunos-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/sunos-x64@npm:0.27.3"
|
resolution: "@esbuild/sunos-x64@npm:0.27.3"
|
||||||
@@ -1555,6 +1695,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/win32-arm64@npm:0.19.12"
|
||||||
|
conditions: os=win32 & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/win32-arm64@npm:0.27.3":
|
"@esbuild/win32-arm64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/win32-arm64@npm:0.27.3"
|
resolution: "@esbuild/win32-arm64@npm:0.27.3"
|
||||||
@@ -1562,6 +1709,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/win32-ia32@npm:0.19.12"
|
||||||
|
conditions: os=win32 & cpu=ia32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/win32-ia32@npm:0.27.3":
|
"@esbuild/win32-ia32@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/win32-ia32@npm:0.27.3"
|
resolution: "@esbuild/win32-ia32@npm:0.27.3"
|
||||||
@@ -1569,6 +1723,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@npm:0.19.12":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "@esbuild/win32-x64@npm:0.19.12"
|
||||||
|
conditions: os=win32 & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@esbuild/win32-x64@npm:0.27.3":
|
"@esbuild/win32-x64@npm:0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "@esbuild/win32-x64@npm:0.27.3"
|
resolution: "@esbuild/win32-x64@npm:0.27.3"
|
||||||
@@ -1576,6 +1737,57 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@formatjs/ecma402-abstract@npm:2.3.6":
|
||||||
|
version: 2.3.6
|
||||||
|
resolution: "@formatjs/ecma402-abstract@npm:2.3.6"
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/fast-memoize": "npm:2.2.7"
|
||||||
|
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||||
|
decimal.js: "npm:^10.4.3"
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@formatjs/fast-memoize@npm:2.2.7":
|
||||||
|
version: 2.2.7
|
||||||
|
resolution: "@formatjs/fast-memoize@npm:2.2.7"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/e7e6efc677d63a13d99a854305db471b69f64cbfebdcb6dbe507dab9aa7eaae482ca5de86f343c856ca0a2c8f251672bd1f37c572ce14af602c0287378097d43
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@formatjs/icu-messageformat-parser@npm:2.11.4":
|
||||||
|
version: 2.11.4
|
||||||
|
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.4"
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract": "npm:2.3.6"
|
||||||
|
"@formatjs/icu-skeleton-parser": "npm:1.8.16"
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/2acb100c06c2ade666d72787fb9f9795b1ace41e8e73bfadc2b1a7b8562e81f655e484f0f33d8c39473aa17bf0ad96fb2228871806a9b3dc4f5f876754a0de3a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@formatjs/icu-skeleton-parser@npm:1.8.16":
|
||||||
|
version: 1.8.16
|
||||||
|
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.16"
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract": "npm:2.3.6"
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/428001e5bed81889b276a2356a1393157af91dc59220b765a1a132f6407ac5832b7ac6ae9737674ac38e44035295c0c1c310b2630f383f2b5779ea90bf2849e6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@formatjs/intl-localematcher@npm:0.6.2":
|
||||||
|
version: 0.6.2
|
||||||
|
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@isaacs/cliui@npm:^8.0.2":
|
"@isaacs/cliui@npm:^8.0.2":
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
resolution: "@isaacs/cliui@npm:8.0.2"
|
resolution: "@isaacs/cliui@npm:8.0.2"
|
||||||
@@ -1725,6 +1937,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@playwright/test@npm:^1.50.0":
|
||||||
|
version: 1.58.2
|
||||||
|
resolution: "@playwright/test@npm:1.58.2"
|
||||||
|
dependencies:
|
||||||
|
playwright: "npm:1.58.2"
|
||||||
|
bin:
|
||||||
|
playwright: cli.js
|
||||||
|
checksum: 10/58bf90139280a0235eeeb6049e9fb4db6425e98be1bf0cc17913b068eef616cf67be57bfb36dc4cb56bcf116f498ffd0225c4916e85db404b343ea6c5efdae13
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sentry-internal/browser-utils@npm:10.38.0":
|
"@sentry-internal/browser-utils@npm:10.38.0":
|
||||||
version: 10.38.0
|
version: 10.38.0
|
||||||
resolution: "@sentry-internal/browser-utils@npm:10.38.0"
|
resolution: "@sentry-internal/browser-utils@npm:10.38.0"
|
||||||
@@ -2529,6 +2752,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cli-color@npm:^2.0.3":
|
||||||
|
version: 2.0.4
|
||||||
|
resolution: "cli-color@npm:2.0.4"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:^1.0.1"
|
||||||
|
es5-ext: "npm:^0.10.64"
|
||||||
|
es6-iterator: "npm:^2.0.3"
|
||||||
|
memoizee: "npm:^0.4.15"
|
||||||
|
timers-ext: "npm:^0.1.7"
|
||||||
|
checksum: 10/6706fbb98f5db62c47deaba7116a1e37470c936dc861b84a180b5ce1a58fbf50ae6582b30a65e4b30ddb39e0469d3bac6851a9d925ded02b7e0c1c00858ef14b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"cliui@npm:^8.0.1":
|
"cliui@npm:^8.0.1":
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
resolution: "cliui@npm:8.0.1"
|
resolution: "cliui@npm:8.0.1"
|
||||||
@@ -2688,6 +2924,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "d@npm:1.0.2"
|
||||||
|
dependencies:
|
||||||
|
es5-ext: "npm:^0.10.64"
|
||||||
|
type: "npm:^2.7.2"
|
||||||
|
checksum: 10/a3f45ef964622f683f6a1cb9b8dcbd75ce490cd2f4ac9794099db3d8f0e2814d412d84cd3fe522e58feb1f273117bb480f29c5381f6225f0abca82517caaa77a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"data-uri-to-buffer@npm:^4.0.0":
|
"data-uri-to-buffer@npm:^4.0.0":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "data-uri-to-buffer@npm:4.0.1"
|
resolution: "data-uri-to-buffer@npm:4.0.1"
|
||||||
@@ -2740,6 +2986,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"decimal.js@npm:^10.4.3":
|
||||||
|
version: 10.6.0
|
||||||
|
resolution: "decimal.js@npm:10.6.0"
|
||||||
|
checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"deepmerge@npm:^4.2.2":
|
||||||
|
version: 4.3.1
|
||||||
|
resolution: "deepmerge@npm:4.3.1"
|
||||||
|
checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"depd@npm:2.0.0, depd@npm:~2.0.0":
|
"depd@npm:2.0.0, depd@npm:~2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "depd@npm:2.0.0"
|
resolution: "depd@npm:2.0.0"
|
||||||
@@ -2939,6 +3199,51 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2":
|
||||||
|
version: 0.10.64
|
||||||
|
resolution: "es5-ext@npm:0.10.64"
|
||||||
|
dependencies:
|
||||||
|
es6-iterator: "npm:^2.0.3"
|
||||||
|
es6-symbol: "npm:^3.1.3"
|
||||||
|
esniff: "npm:^2.0.1"
|
||||||
|
next-tick: "npm:^1.1.0"
|
||||||
|
checksum: 10/0c5d8657708b1695ddc4b06f4e0b9fbdda4d2fe46d037b6bedb49a7d1931e542ec9eecf4824d59e1d357e93229deab014bb4b86485db2d41b1d68e54439689ce
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"es6-iterator@npm:^2.0.3":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "es6-iterator@npm:2.0.3"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:1"
|
||||||
|
es5-ext: "npm:^0.10.35"
|
||||||
|
es6-symbol: "npm:^3.1.1"
|
||||||
|
checksum: 10/dbadecf3d0e467692815c2b438dfa99e5a97cbbecf4a58720adcb467a04220e0e36282399ba297911fd472c50ae4158fffba7ed0b7d4273fe322b69d03f9e3a5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3":
|
||||||
|
version: 3.1.4
|
||||||
|
resolution: "es6-symbol@npm:3.1.4"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:^1.0.2"
|
||||||
|
ext: "npm:^1.7.0"
|
||||||
|
checksum: 10/3743119fe61f89e2f049a6ce52bd82fab5f65d13e2faa72453b73f95c15292c3cb9bdf3747940d504517e675e45fd375554c6b5d35d2bcbefd35f5489ecba546
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"es6-weak-map@npm:^2.0.3":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "es6-weak-map@npm:2.0.3"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:1"
|
||||||
|
es5-ext: "npm:^0.10.46"
|
||||||
|
es6-iterator: "npm:^2.0.3"
|
||||||
|
es6-symbol: "npm:^3.1.1"
|
||||||
|
checksum: 10/5958a321cf8dfadc82b79eeaa57dc855893a4afd062b4ef5c9ded0010d3932099311272965c3d3fdd3c85df1d7236013a570e704fa6c1f159bbf979c203dd3a3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"esbuild-postcss@npm:^0.0.4":
|
"esbuild-postcss@npm:^0.0.4":
|
||||||
version: 0.0.4
|
version: 0.0.4
|
||||||
resolution: "esbuild-postcss@npm:0.0.4"
|
resolution: "esbuild-postcss@npm:0.0.4"
|
||||||
@@ -2963,6 +3268,86 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"esbuild@npm:^0.19.2":
|
||||||
|
version: 0.19.12
|
||||||
|
resolution: "esbuild@npm:0.19.12"
|
||||||
|
dependencies:
|
||||||
|
"@esbuild/aix-ppc64": "npm:0.19.12"
|
||||||
|
"@esbuild/android-arm": "npm:0.19.12"
|
||||||
|
"@esbuild/android-arm64": "npm:0.19.12"
|
||||||
|
"@esbuild/android-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/darwin-arm64": "npm:0.19.12"
|
||||||
|
"@esbuild/darwin-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/freebsd-arm64": "npm:0.19.12"
|
||||||
|
"@esbuild/freebsd-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-arm": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-arm64": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-ia32": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-loong64": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-mips64el": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-ppc64": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-riscv64": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-s390x": "npm:0.19.12"
|
||||||
|
"@esbuild/linux-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/netbsd-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/openbsd-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/sunos-x64": "npm:0.19.12"
|
||||||
|
"@esbuild/win32-arm64": "npm:0.19.12"
|
||||||
|
"@esbuild/win32-ia32": "npm:0.19.12"
|
||||||
|
"@esbuild/win32-x64": "npm:0.19.12"
|
||||||
|
dependenciesMeta:
|
||||||
|
"@esbuild/aix-ppc64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/android-arm":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/android-arm64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/android-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/darwin-arm64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/darwin-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/freebsd-arm64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/freebsd-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-arm":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-arm64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-ia32":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-loong64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-mips64el":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-ppc64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-riscv64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-s390x":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/linux-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/netbsd-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/openbsd-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/sunos-x64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/win32-arm64":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/win32-ia32":
|
||||||
|
optional: true
|
||||||
|
"@esbuild/win32-x64":
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
esbuild: bin/esbuild
|
||||||
|
checksum: 10/861fa8eb2428e8d6521a4b7c7930139e3f45e8d51a86985cc29408172a41f6b18df7b3401e7e5e2d528cdf83742da601ddfdc77043ddc4f1c715a8ddb2d8a255
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"esbuild@npm:^0.27.3":
|
"esbuild@npm:^0.27.3":
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
resolution: "esbuild@npm:0.27.3"
|
resolution: "esbuild@npm:0.27.3"
|
||||||
@@ -3073,6 +3458,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"esniff@npm:^2.0.1":
|
||||||
|
version: 2.0.1
|
||||||
|
resolution: "esniff@npm:2.0.1"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:^1.0.1"
|
||||||
|
es5-ext: "npm:^0.10.62"
|
||||||
|
event-emitter: "npm:^0.3.5"
|
||||||
|
type: "npm:^2.7.2"
|
||||||
|
checksum: 10/f6a2abd2f8c5fe57c5fcf53e5407c278023313d0f6c3a92688e7122ab9ac233029fd424508a196ae5bc561aa1f67d23f4e2435b1a0d378030f476596129056ac
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"esrap@npm:^2.2.2":
|
"esrap@npm:^2.2.2":
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
resolution: "esrap@npm:2.2.3"
|
resolution: "esrap@npm:2.2.3"
|
||||||
@@ -3082,6 +3479,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"estree-walker@npm:^2":
|
||||||
|
version: 2.0.2
|
||||||
|
resolution: "estree-walker@npm:2.0.2"
|
||||||
|
checksum: 10/b02109c5d46bc2ed47de4990eef770f7457b1159a229f0999a09224d2b85ffeed2d7679cffcff90aeb4448e94b0168feb5265b209cdec29aad50a3d6e93d21e2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"esutils@npm:^2.0.2":
|
"esutils@npm:^2.0.2":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "esutils@npm:2.0.3"
|
resolution: "esutils@npm:2.0.3"
|
||||||
@@ -3096,6 +3500,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"event-emitter@npm:^0.3.5":
|
||||||
|
version: 0.3.5
|
||||||
|
resolution: "event-emitter@npm:0.3.5"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:1"
|
||||||
|
es5-ext: "npm:~0.10.14"
|
||||||
|
checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"eventemitter3@npm:^4.0.0":
|
"eventemitter3@npm:^4.0.0":
|
||||||
version: 4.0.7
|
version: 4.0.7
|
||||||
resolution: "eventemitter3@npm:4.0.7"
|
resolution: "eventemitter3@npm:4.0.7"
|
||||||
@@ -3110,6 +3524,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ext@npm:^1.7.0":
|
||||||
|
version: 1.7.0
|
||||||
|
resolution: "ext@npm:1.7.0"
|
||||||
|
dependencies:
|
||||||
|
type: "npm:^2.7.2"
|
||||||
|
checksum: 10/666a135980b002df0e75c8ac6c389140cdc59ac953db62770479ee2856d58ce69d2f845e5f2586716350b725400f6945e51e9159573158c39f369984c72dcd84
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fdir@npm:^6.2.0":
|
"fdir@npm:^6.2.0":
|
||||||
version: 6.4.3
|
version: 6.4.3
|
||||||
resolution: "fdir@npm:6.4.3"
|
resolution: "fdir@npm:6.4.3"
|
||||||
@@ -3226,6 +3649,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fsevents@npm:2.3.2":
|
||||||
|
version: 2.3.2
|
||||||
|
resolution: "fsevents@npm:2.3.2"
|
||||||
|
dependencies:
|
||||||
|
node-gyp: "npm:latest"
|
||||||
|
checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
|
||||||
|
conditions: os=darwin
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@npm:~2.3.2":
|
"fsevents@npm:~2.3.2":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@npm:2.3.3"
|
resolution: "fsevents@npm:2.3.3"
|
||||||
@@ -3236,6 +3669,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
|
||||||
|
version: 2.3.2
|
||||||
|
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
||||||
|
dependencies:
|
||||||
|
node-gyp: "npm:latest"
|
||||||
|
conditions: os=darwin
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
|
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||||
@@ -3312,6 +3754,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"globalyzer@npm:0.1.0":
|
||||||
|
version: 0.1.0
|
||||||
|
resolution: "globalyzer@npm:0.1.0"
|
||||||
|
checksum: 10/419a0f95ba542534fac0842964d31b3dc2936a479b2b1a8a62bad7e8b61054faa9b0a06ad9f2e12593396b9b2621cac93358d9b3071d33723fb1778608d358a1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"globrex@npm:^0.1.2":
|
||||||
|
version: 0.1.2
|
||||||
|
resolution: "globrex@npm:0.1.2"
|
||||||
|
checksum: 10/81ce62ee6f800d823d6b7da7687f841676d60ee8f51f934ddd862e4057316d26665c4edc0358d4340a923ac00a514f8b67c787e28fe693aae16350f4e60d55e9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
||||||
version: 4.2.11
|
version: 4.2.11
|
||||||
resolution: "graceful-fs@npm:4.2.11"
|
resolution: "graceful-fs@npm:4.2.11"
|
||||||
@@ -3477,6 +3933,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"intl-messageformat@npm:^10.5.3":
|
||||||
|
version: 10.7.18
|
||||||
|
resolution: "intl-messageformat@npm:10.7.18"
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract": "npm:2.3.6"
|
||||||
|
"@formatjs/fast-memoize": "npm:2.2.7"
|
||||||
|
"@formatjs/icu-messageformat-parser": "npm:2.11.4"
|
||||||
|
tslib: "npm:^2.8.0"
|
||||||
|
checksum: 10/96650d673912763d21bbfa14b50749b992d45f1901092a020e3155961e3c70f4644dd1731c3ecb1207a1eb94d84bedf4c34b1ac8127c29ad6b015b6a2a4045cb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ip-address@npm:^9.0.5":
|
"ip-address@npm:^9.0.5":
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
resolution: "ip-address@npm:9.0.5"
|
resolution: "ip-address@npm:9.0.5"
|
||||||
@@ -3551,6 +4019,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"is-promise@npm:^2.2.2":
|
||||||
|
version: 2.2.2
|
||||||
|
resolution: "is-promise@npm:2.2.2"
|
||||||
|
checksum: 10/18bf7d1c59953e0ad82a1ed963fb3dc0d135c8f299a14f89a17af312fc918373136e56028e8831700e1933519630cc2fd4179a777030330fde20d34e96f40c78
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"is-reference@npm:^3.0.3":
|
"is-reference@npm:^3.0.3":
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
resolution: "is-reference@npm:3.0.3"
|
resolution: "is-reference@npm:3.0.3"
|
||||||
@@ -3883,6 +4358,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lru-queue@npm:^0.1.0":
|
||||||
|
version: 0.1.0
|
||||||
|
resolution: "lru-queue@npm:0.1.0"
|
||||||
|
dependencies:
|
||||||
|
es5-ext: "npm:~0.10.2"
|
||||||
|
checksum: 10/55b08ee3a7dbefb7d8ee2d14e0a97c69a887f78bddd9e28a687a1944b57e09513d4b401db515279e8829d52331df12a767f3ed27ca67c3322c723cc25c06403f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"magic-string@npm:^0.30.0, magic-string@npm:^0.30.11":
|
"magic-string@npm:^0.30.0, magic-string@npm:^0.30.11":
|
||||||
version: 0.30.17
|
version: 0.30.17
|
||||||
resolution: "magic-string@npm:0.30.17"
|
resolution: "magic-string@npm:0.30.17"
|
||||||
@@ -3937,6 +4421,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"memoizee@npm:^0.4.15":
|
||||||
|
version: 0.4.17
|
||||||
|
resolution: "memoizee@npm:0.4.17"
|
||||||
|
dependencies:
|
||||||
|
d: "npm:^1.0.2"
|
||||||
|
es5-ext: "npm:^0.10.64"
|
||||||
|
es6-weak-map: "npm:^2.0.3"
|
||||||
|
event-emitter: "npm:^0.3.5"
|
||||||
|
is-promise: "npm:^2.2.2"
|
||||||
|
lru-queue: "npm:^0.1.0"
|
||||||
|
next-tick: "npm:^1.1.0"
|
||||||
|
timers-ext: "npm:^0.1.7"
|
||||||
|
checksum: 10/b7abda74d1057878f3570c45995f24da8a4f8636e0e9a7c29a6709be2314bf40c7d78e3be93c0b1660ba419de5740fa5e447c400ab5df407ffbd236421066380
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"micromatch@npm:^4.0.8":
|
"micromatch@npm:^4.0.8":
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
resolution: "micromatch@npm:4.0.8"
|
resolution: "micromatch@npm:4.0.8"
|
||||||
@@ -4152,6 +4652,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"next-tick@npm:^1.1.0":
|
||||||
|
version: 1.1.0
|
||||||
|
resolution: "next-tick@npm:1.1.0"
|
||||||
|
checksum: 10/83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-domexception@npm:^1.0.0":
|
"node-domexception@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "node-domexception@npm:1.0.0"
|
resolution: "node-domexception@npm:1.0.0"
|
||||||
@@ -4352,6 +4859,30 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"playwright-core@npm:1.58.2":
|
||||||
|
version: 1.58.2
|
||||||
|
resolution: "playwright-core@npm:1.58.2"
|
||||||
|
bin:
|
||||||
|
playwright-core: cli.js
|
||||||
|
checksum: 10/8a98fcf122167e8703d525db2252de0e3da4ab9110ab6ea9951247e52d846310eb25ea2c805e1b7ccb54b4010c44e5adc3a76aae6da02f34324ccc3e76683bb1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"playwright@npm:1.58.2":
|
||||||
|
version: 1.58.2
|
||||||
|
resolution: "playwright@npm:1.58.2"
|
||||||
|
dependencies:
|
||||||
|
fsevents: "npm:2.3.2"
|
||||||
|
playwright-core: "npm:1.58.2"
|
||||||
|
dependenciesMeta:
|
||||||
|
fsevents:
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
playwright: cli.js
|
||||||
|
checksum: 10/d89d6c8a32388911b9aff9ee0f1a90076219f15c804f2b287db048b9e9cde182aea3131fac1959051d25189ed4218ec4272b137c83cd7f9cd24781cbc77edd86
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"portscanner@npm:2.2.0":
|
"portscanner@npm:2.2.0":
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
resolution: "portscanner@npm:2.2.0"
|
resolution: "portscanner@npm:2.2.0"
|
||||||
@@ -4647,7 +5178,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"sade@npm:^1.7.4":
|
"sade@npm:^1.7.4, sade@npm:^1.8.1":
|
||||||
version: 1.8.1
|
version: 1.8.1
|
||||||
resolution: "sade@npm:1.8.1"
|
resolution: "sade@npm:1.8.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5036,6 +5567,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"svelte-i18n@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "svelte-i18n@npm:4.0.1"
|
||||||
|
dependencies:
|
||||||
|
cli-color: "npm:^2.0.3"
|
||||||
|
deepmerge: "npm:^4.2.2"
|
||||||
|
esbuild: "npm:^0.19.2"
|
||||||
|
estree-walker: "npm:^2"
|
||||||
|
intl-messageformat: "npm:^10.5.3"
|
||||||
|
sade: "npm:^1.8.1"
|
||||||
|
tiny-glob: "npm:^0.2.9"
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3 || ^4 || ^5
|
||||||
|
bin:
|
||||||
|
svelte-i18n: dist/cli.js
|
||||||
|
checksum: 10/683f921429b62b2cf53bcb56d5744cdd1e3ce2448a7be6e1e50c78f06ae895d434a8fa9f485fbbe5aad7dab771a6678600d7dc5a45d05a4a92348ffc70476ff2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"svelte-preprocess-esbuild@npm:^3.0.1":
|
"svelte-preprocess-esbuild@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "svelte-preprocess-esbuild@npm:3.0.1"
|
resolution: "svelte-preprocess-esbuild@npm:3.0.1"
|
||||||
@@ -5144,6 +5694,7 @@ __metadata:
|
|||||||
"@babel/cli": "npm:^7.28.6"
|
"@babel/cli": "npm:^7.28.6"
|
||||||
"@babel/core": "npm:^7.29.0"
|
"@babel/core": "npm:^7.29.0"
|
||||||
"@babel/preset-env": "npm:^7.29.0"
|
"@babel/preset-env": "npm:^7.29.0"
|
||||||
|
"@playwright/test": "npm:^1.50.0"
|
||||||
"@sentry/cli": "npm:^3.2.0"
|
"@sentry/cli": "npm:^3.2.0"
|
||||||
"@sentry/svelte": "npm:^10.38.0"
|
"@sentry/svelte": "npm:^10.38.0"
|
||||||
"@tailwindcss/postcss": "npm:^4.1.18"
|
"@tailwindcss/postcss": "npm:^4.1.18"
|
||||||
@@ -5165,6 +5716,7 @@ __metadata:
|
|||||||
prettier-plugin-svelte: "npm:^3.4.1"
|
prettier-plugin-svelte: "npm:^3.4.1"
|
||||||
svelte: "npm:^5.50.1"
|
svelte: "npm:^5.50.1"
|
||||||
svelte-check: "npm:^4.3.6"
|
svelte-check: "npm:^4.3.6"
|
||||||
|
svelte-i18n: "npm:^4.0.1"
|
||||||
svelte-preprocess: "npm:^6.0.3"
|
svelte-preprocess: "npm:^6.0.3"
|
||||||
svelte-preprocess-esbuild: "npm:^3.0.1"
|
svelte-preprocess-esbuild: "npm:^3.0.1"
|
||||||
tailwindcss: "npm:^4.1.18"
|
tailwindcss: "npm:^4.1.18"
|
||||||
@@ -5173,6 +5725,26 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"timers-ext@npm:^0.1.7":
|
||||||
|
version: 0.1.8
|
||||||
|
resolution: "timers-ext@npm:0.1.8"
|
||||||
|
dependencies:
|
||||||
|
es5-ext: "npm:^0.10.64"
|
||||||
|
next-tick: "npm:^1.1.0"
|
||||||
|
checksum: 10/8abd168c57029e25d1fa4b7e101b053e261479e43ba4a32ead76e601e7037f74f850c311e22dc3dbb50dc211b34b092e0a349274d3997a493295e9ec725e6395
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"tiny-glob@npm:^0.2.9":
|
||||||
|
version: 0.2.9
|
||||||
|
resolution: "tiny-glob@npm:0.2.9"
|
||||||
|
dependencies:
|
||||||
|
globalyzer: "npm:0.1.0"
|
||||||
|
globrex: "npm:^0.1.2"
|
||||||
|
checksum: 10/5fb773747f6a8fcae4b8884642901fa7b884879695186c422eb24b2213dfe90645f34225ced586329b3080d850472ea938646ab1c8b3a2989f9fa038fef8eee3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"to-regex-range@npm:^5.0.1":
|
"to-regex-range@npm:^5.0.1":
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
resolution: "to-regex-range@npm:5.0.1"
|
resolution: "to-regex-range@npm:5.0.1"
|
||||||
@@ -5189,13 +5761,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1":
|
"tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
resolution: "tslib@npm:2.8.1"
|
resolution: "tslib@npm:2.8.1"
|
||||||
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
|
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"type@npm:^2.7.2":
|
||||||
|
version: 2.7.3
|
||||||
|
resolution: "type@npm:2.7.3"
|
||||||
|
checksum: 10/82e99e7795b3de3ecfe685680685e79a77aea515fad9f60b7c55fbf6d43a5c360b1e6e9443354ec8906b38cdf5325829c69f094cb7cd2a1238e85bef9026dc04
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"typescript@npm:^5.9.3":
|
"typescript@npm:^5.9.3":
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
resolution: "typescript@npm:5.9.3"
|
resolution: "typescript@npm:5.9.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user