Skip to content

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).

There are two adjacent Playwright specs that already touch /register:

SpecPurposeWhere
docs-register-screenshots.spec.tsCaptures 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.

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: Continue stays 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_taken 4xx is surfaced via the inline error banner and the form remains usable so the user can retry.
  • Already-authenticated owners are redirected from /register to the vendor dashboard root.

It does not cover:

  • Backend unit tests for VendorAuthService.register() — see apps/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.
┌──────────────┐ 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_id was previously surfaced as the “Business module” dropdown and is now derived server-side. See Backend API catalog and vendor-register.dto.ts (@ApiPropertyOptional).

VerbPathWhere
GET/api/v1/healthapps/vendor-web/e2e/global-setup.ts:18 (gated)
GET/registerVite dev server (webServer in playwright.config.cjs)
GET/api/v1/auth/vendor/register/modulesOptional — 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/registerapps/backend/src/modules/vendor/vendor-auth.controller.ts:104
POST/api/v1/auth/vendor/loginapps/backend/src/modules/vendor/vendor-auth.controller.ts (login handler)
GET/vendor-loginVite dev server
GET/dashboardVite dev server

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).

StatusBody code (canonical)Test
400validation_error (Zod/class-validator)step 1 validation (UI-level)
409email_taken (Laravel)duplicate email (HTTP-level via request.post)
400slug_takenserver-side slug_taken
200{ token, refresh_token, zone_wise_topic, ... }happy path, auto-suggest
>=500anyexplicitly rejected by every happy-path test
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 redirect

Local (assumes NestJS is already running on :3000)

Section titled “Local (assumes NestJS is already running on :3000)”
Terminal window
# From the repo root or apps/vendor-web
npm run test:e2e -- vendor/getting-started/register.spec.ts
Terminal window
cd apps/backend
npm run db:test:prepare
npm run start:test &
cd ../vendor-web
npm run test:e2e -- vendor/getting-started/register.spec.ts

playwright.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.

VariableDefaultPurpose
PLAYWRIGHT_API_BASE_URLhttp://127.0.0.1:3100Backend base URL.
PLAYWRIGHT_VENDOR_WEB_PORT3100Vite port.
PLAYWRIGHT_SKIP_BACKEND_HEALTHunsetSkip the global-setup health probe.
PLAYWRIGHT_VENDOR_OWNER_EMAIL / _PASSWORDowner@test.nipos.local / 123123Reserved — the register spec generates fresh emails per run.
DB_ENABLEDfalseBackend flag — set true to use a real MySQL test database.

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).

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.

SymptomLikely causeFix
GET /api/v1/health times out in global-setupBackend not running, or DB_ENABLED=true and migrations not appliedSee playwright.config.cjs webServer.command and the db:test:prepare script in apps/backend/package.json.
Tests stuck on step 2 with Create account disabledacceptTerms checkbox toggled but the form thinks it is still falseLook 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 testBackend returned a 5xx instead of 4xx — possible when AdminStoresService.create propagates an unexpected errorInspect apps/backend/src/modules/admin/admin-stores.service.ts createStoreForVendor (slug_taken branch).
Authenticated owner is redirected failsvendor-auth Zustand store name changedUpdate page.evaluate(() => window.localStorage.getItem("vendor-auth")) lookup.
Email already in use test intermittently passes/failsTest order non-deterministic; another test used the same emailAlready mitigated by uniqueEmail() per test; check the parallel/worker settings in playwright.config.cjs (fullyParallel: false, workers: 1).

When adding a new field to the registration form:

  1. Add the field to VendorRegisterDto (apps/backend/src/modules/vendor/dto/vendor-register.dto.ts).
  2. Update VendorAuthService.register() (apps/backend/src/modules/vendor/vendor-auth.service.ts:248) to derive or default it.
  3. Add a unit test in vendor-auth.service.spec.ts.
  4. Add a Playwright test in register.spec.ts that drives the form and asserts both the success path and at least one validation path.
  5. 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.

  • 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