From 67e1e0a74fb64a6a8268d008403e2b875a0b08ea Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 31 Aug 2026 11:52:27 +0700 Subject: [PATCH] feat: integrate Playwright for E2E testing and enhance testing framework - Added Playwright for end-to-end testing in the `apps/web` module, including a new `login.spec.ts` for testing login functionality. - Updated package.json to include Playwright dependencies and new test commands for E2E testing. - Enhanced .gitignore to exclude Playwright test results and reports. - Modified existing documentation to reflect the integration of Playwright and updated testing guidelines. - Refactored test commands to streamline the testing process, including a dedicated command for E2E tests. These changes improve the testing framework by providing robust E2E testing capabilities, enhancing the reliability and quality of the application. --- .agents/skills/archify/SKILL.md | 15 +-- .../project-guidelines-example/SKILL.md | 6 +- .agents/skills/tdd-workflow/SKILL.md | 9 +- .cursor/agents/e2e-runner.md | 55 ++++------- .cursor/agents/tdd-guide.md | 2 +- .cursor/commands/e2e.md | 22 ++--- .cursor/rules/agents.mdc | 2 +- .cursor/rules/frontend-overview.mdc | 2 +- .cursor/rules/testing.mdc | 10 +- .gitignore | 8 +- apps/docs-dev/src/apps/web/SALES_WORKFLOW.md | 56 ++++++------ apps/docs-dev/src/apps/web/index.md | 12 +-- apps/docs-dev/src/setup.md | 1 + apps/web/e2e/fixtures/credentials.ts | 4 + apps/web/e2e/fixtures/fake-jwt.ts | 11 +++ apps/web/e2e/fixtures/mock-api.ts | 91 +++++++++++++++++++ apps/web/e2e/login.spec.ts | 40 ++++++++ apps/web/package.json | 6 +- apps/web/playwright.config.ts | 25 +++++ apps/web/vite.config.ts | 1 + package.json | 1 + pnpm-lock.yaml | 43 ++++++++- turbo.json | 4 + 23 files changed, 318 insertions(+), 108 deletions(-) create mode 100644 apps/web/e2e/fixtures/credentials.ts create mode 100644 apps/web/e2e/fixtures/fake-jwt.ts create mode 100644 apps/web/e2e/fixtures/mock-api.ts create mode 100644 apps/web/e2e/login.spec.ts create mode 100644 apps/web/playwright.config.ts diff --git a/.agents/skills/archify/SKILL.md b/.agents/skills/archify/SKILL.md index b130e5c..46d57ca 100644 --- a/.agents/skills/archify/SKILL.md +++ b/.agents/skills/archify/SKILL.md @@ -3,7 +3,7 @@ name: archify description: Create polished, validated architecture, workflow, sequence, data-flow, and lifecycle/state diagrams as explorable standalone HTML with inline SVG, dark/light themes, optional trace motion, and PNG/JPEG/WebP/SVG/WebM export. Accept plain-language requirements or pasted Mermaid flowchart, sequenceDiagram, and stateDiagram input; inspect repository evidence when the diagram must reflect real code. Use when the user asks to visualize system architecture, infrastructure, cloud/security/network topology, technical workflows, API call sequences, request lifecycles, data pipelines, ETL/ELT, data lineage, state machines, or to convert/beautify Mermaid. license: MIT metadata: - version: "2.16" + version: '2.16' author: tt-a1i based_on: Cocoon-AI/architecture-diagram-generator (MIT, v1.0) --- @@ -26,6 +26,7 @@ Use this bounded path for ordinary generation. Do not read the optional Viewer R ``` A receipt with only 4 artifact checks is basic validation, never showcase acceptance. A showcase pass must report all 9 artifact checks with 0 composition errors and 0 warnings. If the candidate omits or misspells the exact `meta.quality_profile` field, fix it before geometry. For a workflow v2 geometry diagnosis, run `node bin/archify.mjs validate workflow --layout-json` and use the stable compiler receipt; solver internals are not authoring controls. A passing final validation freezes the candidate: never edit it afterward. + 5. For a delivered HTML, `deliver` is the final acceptance command: ```bash @@ -54,13 +55,13 @@ Lifecycle note: phase columns `0..4` occupy the main rail; event/terminal column ## Type router -| Type | Use for | -|---|---| +| Type | Use for | +| -------------- | --------------------------------------------------------------- | | `architecture` | Components, services, cloud/security boundaries, infrastructure | -| `workflow` | Processes, approval gates, tool calls, runbooks, CI/CD | -| `sequence` | API call chains, request lifecycles, async traces, returns | -| `dataflow` | Pipelines, ETL/ELT, lineage, governance, consumers | -| `lifecycle` | State/status transitions, retries, waiting and terminal states | +| `workflow` | Processes, approval gates, tool calls, runbooks, CI/CD | +| `sequence` | API call chains, request lifecycles, async traces, returns | +| `dataflow` | Pipelines, ETL/ELT, lineage, governance, consumers | +| `lifecycle` | State/status transitions, retries, waiting and terminal states | When ambiguous, run `node bin/archify.mjs guide "" --json`. Scenario proof examples are structural references, not facts to copy. diff --git a/.agents/skills/project-guidelines-example/SKILL.md b/.agents/skills/project-guidelines-example/SKILL.md index 6e2bb35..c0492c7 100644 --- a/.agents/skills/project-guidelines-example/SKILL.md +++ b/.agents/skills/project-guidelines-example/SKILL.md @@ -25,7 +25,7 @@ Reference this skill when working on this project. It contains: - **Apps**: React 19 + Vite (`apps/web`, `apps/showcase`, `apps/landing`) + Electron (`apps/desktop`) + VitePress (`apps/docs-dev`) - **Packages**: `@repo/ui` (Mantine), `@repo/core-api`, `@repo/core-storage`, `@repo/core-i18n`, `@repo/core-events`, `@repo/utils`, `@repo/brand`, `packages/configs` -- **Testing**: Vitest; Testing Library in `packages/ui` and `packages/core-events` +- **Testing**: Vitest; Playwright E2E in `apps/web/e2e/`; Testing Library in `packages/ui` and `packages/core-events` - **Package manager**: pnpm 8.15.6 + Turbo **Where to work:** product in `apps/web`; copy UI/API usage from `apps/showcase`; concepts from `apps/docs-dev`. Do not invent a parallel UI kit. @@ -128,6 +128,7 @@ export const createFullPageSchema = (t: (key: string) => string) => ```bash pnpm test pnpm --filter web test +pnpm test:e2e:web pnpm --filter @repo/ui test pnpm typecheck:web pnpm check:all @@ -135,7 +136,7 @@ pnpm check:all - Unit tests colocated as `*.test.ts(x)` - Component tests with Testing Library in packages that already have it -- App journeys: browser-verify login and FULL_PAGE index / form / detail +- App journeys: Playwright in `apps/web/e2e/` (login is the first sample) - Minimum 80% coverage; TDD (red → green → refactor) --- @@ -149,6 +150,7 @@ pnpm dev:docs-dev pnpm lint pnpm typecheck:web pnpm test +pnpm test:e2e:web pnpm check:all ``` diff --git a/.agents/skills/tdd-workflow/SKILL.md b/.agents/skills/tdd-workflow/SKILL.md index 3b34fe2..0043c8a 100644 --- a/.agents/skills/tdd-workflow/SKILL.md +++ b/.agents/skills/tdd-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: tdd-workflow -description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage using Vitest, Testing Library, and browser journeys. +description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage using Vitest, Testing Library, and Playwright E2E. --- # Test-Driven Development Workflow @@ -24,7 +24,7 @@ TDD for this frontend monorepo (Vitest, not NestJS/Supertest). - **Unit** — validators, transformers, utils, stores (`*.test.ts`) - **Component** — Testing Library in `packages/ui` / `packages/core-events` -- **Journeys** — login and FULL_PAGE index/form/detail; mock HTTP services; browser-verify layout +- **Journeys** — Playwright in `apps/web/e2e/` (login sample first); mock HTTP with `page.route` ## Workflow @@ -77,7 +77,7 @@ vi.mock('@repo/core-api/http-client', () => ({ ## File organization -Colocate `*.test.ts(x)` next to source. No `test/*.e2e-spec.ts` NestJS tree. +Colocate unit tests as `*.test.ts(x)` next to source. Playwright E2E lives in `apps/web/e2e/` (see `login.spec.ts`). Do not use a NestJS `test/*.e2e-spec.ts` tree. ## Commands @@ -85,6 +85,7 @@ Colocate `*.test.ts(x)` next to source. No `test/*.e2e-spec.ts` NestJS tree. pnpm test pnpm --filter @repo/ui test pnpm --filter web test -- --watch +pnpm test:e2e:web pnpm lint ``` @@ -99,4 +100,4 @@ pnpm lint - 80%+ coverage - All tests green -- Critical FULL_PAGE flows covered or browser-verified +- Critical FULL_PAGE flows covered by Playwright or browser-verified diff --git a/.cursor/agents/e2e-runner.md b/.cursor/agents/e2e-runner.md index e798dc2..16dff25 100644 --- a/.cursor/agents/e2e-runner.md +++ b/.cursor/agents/e2e-runner.md @@ -1,43 +1,46 @@ --- name: e2e-runner -description: Frontend journey specialist using Vitest, Testing Library, and browser verification for apps/web module flows. Use PROACTIVELY for critical UI journeys (login, index, form, detail). +description: Frontend journey specialist using Playwright for apps/web E2E, plus Vitest and Testing Library for package journeys. Use PROACTIVELY for critical UI journeys (login, index, form, detail). tools: Read, Write, Edit, Bash, Grep, Glob model: opus --- # E2E / Journey Runner -You are a frontend journey specialist for this pnpm + Turborepo React monorepo. There is no NestJS, Supertest, or Playwright suite. Cover critical user journeys with Vitest (+ Testing Library where the package already uses it) and browser verification for `apps/web`. +You are a frontend journey specialist for this pnpm + Turborepo React monorepo. Cover critical user journeys with Playwright in `apps/web/e2e/` and with Vitest (+ Testing Library where the package already uses it) for packages. ## Core Responsibilities -1. **Package / component journeys** — Vitest + Testing Library in `packages/ui` and `packages/core-events` -2. **App journeys** — browser-verify `apps/web` flows (login, FULL_PAGE index / form / detail) -3. **Isolation** — mock `@repo/core-api` HTTP services; never hit a real backend unless the user asks +1. **App E2E** — Playwright specs in `apps/web/e2e/` (first sample: `e2e/login.spec.ts`) +2. **Package / component journeys** — Vitest + Testing Library in `packages/ui` and `packages/core-events` +3. **Isolation** — mock the backend with Playwright `page.route` (see `e2e/fixtures/mock-api.ts`); never hit a real API unless the user asks 4. **Flaky management** — no arbitrary sleeps; wait for UI or network conditions -5. **Reporting** — Vitest output and a short pass/fail summary +5. **Reporting** — Playwright/Vitest output and a short pass/fail summary ## Commands ```bash +pnpm test:e2e:web +pnpm --filter web test:e2e +pnpm --filter web test:e2e:ui pnpm test pnpm --filter @repo/ui test -pnpm --filter @repo/core-events test pnpm --filter web test -pnpm check:all ``` +First-time browsers: `pnpm --filter web exec playwright install chromium` + ## What to test -### Critical `apps/web` journeys +### Critical `apps/web` journeys (Playwright) -1. Login (`src/apps/auth/login`) +1. Login (`e2e/login.spec.ts`) — copy this sample for new journeys 2. FULL_PAGE index — table + filters 3. FULL_PAGE form — create / edit / duplicate 4. FULL_PAGE detail 5. Auth session teardown (`terminateAuthSession`) -Canonical sample: `apps/web/src/apps/main/modules/example/full-page/`. Copy that pattern; do not invent a third page style. +Canonical UI sample: `apps/web/src/apps/main/modules/example/full-page/`. Copy that page pattern; do not invent a third page style. ### Package component tests (Testing Library) @@ -52,36 +55,18 @@ it('renders the field label', () => { }); ``` -### Mock remote data services (not a database) +### Mock the HTTP backend (Playwright) -```ts -vi.mock('../../domain/factories', () => ({ - fullPageDataService: { - list: vi.fn(), - get: vi.fn(), - create: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - }, -})); -``` +Reuse `mockBackend(page)` from `apps/web/e2e/fixtures/mock-api.ts`. Extend that helper for new endpoints instead of calling a live server. -## Browser verification (`apps/web`) - -When the change is routing, layout, or a flow Vitest cannot see: - -1. Use `pnpm dev:web` -2. Drive login → index → form → detail the way a user would -3. Check empty, error, and success states -4. Confirm related routes that share module state stay consistent - -Do not add Playwright unless the user explicitly asks. +Unit tests still mock `@repo/core-api` services — not a database. ## Flaky-test rules -- Prefer `getByRole` / `getByLabelText` over CSS classes +- Prefer `getByRole` / `getByLabel` over CSS classes - Wait for elements or responses, never fixed sleeps - Each test sets up its own data +- Keep Playwright specs in `apps/web/e2e/`; do not colocate them next to source (Vitest would pick them up) ## Report format @@ -89,7 +74,7 @@ Do not add Playwright unless the user explicitly asks. # Journey Report **Status:** PASSING / FAILING -**Command:** pnpm test +**Command:** pnpm test:e2e:web ## Summary diff --git a/.cursor/agents/tdd-guide.md b/.cursor/agents/tdd-guide.md index 71cf3fc..77e39ef 100644 --- a/.cursor/agents/tdd-guide.md +++ b/.cursor/agents/tdd-guide.md @@ -56,7 +56,7 @@ export const createFullPageSchema = (t: (key: string) => string) => 1. **Unit** — validators, transformers, utils, stores (`*.test.ts`) 2. **Component** — Testing Library in packages that already have it -3. **Journeys** — login and FULL_PAGE index / form / detail; mock `fullPageDataService`. Browser-verify layout/routing. See **e2e-runner**. +3. **Journeys** — Playwright in `apps/web/e2e/` (copy `login.spec.ts`). Mock HTTP with `mockBackend`. See **e2e-runner**. ## Mocking diff --git a/.cursor/commands/e2e.md b/.cursor/commands/e2e.md index 39f4b00..711190a 100644 --- a/.cursor/commands/e2e.md +++ b/.cursor/commands/e2e.md @@ -1,18 +1,17 @@ --- -description: Generate and run frontend journey tests with Vitest/Testing Library, plus browser verification for apps/web. Covers login and FULL_PAGE index/form/detail. +description: Generate and run frontend journey tests with Playwright for apps/web, plus Vitest/Testing Library for packages. Covers login and FULL_PAGE index/form/detail. --- # E2E / Journey Command -This command invokes the **e2e-runner** agent to cover critical UI journeys. There is no NestJS or Playwright suite. +This command invokes the **e2e-runner** agent to cover critical UI journeys. ## What This Command Does 1. **Identify journeys** — login, FULL_PAGE index / form / detail -2. **Write or update Vitest tests** — mock `@repo/core-api` services -3. **Run** `pnpm test` (or `pnpm --filter web test`) -4. **Browser-verify** `apps/web` when the change is routing or layout -5. **Report** pass/fail +2. **Write or update Playwright specs** in `apps/web/e2e/` (copy `e2e/login.spec.ts`) +3. **Run** `pnpm test:e2e:web` (or `pnpm --filter web test:e2e`) +4. **Report** pass/fail ## When to Use @@ -27,11 +26,10 @@ Use `/e2e` when: The e2e-runner agent will: -1. Copy patterns from `apps/web/src/apps/main/modules/example/full-page/` -2. Mock `fullPageDataService` (or the module's factory) — not a database -3. Run `pnpm test` -4. Drive the browser for layout/routing if needed (`pnpm dev:web`) -5. Summarize failures +1. Copy the login sample in `apps/web/e2e/login.spec.ts` and UI patterns from `example/full-page` +2. Mock the backend with `mockBackend` (`e2e/fixtures/mock-api.ts`) — not a live API +3. Run `pnpm test:e2e:web` +4. Summarize failures ## Example @@ -39,7 +37,7 @@ The e2e-runner agent will: User: /e2e Test the example full-page index and detail flow ``` -Canonical sample: `example/full-page`. Do not invent a third page style. Do not add Playwright unless asked. +Canonical UI sample: `example/full-page`. Canonical E2E sample: `apps/web/e2e/login.spec.ts`. ## Related diff --git a/.cursor/rules/agents.mdc b/.cursor/rules/agents.mdc index e8f62ec..616f744 100644 --- a/.cursor/rules/agents.mdc +++ b/.cursor/rules/agents.mdc @@ -22,7 +22,7 @@ alwaysApply: true | code-reviewer | Code review | After writing code | | security-reviewer | Security analysis | Before commits | | build-error-resolver | Fix build errors | When build fails | -| e2e-runner | Frontend journeys | Vitest + RTL, browser flows | +| e2e-runner | Frontend journeys | Playwright in `apps/web/e2e/` | | refactor-cleaner | Dead code cleanup | Code maintenance | | doc-updater | Documentation | Updating docs | diff --git a/.cursor/rules/frontend-overview.mdc b/.cursor/rules/frontend-overview.mdc index 8bd32e8..9fb3878 100644 --- a/.cursor/rules/frontend-overview.mdc +++ b/.cursor/rules/frontend-overview.mdc @@ -31,7 +31,7 @@ pnpm + Turborepo monorepo. Work from the repository root. Package manager: `pnpm - Before inventing UI or package usage, match `apps/showcase` demos and `apps/docs-dev` docs. - Prefer `@repo/ui`, `@repo/core-*`, `@repo/utils` over app-local duplicates or raw Mantine/axios. - Env files live **inside the app** (`apps/web/.env*`), never at monorepo root. Read env via `src/core/environment` (`ENV`), not `import.meta.env` in components. -- Run scripts from root: `pnpm dev:web`, `pnpm dev:showcase`, `pnpm lint`, `pnpm typecheck:web`, `pnpm test`, `pnpm check:all`. +- Run scripts from root: `pnpm dev:web`, `pnpm dev:showcase`, `pnpm lint`, `pnpm typecheck:web`, `pnpm test`, `pnpm test:e2e:web`, `pnpm check:all`. ## Import map (preferred) diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc index 8a32fb6..5c210b4 100644 --- a/.cursor/rules/testing.mdc +++ b/.cursor/rules/testing.mdc @@ -1,5 +1,5 @@ --- -description: TDD workflow, 80% coverage minimum, Vitest unit tests, and browser journey verification +description: TDD workflow, 80% coverage minimum, Vitest unit tests, and Playwright E2E for apps/web alwaysApply: true --- @@ -11,9 +11,9 @@ Test types: 1. **Unit tests** — pure functions, transformers, validators, stores, utilities (`*.test.ts` / `*.test.tsx`, colocated or `__tests__/`) 2. **Component tests** — Testing Library in packages that already have it (`packages/ui`, `packages/core-events`) -3. **App journeys** — browser verification of `apps/web` flows (login, index / form / detail). There is no Playwright or NestJS E2E suite in this repo. +3. **App journeys** — Playwright E2E in `apps/web/e2e/` (login is the first sample). Mock the backend with `page.route`; do not hit a live API unless the user asks. There is no NestJS/Supertest suite in this frontend repo. -Runner: **Vitest**. Root command: `pnpm test`. Per-package: `pnpm --filter test`. Also `pnpm typecheck` and `pnpm check:all`. +Runner: **Vitest** for unit tests (`pnpm test`). Playwright for web E2E (`pnpm test:e2e:web` or `pnpm --filter web test:e2e`). Also `pnpm typecheck` and `pnpm check:all` (unit/lint/typecheck only). ## Test-Driven Development @@ -30,11 +30,11 @@ MANDATORY workflow: 1. Use **tdd-guide** agent 2. Check test isolation -3. Verify mocks are correct (`@repo/core-api` services, HTTP client — not a database) +3. Verify mocks are correct (`@repo/core-api` / HTTP client for unit tests; `mockBackend` in `apps/web/e2e/fixtures/mock-api.ts` for Playwright) 4. Fix implementation, not tests (unless tests are wrong) ## Agent Support - **tdd-guide** — Use PROACTIVELY for new features, enforces write-tests-first -- **e2e-runner** — Frontend journeys: Vitest + Testing Library, plus browser verification for `apps/web` +- **e2e-runner** — Playwright E2E in `apps/web/e2e/`, plus Vitest + Testing Library for package journeys - Skill: `.agents/skills/tdd-workflow/` diff --git a/.gitignore b/.gitignore index 1f484b1..4cb0b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,10 @@ legacy/ # VitePress **/.vitepress/cache/ -**/.vitepress/dist/ \ No newline at end of file +**/.vitepress/dist/ + +# Playwright +**/test-results/ +**/playwright-report/ +**/blob-report/ +**/playwright/.cache/ \ No newline at end of file diff --git a/apps/docs-dev/src/apps/web/SALES_WORKFLOW.md b/apps/docs-dev/src/apps/web/SALES_WORKFLOW.md index 86aaaaa..e499eed 100644 --- a/apps/docs-dev/src/apps/web/SALES_WORKFLOW.md +++ b/apps/docs-dev/src/apps/web/SALES_WORKFLOW.md @@ -10,10 +10,10 @@ You need permission to view each menu. Status buttons (Process, Complete, Submit Set up the records that every sales document needs: -| You need | Where to create it | -| --- | --- | -| Division, branch, customer, product | **Settings → Data** | -| Salesperson | **Sales → Data → Employees** | +| You need | Where to create it | +| ----------------------------------- | ---------------------------- | +| Division, branch, customer, product | **Settings → Data** | +| Salesperson | **Sales → Data → Employees** | If a menu is missing, ask an administrator to give your privilege **View** (and **Update** for status buttons) on that area. @@ -70,7 +70,7 @@ A request is optional. You can go straight to a sales order. 1. Click **Create**. Fill in the same kind of details as a request. On a new order you can pick a sales request under **Source**; that copies the request into the form. Save. A new order starts as **Draft**. 2. On a **Draft** order, click **Process**. - - You will see: *Processing creates a sales invoice and, unless skipped, a packing slip.* + - You will see: _Processing creates a sales invoice and, unless skipped, a packing slip._ - **Generate packing slip** is checked by default. Uncheck it if you do not want a packing slip. 3. On **Draft** or **Processed**, you can **Cancel**. There is no **Complete** button on orders. If the order later shows **Completed**, that was not something you clicked on this screen. 4. On the order detail page you can see linked packing slips and invoices, and click **Create Sales Invoice**. @@ -134,32 +134,32 @@ Approving a payment does not change the invoice status from this screen. Refresh Both use Draft → Pending → Approved or Rejected. -| Current status | Buttons | Change status | -| --- | --- | --- | -| Draft | Submit | Rejected | -| Pending | Approve, Reject. Payments also have Rollback (back to Draft) | Draft, Approved, Rejected | -| Approved | — | — | -| Rejected | — | Draft | +| Current status | Buttons | Change status | +| -------------- | ------------------------------------------------------------ | ------------------------- | +| Draft | Submit | Rejected | +| Pending | Approve, Reject. Payments also have Rollback (back to Draft) | Draft, Approved, Rejected | +| Approved | — | — | +| Rejected | — | Draft | ### Sales order -| Current status | Buttons | Change status | -| --- | --- | --- | -| Draft | Process, Cancel | Processed, Cancelled | -| Processed | Cancel | Cancelled | -| Completed | — | — | -| Cancelled | — | — | +| Current status | Buttons | Change status | +| -------------- | --------------- | -------------------- | +| Draft | Process, Cancel | Processed, Cancelled | +| Processed | Cancel | Cancelled | +| Completed | — | — | +| Cancelled | — | — | **Process** always opens the packing-slip checkbox dialog (not Change status). ### Packing slip -| Current status | Buttons | Change status | -| --- | --- | --- | -| Draft | Cancel | Processed, Cancelled | -| Processed | Complete, Cancel | Completed, Cancelled | -| Completed | — | — | -| Cancelled | — | — | +| Current status | Buttons | Change status | +| -------------- | ---------------- | -------------------- | +| Draft | Cancel | Processed, Cancelled | +| Processed | Complete, Cancel | Completed, Cancelled | +| Completed | — | — | +| Cancelled | — | — | **Complete** always asks for delivered quantities. @@ -167,11 +167,11 @@ Both use Draft → Pending → Approved or Rejected. You may see Draft, Processed, Partial, Completed, or Cancelled. -| Current status | What you can do | -| --- | --- | -| Draft, Processed, or Partial | Cancel | -| Completed | No status buttons | -| Cancelled | No status buttons | +| Current status | What you can do | +| ---------------------------- | ----------------- | +| Draft, Processed, or Partial | Cancel | +| Completed | No status buttons | +| Cancelled | No status buttons | --- diff --git a/apps/docs-dev/src/apps/web/index.md b/apps/docs-dev/src/apps/web/index.md index ff5a861..dccca4a 100644 --- a/apps/docs-dev/src/apps/web/index.md +++ b/apps/docs-dev/src/apps/web/index.md @@ -8,12 +8,12 @@ Use the sidebar to open **Sales** for day-to-day selling, **Logistics** for pack ## Where to find things -| Area | Menu | What you do there | -| --- | --- | --- | -| Sales | Sales → Activities | Requests, orders, invoices, payments, visit plans | -| Logistics | Logistics → Activities | Packing slips, delivery plans | -| Company data | Settings → Data | Divisions, branches, customers, products | -| Access | Settings → User | Users and privileges | +| Area | Menu | What you do there | +| ------------ | ---------------------- | ------------------------------------------------- | +| Sales | Sales → Activities | Requests, orders, invoices, payments, visit plans | +| Logistics | Logistics → Activities | Packing slips, delivery plans | +| Company data | Settings → Data | Divisions, branches, customers, products | +| Access | Settings → User | Users and privileges | If a menu item is missing, your privilege does not include **View** for that area. diff --git a/apps/docs-dev/src/setup.md b/apps/docs-dev/src/setup.md index a3ba504..d8ba352 100644 --- a/apps/docs-dev/src/setup.md +++ b/apps/docs-dev/src/setup.md @@ -48,6 +48,7 @@ This repository uses **[Turborepo](https://turbo.build/repo)** to orchestrate ta | `pnpm build:docs-dev` | Build only the docs-dev application | | `pnpm build:desktop` | Build the web app, then compile the Electron app | | `pnpm test` | Run unit tests ([Vitest](https://vitest.dev/)) across all packages | +| `pnpm test:e2e:web` | Run Playwright E2E for `apps/web` (starts Vite on port 4173) | | `pnpm lint` | Run [ESLint](https://eslint.org/) across the workspace | | `pnpm format` | Format code using [Prettier](https://prettier.io/) | diff --git a/apps/web/e2e/fixtures/credentials.ts b/apps/web/e2e/fixtures/credentials.ts new file mode 100644 index 0000000..038fe9d --- /dev/null +++ b/apps/web/e2e/fixtures/credentials.ts @@ -0,0 +1,4 @@ +export const E2E_LOGIN = { + username: 'administrator', + password: 'password123', +} as const; diff --git a/apps/web/e2e/fixtures/fake-jwt.ts b/apps/web/e2e/fixtures/fake-jwt.ts new file mode 100644 index 0000000..75cac2b --- /dev/null +++ b/apps/web/e2e/fixtures/fake-jwt.ts @@ -0,0 +1,11 @@ +export function createFakeJwt(expiresInSeconds = 3600): string { + const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url'); + const payload = Buffer.from( + JSON.stringify({ + sub: 'e2e-user', + exp: Math.floor(Date.now() / 1000) + expiresInSeconds, + }), + ).toString('base64url'); + + return `${header}.${payload}.e2e`; +} diff --git a/apps/web/e2e/fixtures/mock-api.ts b/apps/web/e2e/fixtures/mock-api.ts new file mode 100644 index 0000000..a34b94e --- /dev/null +++ b/apps/web/e2e/fixtures/mock-api.ts @@ -0,0 +1,91 @@ +import type { Page, Route } from '@playwright/test'; +import { E2E_LOGIN } from './credentials'; +import { createFakeJwt } from './fake-jwt'; + +const APP_HOSTS = new Set(['127.0.0.1', 'localhost']); +const APP_PORT = Number(process.env.E2E_PORT ?? 4173); + +const EMPTY_LIST = { + data: [], + meta: { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + itemCount: 0, + }, +}; + +const E2E_USER = { + id: 'e2e-user-id', + username: E2E_LOGIN.username, + isSuperadmin: true, + privilege: null, + permissions: {}, +}; + +export type LoginApiOutcome = 'success' | 'unauthorized'; + +export async function mockBackend(page: Page, login: LoginApiOutcome = 'success'): Promise { + await page.route('**/*', async (route) => { + const requestUrl = new URL(route.request().url()); + if (isAppRequest(requestUrl)) { + await route.continue(); + return; + } + + await fulfillBackend(route, login); + }); +} + +function isAppRequest(requestUrl: URL): boolean { + const port = Number(requestUrl.port || (requestUrl.protocol === 'https:' ? 443 : 80)); + return APP_HOSTS.has(requestUrl.hostname) && port === APP_PORT; +} + +async function fulfillBackend(route: Route, login: LoginApiOutcome): Promise { + const request = route.request(); + const pathname = new URL(request.url()).pathname.replace(/\/$/, ''); + + if (pathname.endsWith('/auth/login') && request.method() === 'POST') { + if (login === 'unauthorized') { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ statusCode: 401, message: 'Unauthorized' }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + accessToken: createFakeJwt(), + refreshToken: createFakeJwt(86_400), + }), + }); + return; + } + + if (pathname.endsWith('/auth/me') && request.method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(E2E_USER), + }); + return; + } + + const resourceType = request.resourceType(); + if (resourceType === 'xhr' || resourceType === 'fetch') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(EMPTY_LIST), + }); + return; + } + + await route.abort(); +} diff --git a/apps/web/e2e/login.spec.ts b/apps/web/e2e/login.spec.ts new file mode 100644 index 0000000..3307e5e --- /dev/null +++ b/apps/web/e2e/login.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; +import { E2E_LOGIN } from './fixtures/credentials'; +import { mockBackend } from './fixtures/mock-api'; + +test.describe('login', () => { + test('renders the login form', async ({ page }) => { + await mockBackend(page); + await page.goto('/auth/login'); + + await expect(page.getByRole('heading', { name: 'Login to your account!' })).toBeVisible(); + await expect(page.getByLabel('Username')).toBeVisible(); + await expect(page.getByLabel('Password')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); + }); + + test('signs in with valid credentials and lands in the app', async ({ page }) => { + await mockBackend(page, 'success'); + await page.goto('/auth/login'); + + await page.getByLabel('Username').fill(E2E_LOGIN.username); + await page.getByLabel('Password').fill(E2E_LOGIN.password); + await page.getByRole('button', { name: 'Login' }).click(); + + await expect(page).toHaveURL(/\/app(\/|$)/); + await expect(page.getByRole('heading', { name: 'Full Page' })).toBeVisible(); + }); + + test('shows an error and stays on login when credentials are rejected', async ({ page }) => { + await mockBackend(page, 'unauthorized'); + await page.goto('/auth/login'); + + await page.getByLabel('Username').fill(E2E_LOGIN.username); + await page.getByLabel('Password').fill(E2E_LOGIN.password); + await page.getByRole('button', { name: 'Login' }).click(); + + await expect(page.getByText('Invalid username or password')).toBeVisible(); + await expect(page).toHaveURL(/\/auth\/login/); + await expect(page.getByRole('heading', { name: 'Login to your account!' })).toBeVisible(); + }); +}); diff --git a/apps/web/package.json b/apps/web/package.json index ef5ab0c..9da6918 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,9 +8,12 @@ "dev": "pnpm run copy:brand && vite --clearScreen false", "build": "pnpm run copy:brand && tsc && vite build", "preview": "vite preview", - "lint": "eslint \"src/**/*.ts\"", + "lint": "eslint \"src/**/*.ts\" \"e2e/**/*.ts\" \"playwright.config.ts\"", "test": "vitest run", "test:watch": "vitest --watch", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:install": "playwright install chromium", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -37,6 +40,7 @@ "zustand": "^5.0.14" }, "devDependencies": { + "@playwright/test": "^1.55.0", "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@types/react": "^19.2.7", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 0000000..035cf7c --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test'; + +const E2E_PORT = Number(process.env.E2E_PORT ?? 4173); +const E2E_ORIGIN = `http://127.0.0.1:${E2E_PORT}`; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + reporter: 'list', + timeout: 60_000, + use: { + baseURL: E2E_ORIGIN, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + webServer: { + command: `pnpm run copy:brand && vite --clearScreen false --host 127.0.0.1 --port ${E2E_PORT} --strictPort`, + url: E2E_ORIGIN, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], +}); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 1ec069c..ff40001 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -21,5 +21,6 @@ export default defineConfig({ test: { environment: 'node', globals: false, + exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'], }, }); diff --git a/package.json b/package.json index c04febc..3f6d312 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "test": "turbo run test", + "test:e2e:web": "pnpm --filter web test:e2e", "typecheck": "turbo run typecheck", "typecheck:web": "turbo run typecheck --filter=web", "typecheck:landing": "turbo run typecheck --filter=landing", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d84063f..0e5bb3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -239,7 +239,7 @@ importers: version: 5.4.17(@types/node@22.19.3) vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1) + version: 4.0.17(jsdom@26.1.0) apps/web: dependencies: @@ -307,6 +307,9 @@ importers: specifier: ^5.0.14 version: 5.0.14(@types/react@19.2.7)(react@19.2.3) devDependencies: + '@playwright/test': + specifier: ^1.55.0 + version: 1.62.1 '@repo/eslint-config': specifier: workspace:* version: link:../../packages/configs/eslint @@ -333,7 +336,7 @@ importers: version: 5.4.17(@types/node@22.19.3) vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1) + version: 4.0.17(jsdom@26.1.0) packages/brand: devDependencies: @@ -551,7 +554,7 @@ importers: version: 5.5.4 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1) + version: 4.0.17(jsdom@26.1.0) packages/ui: dependencies: @@ -733,7 +736,7 @@ importers: version: 5.5.4 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1) + version: 4.0.17(jsdom@26.1.0) packages: @@ -2607,6 +2610,14 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: false + /@playwright/test@1.62.1: + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + dependencies: + playwright: 1.62.1 + dev: true + /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} dev: false @@ -7682,6 +7693,14 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -10098,6 +10117,22 @@ packages: engines: {node: '>=12'} dev: false + /playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + dev: true + + /playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} diff --git a/turbo.json b/turbo.json index 669fa55..1b9b665 100644 --- a/turbo.json +++ b/turbo.json @@ -52,6 +52,10 @@ "test:watch": { "cache": false, "persistent": true + }, + "test:e2e": { + "cache": false, + "dependsOn": [] } } } \ No newline at end of file