Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ updates:
update-types:
- "minor"
- "patch"

# Browser-test tooling only (ADR-0011); nothing in e2e/ ships.
- package-ecosystem: "npm"
directory: "/e2e"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 2
groups:
e2e-dependencies:
patterns:
- "*"
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,55 @@ jobs:
- name: Detener el contenedor
if: always()
run: docker rm -f nainorder-ci || true

# The panel's visual behaviour (landing, roles, language, QR, offline, phone layout) is
# verified in a real browser against the real application (ADR-0011). It is its own job so a
# browser flake never hides the result of the .NET tests.
browser-tests:
name: Browser tests
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: e2e/package-lock.json

# Built before the suite starts so the server is up within Playwright's start-up window.
- name: Build the application
run: dotnet build NainOrder.Api/NainOrder.Api.csproj -c Release

- name: Install test dependencies
working-directory: e2e
run: npm ci

- name: Audit test dependencies
working-directory: e2e
run: npm audit --audit-level=high

- name: Install the browser
working-directory: e2e
run: npx playwright install --with-deps chromium

- name: Run the browser tests
working-directory: e2e
run: npx playwright test

- name: Keep the report and traces if something fails
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
e2e/playwright-report
e2e/test-results
if-no-files-found: ignore
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,10 @@ artifacts/
*.db-shm
*.db-wal

# Browser tests (e2e/): dependencies and run output
e2e/node_modules/
e2e/test-results/
e2e/playwright-report/

