Vendor self-registration — Playwright E2E reference
Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.
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