Vendor self-registration — Playwright E2E reference
This document is the technical reference for the dedicated Playwright E2E
suite covering the public vendor self-registration flow
(/register → POST /api/v1/auth/vendor/register). It complements the
end-user guide at Đăng ký tài khoản.
Audience: engineers maintaining the vendor-web registration form, the backend
VendorAuthService.register()contract, or the test infrastructure (Playwright + NestJS backend test mode).
1. Why this suite exists
Section titled “1. Why this suite exists”There are two adjacent Playwright specs that already touch /register:
| Spec | Purpose | Where |
|---|---|---|
docs-register-screenshots.spec.ts | Captures screenshots for apps/docs (gated by PLAYWRIGHT_UPDATE_DOCS_SCREENSHOTS=1). | apps/vendor-web/e2e/vendor/getting-started/docs-register-screenshots.spec.ts:1 |
register.spec.ts (this doc) | Asserts business behaviour of the registration flow: happy path, validation, slug derivation, slug conflict, guards. | apps/vendor-web/e2e/vendor/getting-started/register.spec.ts:1 |
Splitting the two keeps the docs-screenshots spec fast and idempotent
(it skips every test outside PLAYWRIGHT_UPDATE_DOCS_SCREENSHOTS=1), and
keeps the behavioural suite small, deterministic, and safe to run on
every PR.
2. Scope
Section titled “2. Scope”The suite drives the frontend through /register and verifies the
observable contract of POST /api/v1/auth/vendor/register:
- Happy path: account info → store info → welcome screen → real auth via the owner login screen with the freshly minted credentials.
- Step 1 validation:
Continuestays disabled until the form is valid (full name required, email format, phone ≥ 5 chars). - Step 2 validation: store name required, slug rule
(
^[a-z][a-z0-9-]{3,40}$), terms checkbox required. - Slug auto-suggest: typing a store name fills the slug field, but only until the user touches the slug themselves.
- Backend
slug_taken4xx is surfaced via the inline error banner and the form remains usable so the user can retry. - Already-authenticated owners are redirected from
/registerto the vendor dashboard root.
It does not cover:
- Backend unit tests for
VendorAuthService.register()— seeapps/backend/src/modules/vendor/vendor-auth.service.spec.ts. - Backend e2e (Jest + supertest) — see
apps/backend/test/vendor-auth.e2e-spec.ts. - Mobile / Expo app registration — covered separately.
- Visual regressions — see
docs-register-screenshots.spec.ts.
3. Architecture & data flow
Section titled “3. Architecture & data flow”┌──────────────┐ GET /register ┌──────────────────┐│ Browser │ ─────────────────────────────▶ │ Vite dev server ││ (Playwright)│ ◀───────────────────────────── │ apps/vendor-web │└──────┬───────┘ HTML + JS bundle └──────────────────┘ │ │ GET /api/v1/auth/vendor/register/modules │ POST /api/v1/auth/vendor/register { f_name, l_name, │ phone, email, │ password, module_id? } ▼┌──────────────────────────────────────────────┐│ NestJS backend (apps/backend) ││ VendorAuthController → VendorAuthService ││ └─ register(dto) ││ ├─ resolve moduleId (Retail default) ││ ├─ AdminVendorsService.create(...) ││ └─ login(dto) → owner JWT │└──────────────────────────────────────────────┘The frontend renders the form via apps/vendor-web/src/app/register/page.tsx.
Step 1 collects f_name / l_name / phone / email; Step 2 collects
store_name / store_slug / password / accept_terms. The current build
no longer asks the user for module_id — the backend defaults to the
active Retail module when the field is omitted.
Field rename:
module_idwas previously surfaced as the “Business module” dropdown and is now derived server-side. See Backend API catalog andvendor-register.dto.ts(@ApiPropertyOptional).
4. The contract under test
Section titled “4. The contract under test”4.1 Endpoints exercised
Section titled “4.1 Endpoints exercised”| Verb | Path | Where |
|---|---|---|
GET | /api/v1/health | apps/vendor-web/e2e/global-setup.ts:18 (gated) |
GET | /register | Vite dev server (webServer in playwright.config.cjs) |
GET | /api/v1/auth/vendor/register/modules | Optional — only used by the docs-screenshots spec to wait for modules to load. The current register.spec.ts does not block on it because the dropdown is hidden when the backend returns a single module or none. |
POST | /api/v1/auth/vendor/register | apps/backend/src/modules/vendor/vendor-auth.controller.ts:104 |
POST | /api/v1/auth/vendor/login | apps/backend/src/modules/vendor/vendor-auth.controller.ts (login handler) |
GET | /vendor-login | Vite dev server |
GET | /dashboard | Vite dev server |
4.2 Request shape
Section titled “4.2 Request shape”POST /api/v1/auth/vendor/register accepts:
{ "f_name": "Jane", "l_name": "Doe", "phone": "+15550001111", "email": "happy-abc@e2e.lionpos.local", "password": "HappyPwd!23", "store_name": "Happy Store", "store_slug": "happy-abc"}module_id is optional. When omitted, the backend resolves it from
AdminModulesService.listPublicForVendorRegistration() by matching
/retail/i against the module name. If no Retail module is present, the
backend still creates the vendor with no module_id set
(AdminVendorsService.create already treats module_id as optional).
4.3 Failure modes the suite asserts on
Section titled “4.3 Failure modes the suite asserts on”| Status | Body code (canonical) | Test |
|---|---|---|
400 | validation_error (Zod/class-validator) | step 1 validation (UI-level) |
409 | email_taken (Laravel) | duplicate email (HTTP-level via request.post) |
400 | slug_taken | server-side slug_taken |
200 | { token, refresh_token, zone_wise_topic, ... } | happy path, auto-suggest |
>=500 | any | explicitly rejected by every happy-path test |
5. Layout
Section titled “5. Layout”apps/vendor-web/e2e/vendor/getting-started/register.spec.ts├── beforeEach setEnglishLocale + persistent i18nextLng├── describe "happy path"│ ├── signs up an owner … welcome screen + real login│ └── omits the slug field … auto-derived slug from store name├── describe "step 1 validation"│ ├── Continue is disabled … full name / email / phone│ └── Duplicate email surfaces … HTTP 4xx, no advance to step 2├── describe "step 2 validation"│ ├── Store name is required│ ├── Invalid slug shows hint … Zod refine → inline error│ └── Terms checkbox must … disabled until accepted├── describe "slug auto-suggest …"│ ├── Auto-suggest populates … then stops once user edits│ └── Server-side slug_taken … banner visible, retry works└── describe "guards" └── Authenticated owner … /register → /dashboard redirect6. Running
Section titled “6. Running”Local (assumes NestJS is already running on :3000)
Section titled “Local (assumes NestJS is already running on :3000)”# From the repo root or apps/vendor-webnpm run test:e2e -- vendor/getting-started/register.spec.tscd apps/backendnpm run db:test:preparenpm run start:test &cd ../vendor-webnpm run test:e2e -- vendor/getting-started/register.spec.tsplaywright.config.cjs starts Vite on a dedicated port
(PLAYWRIGHT_VENDOR_WEB_PORT, default 3100) so reuseExistingServer
does not collide with a developer session. global-setup.ts waits up to
120 s for GET /api/v1/health to respond.
Environment variables
Section titled “Environment variables”| Variable | Default | Purpose |
|---|---|---|
PLAYWRIGHT_API_BASE_URL | http://127.0.0.1:3100 | Backend base URL. |
PLAYWRIGHT_VENDOR_WEB_PORT | 3100 | Vite port. |
PLAYWRIGHT_SKIP_BACKEND_HEALTH | unset | Skip the global-setup health probe. |
PLAYWRIGHT_VENDOR_OWNER_EMAIL / _PASSWORD | owner@test.nipos.local / 123123 | Reserved — the register spec generates fresh emails per run. |
DB_ENABLED | false | Backend flag — set true to use a real MySQL test database. |
7. Why every test creates a fresh email
Section titled “7. Why every test creates a fresh email”uniqueEmail(prefix) mints prefix-<base36 timestamp>-<base36 random>@e2e.lionpos.local,
and uniqueSlug(prefix) does the same for the slug. Re-running the suite
must not depend on previously seeded rows — and on DB_ENABLED=true,
re-running against an existing seed would surface stale-data failures
instead of contract failures.
The slug pattern enforces the same Zod rule as the frontend
(^[a-z][a-z0-9-]{3,40}$, must start with a letter).
8. Why we set the English locale
Section titled “8. Why we set the English locale”The register/page.tsx component reads user-visible copy from
apps/vendor-web/src/en.ts / vi.ts. The label English text is the
one thing Playwright can match via getByLabel(/full name/i). The
beforeEach writes i18nextLng=en to localStorage before the page
loads so the i18n bootstrap picks the English bundle.
If the suite is ever run against a deployment that does not ship the
English bundle, the tests will fail at getByLabel resolution. Add a
language negotiation step in setEnglishLocale if you need to support
non-English test environments.
9. Common failure modes
Section titled “9. Common failure modes”| Symptom | Likely cause | Fix |
|---|---|---|
GET /api/v1/health times out in global-setup | Backend not running, or DB_ENABLED=true and migrations not applied | See playwright.config.cjs webServer.command and the db:test:prepare script in apps/backend/package.json. |
Tests stuck on step 2 with Create account disabled | acceptTerms checkbox toggled but the form thinks it is still false | Look at form2.formState.errors.acceptTerms in apps/vendor-web/src/app/register/page.tsx; assert the checkbox name matches the RHF key. |
slug_taken banner missing on conflict test | Backend returned a 5xx instead of 4xx — possible when AdminStoresService.create propagates an unexpected error | Inspect apps/backend/src/modules/admin/admin-stores.service.ts createStoreForVendor (slug_taken branch). |
Authenticated owner is redirected fails | vendor-auth Zustand store name changed | Update page.evaluate(() => window.localStorage.getItem("vendor-auth")) lookup. |
Email already in use test intermittently passes/fails | Test order non-deterministic; another test used the same email | Already mitigated by uniqueEmail() per test; check the parallel/worker settings in playwright.config.cjs (fullyParallel: false, workers: 1). |
10. Extending the suite
Section titled “10. Extending the suite”When adding a new field to the registration form:
- Add the field to
VendorRegisterDto(apps/backend/src/modules/vendor/dto/vendor-register.dto.ts). - Update
VendorAuthService.register()(apps/backend/src/modules/vendor/vendor-auth.service.ts:248) to derive or default it. - Add a unit test in
vendor-auth.service.spec.ts. - Add a Playwright test in
register.spec.tsthat drives the form and asserts both the success path and at least one validation path. - If the field changes a user-facing API contract, update
apps/docs/src/content/docs/technical/backend-api-catalog.md.
When changing the slug_taken error shape, update the slug_taken test
expectation — currently it only asserts the HTTP status range (4xx,
not 5xx), which is intentionally lenient to survive server-side error
copy changes.
11. Cross-references
Section titled “11. Cross-references”- Frontend form:
apps/vendor-web/src/app/register/page.tsx - Backend handler:
apps/backend/src/modules/vendor/vendor-auth.controller.ts - Backend service:
apps/backend/src/modules/vendor/vendor-auth.service.ts - Backend DTO:
apps/backend/src/modules/vendor/dto/vendor-register.dto.ts - Frontend types:
apps/vendor-web/src/types/vendor-auth.types.ts - Frontend service:
apps/vendor-web/src/services/vendor-auth.api.ts - Backend e2e (Jest):
apps/backend/test/vendor-auth.e2e-spec.ts - Login form contract:
apps/vendor-web/src/app/vendor-login/page.tsx - API catalog: Backend API Catalog
- User guide: Đăng ký tài khoản