# Configuración local del editor
.vscode/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![ci](https://github.com/Nain9Dev/NainOrder/actions/workflows/ci.yml/badge.svg)](https://github.com/Nain9Dev/NainOrder/actions/workflows/ci.yml)
[![CodeQL](https://github.com/Nain9Dev/NainOrder/actions/workflows/codeql.yml/badge.svg)](https://github.com/Nain9Dev/NainOrder/actions/workflows/codeql.yml)
![.NET 10](https://img.shields.io/badge/.NET-10-512BD4)
![Tests](https://img.shields.io/badge/tests-123%20passing-2ea44f)
![Tests](https://img.shields.io/badge/tests-123%20.NET%20%2B%2025%20browser-2ea44f)
![Client dependencies](https://img.shields.io/badge/client%20dependencies-0-22d3ee)
[![Live demo](https://img.shields.io/badge/live%20demo-order.naindev.com-8b5cf6)](https://order.naindev.com/)

Expand Down Expand Up @@ -94,6 +94,7 @@ The panel is plain ES modules served by the API itself: no npm, no build step, n
| Stock is never oversold, even in a race | `StockRaceTests`, `ConcurrencyTests` |
| Two writers on one order never lose an update | `OrderAggregateConcurrencyProbe` |
| Domain has zero dependencies | `CleanArchitectureTests` |
| The QR code is the buyer link, the shell works offline, the phone layout fits | `e2e/` — 25 browser tests (Playwright) against the real app, QR decoded by an independent library |
| The shipped container works | CI builds the image and probes it |

Every requirement maps to a test in [`docs/50-traceability.md`](docs/50-traceability.md).
Expand All @@ -112,6 +113,7 @@ and `/health`. No database to install: SQLite creates itself.

```bash
dotnet test NainOrder.slnx -c Release # 123 tests
cd e2e && npm ci && npx playwright test # 25 browser tests (starts the app itself)
docker build -t nainorder . && docker run -p 8080:8080 nainorder
```

Expand Down
7 changes: 7 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ tasks:
desc: "Generate strict TypeScript client SDK from OpenAPI specification"
cmds:
- node scripts/generate-client.mjs

e2e:
desc: "Run the browser tests (starts the application itself; set PW_CHANNEL=msedge to reuse an installed browser)"
dir: e2e
cmds:
- npm ci
- npx playwright test
22 changes: 16 additions & 6 deletions docs/10-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,34 @@ enters this table when it is implemented and tested.

## Drop experience

From [specs/003-drop-experience](../specs/003-drop-experience/spec.md). The visual requirements
(REQ-090 to REQ-094, REQ-096) live in that spec until browser-level tests exist; only what is
verified by an automated test is listed here.
From [specs/003-drop-experience](../specs/003-drop-experience/spec.md). The visual requirements are
verified in a real browser by `e2e/` ([ADR-0011](30-decisions/ADR-0011-browser-tests-with-playwright.md)).

| ID | Requirement |
| :--- | :--- |
| REQ-090 | When the panel opens on the Drop view with no drop selected, the system shall play a short wordless story — a crowd arrives, units are held, an unconfirmed hold expires, units are sold — and offer one action per role. |
| REQ-091 | Where the visitor chooses the buyer role, the panel shall show only what a buyer needs: the count, one action and their own hold. |
| REQ-092 | Where the visitor chooses the organizer role, the panel shall show the drop's units, the figures, the next hold to expire and the buyer link as a QR code, and shall let the organizer release a simulated crowd. |
| REQ-093 | The panel shall let the visitor switch between buyer and organizer on the same drop without losing it. |
| REQ-094 | The panel shall offer Spanish and English, remember the choice, and default to the browser's language. |
| REQ-095 | Every user-visible string of the panel shall exist in both Spanish and English with the same placeholders, and a missing or empty one shall fail the build. |
| REQ-096 | Where the visitor prefers reduced motion, the landing story shall show its end state without animation. |
| REQ-097 | The system shall report, with each drop, when its soonest live hold ends, or nothing when no hold is live. |

## Installable app

From [specs/004-mobile-share](../specs/004-mobile-share/spec.md). Behavioural requirements
(REQ-101 to REQ-105: offline shell, 360 px layout, buyer link, QR code) live in that spec until
browser-level tests exist; the QR encoder was checked against an independent decoder at development time.
From [specs/004-mobile-share](../specs/004-mobile-share/spec.md). The behavioural requirements are
verified in a real browser by `e2e/`; the QR code is decoded by an independent library
(`jsQR`) from a capture of the screen.

| ID | Requirement |
| :--- | :--- |
| REQ-100 | The panel shall be installable: a web manifest with name, start URL, standalone display, theme colour and icons of 192 and 512 px, one of them maskable, and every icon present at its declared size. |
| REQ-101 | While the network is unavailable, the panel shall open its interface from a cache and say that it is offline; it shall never serve a cached API response, and purchases shall be visibly unavailable. |
| REQ-102 | The panel shall be usable at 360 px width: no horizontal page scroll, and touch targets of at least 44 px for the primary actions. |
| REQ-103 | When a visitor opens a drop's buyer link (`#/d/<id>`), the panel shall show only the buyer page — no navigation, no top bar — and remember the visitor across visits; for a drop that does not exist it shall say so. |
| REQ-104 | When the organizer opens a drop, the panel shall offer its buyer link as a QR code and as copy and share actions. |
| REQ-105 | The QR code shall be generated in the browser without any third-party library or service, and shall encode the drop's buyer link exactly. |
| REQ-106 | The service worker's precache list shall name every static asset of the panel and no file that does not exist, and the service worker shall never handle `/api` or a non-GET request. |

## HTTP contract
Expand Down
41 changes: 41 additions & 0 deletions docs/30-decisions/ADR-0011-browser-tests-with-playwright.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# ADR-0011 — Browser tests with Playwright, outside the panel

**Status:** Proposed

## Context

The panel's visual behaviour — the landing story, the buyer and organizer views, the language
selector, the QR code, the offline shell, the phone layout — was verified by hand and listed as a gap
in [50-traceability.md](../50-traceability.md). A gap that depends on someone remembering to look is
not a guarantee. The panel itself has no build step and no dependency
([ADR-0006](ADR-0006-dependency-free-client-panel.md)), so any test tooling must stay out of it.

## Decision

Add a self-contained `e2e/` folder with its own `package.json`, using Playwright Test:

- **Dev-only.** Nothing in `e2e/` ships, and the panel gains no dependency or build step. The .NET
solution does not reference it.
- **Real application.** Playwright starts `NainOrder.Api` itself against a throwaway SQLite file and
drives a real browser: the same path a visitor takes.
- **Independent QR decoder.** `jsQR` decodes the QR code captured from the screen, so the in-repo
encoder is checked by code it does not share.
- **Own CI job**, separate from the .NET job, so a browser flake never hides a unit-test result.
- **Pinned and audited.** Dependencies are pinned by `package-lock.json`; Dependabot watches `e2e/`
and CI runs `npm audit` on it.

## Alternatives considered

- **Keep verifying by hand.** Rejected: it is how a regression ships unnoticed.
- **A .NET browser-test library (Playwright for .NET).** Considered; rejected because the JavaScript
runner has better tracing and a lighter CI setup, and the tests describe a JavaScript panel.
- **Unit-testing the panel's modules in Node.** Rejected: the panel's risk is in the browser
(service worker, layout, touch), which a DOM shim does not reproduce.

## Consequences

- Contributors need Node only to run `e2e/`; `dotnet test` stays the whole story for the backend.
- CI installs a browser for this job (a few minutes) and uploads Playwright's trace on failure.
- The service-worker and offline tests need a secure context; `localhost` qualifies, so no TLS setup
is needed.
- Tests must wait on observable state, never on fixed sleeps, to stay stable.
1 change: 1 addition & 0 deletions docs/30-decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ Records of significant architectural and design decisions for NainOrder.
| [ADR-0008](ADR-0008-drops-with-expiring-holds.md) | Drops with expiring holds | Approved |
| [ADR-0009](ADR-0009-sqlite-wal-mode.md) | SQLite in WAL mode | Proposed |
| [ADR-0010](ADR-0010-service-worker-shell-cache.md) | Service worker that caches the shell, never the API | Proposed |
| [ADR-0011](ADR-0011-browser-tests-with-playwright.md) | Browser tests with Playwright, outside the panel | Proposed |
37 changes: 23 additions & 14 deletions docs/50-traceability.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Traceability

**Status:** Active · verified against a green run of `dotnet test NainOrder.slnx -c Release` (123/123 tests)
**Status:** Active · verified against green runs of `dotnet test NainOrder.slnx -c Release` (123/123 tests) and `npx playwright test` in `e2e/` (25/25 browser tests)

Every requirement in [10-requirements.md](10-requirements.md) and the test that demonstrates it.
A requirement with no test is not done, whatever the code says.
Expand All @@ -17,6 +17,7 @@ Test suites:
| `Api.OrderAggregateConcurrencyProbe` | Concurrency, aggregate | Multi-context concurrent aggregate mutations and optimistic concurrency conflict enforcement |
| `Domain.DropTests` | Example-based + property-based | The drop aggregate with an explicit clock: holds, expiry, limits, the held-plus-sold invariant |
| `Api.DropFlowTests` | Integration, concurrent | Drops over HTTP with a hand-moved clock, a 40-buyer crowd and UTC serialisation |
| `e2e/tests/*.spec.ts` | Browser (Playwright), real application | Landing, roles, language, buyer link and QR, offline shell, 360 px layout |
| `Api.PwaAssetsTests` | Contract | Manifest, icons and service-worker precache list match the files served |
| `Api.I18nParityTests` | Contract | Both panel languages define the same keys and placeholders |
| `Api.StockRaceTests` | Integration, concurrent | The on-demand race: many buyers, few units, verdict from persisted stock |
Expand Down Expand Up @@ -97,14 +98,25 @@ Test suites:

| Requirement | Test |
| :--- | :--- |
| REQ-095 | `I18nParityTests.Spanish_and_English_define_exactly_the_same_keys`, `No_string_is_empty`, `Placeholders_match_between_languages` |
| REQ-090 | `e2e/tests/landing.spec.ts` › the landing plays the story and offers one action per role |
| REQ-091 | `e2e/tests/roles.spec.ts` › the buyer view shows the count, one action and no organizer tools |
| REQ-092 | `e2e/tests/roles.spec.ts` › the organizer view shows the grid, the figures and releases a crowd (also asserts available + held + sold = 48 as the panel shows it) |
| REQ-093 | `e2e/tests/roles.spec.ts` › switching between buyer and organizer keeps the same drop; a buyer's purchase appears as a sold unit for the organizer |
| REQ-094 | `e2e/tests/language.spec.ts` › defaults to English, switches to Spanish and remembers the choice; defaults to Spanish |
| REQ-095 | `I18nParityTests.Spanish_and_English_define_exactly_the_same_keys`, `No_string_is_empty`, `Placeholders_match_between_languages`; at run time `e2e/tests/language.spec.ts` › every route renders in es / en with no missing keys or errors |
| REQ-096 | `e2e/tests/landing.spec.ts` › reduced motion › the story does not animate and shows only its last caption |
| REQ-097 | `DropTests.The_next_expiry_is_the_soonest_live_hold_and_null_when_none_is_live`; `DropFlowTests.A_drop_reports_when_its_next_hold_expires` |

## Installable app

| Requirement | Test |
| :--- | :--- |
| REQ-100 | `PwaAssetsTests.The_manifest_declares_what_a_browser_needs_to_install_the_panel`, `The_manifest_has_192_and_512_icons_and_one_of_them_is_maskable`, `Every_icon_exists_and_is_a_png_of_the_declared_size`, `The_page_links_the_manifest_and_declares_its_colours_and_touch_icon` |
| REQ-101 | `e2e/tests/offline.spec.ts` › offline, the shell opens from the cache and says it is offline; going offline mid-session disables purchases and the API is never faked |
| REQ-102 | `e2e/tests/mobile.spec.ts` › every route does not scroll sideways at 360 px; the buyer page and the organizer view fit and have large touch targets; primary buttons reach 44 px on a touch screen |
| REQ-103 | `e2e/tests/buyer-link.spec.ts` › the buyer link shows the buyer page alone and remembers the visitor; a buyer link to a drop that does not exist says so |
| REQ-104 | `e2e/tests/buyer-link.spec.ts` › the QR code decodes to the buyer link; copy puts the buyer link on the clipboard |
| REQ-105 | `e2e/tests/buyer-link.spec.ts` › the QR code decodes to the buyer link (decoded by `jsQR`, independent of the encoder); the encoder is `assets/js/qr.js`, with no dependency |
| REQ-106 | `PwaAssetsTests.Every_precache_entry_points_at_a_file_that_exists`, `Every_static_asset_of_the_panel_is_precached`, `The_service_worker_never_handles_the_api_or_non_get_requests` |

## HTTP contract
Expand All @@ -128,17 +140,14 @@ Test suites:

## Gaps

Every requirement declared in [10-requirements.md](10-requirements.md) is covered by an automated test passing in the Release suite.
Every requirement declared in [10-requirements.md](10-requirements.md) is covered by an automated test:
the .NET suite (`dotnet test`) or the browser suite (`e2e/`, CI job `Browser tests`).

One known gap, deliberately outside that file: the visual behaviour of the panel in
[specs/003-drop-experience](../specs/003-drop-experience/spec.md) (REQ-090 to REQ-094, REQ-096:
landing story, role lenses, language selector, reduced motion). It was verified by hand in a browser
against the real API and the in-browser engine, in both languages, with no console errors or missing
translation keys; there is no browser-level test yet (task T-056).
What is still not automated, and honestly so:

A second one, from [specs/004-mobile-share](../specs/004-mobile-share/spec.md) (REQ-101 to REQ-105):
the offline shell, the 360 px layout, the standalone buyer page and the QR code. Verified by hand
against the real API with a phone profile and with the network switched off; the QR encoder's
output was decoded by an independent decoder (`jsQR`, run outside the repository) for seven inputs
from version 2 to 10, and a generated code on screen decoded back to its link. The same task T-056
covers them.
- **Installation on a physical phone and over HTTPS** (Android Chrome, iOS "Add to Home Screen").
The suite proves the manifest, the icons, the service worker and the offline shell; it cannot
press the install button of a real device (tasks T-066 and T-067, which need the owner).
- **The QR encoder across every version.** The suite decodes the codes the panel actually shows
(a link of about 65 characters, version 5); seven inputs from version 2 to 10 were also decoded by `jsQR`
during development. A property test over many inputs is possible and not yet written.
12 changes: 10 additions & 2 deletions docs/60-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ dotnet build NainOrder.slnx -c Release && dotnet test NainOrder.slnx -c Release
```

`TreatWarningsAsErrors` is on in all four projects, so a new warning fails the build. Changes to
`NainOrder.Api/wwwroot` have no build step and must be checked in a browser against a running
instance.
`NainOrder.Api/wwwroot` have no build step; the browser suite checks them against the real
application:

```bash
cd e2e && npm ci && npx playwright install chromium && npx playwright test
```

Playwright starts the API itself on port 8180 with a throwaway database. To reuse an installed
browser, set `PW_CHANNEL=msedge` (or `chrome`). On a failure, `npx playwright show-trace` opens the
recorded trace ([ADR-0011](30-decisions/ADR-0011-browser-tests-with-playwright.md)).

## Container

Expand Down
5 changes: 5 additions & 0 deletions docs/90-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Notable changes. Format loosely follows Keep a Changelog; dates come from git.

### Added

- Browser tests (`e2e/`, ADR-0011): 25 Playwright tests against the real application cover the
landing, the buyer and organizer views, the language selector, the buyer link and its QR code
(decoded by an independent library), the offline shell and the 360 px layout. They run in their own
CI job; the visual requirements REQ-090 to REQ-096 and REQ-101 to REQ-105 are now traced to tests.

- Mobile, install and share (`specs/004-mobile-share`, ADR-0010): the panel is an installable app
(manifest, icons, service worker that caches the shell and never the API), shows when it is
offline, and works at 360 px. An organizer gets a QR code, copy and share for the drop's buyer
Expand Down
Loading
Loading