End-to-end: harness Playwright suite green (14 pass, 1 skip)

After the React-codegen rework, ran the full e2e harness against the
docker-stack backend. Surfaced and fixed real friction:

mizan-base/src/index.ts (kernel):
- MizanError now parses both error envelopes — the FastAPI shape
  ({"error": {"code", "message", "details"}}) and the Django shape
  ({"error": true, "code", "message", "details"}). Exposes .code and
  .details on the thrown error so consumer code can branch on them.
  This was needed for the harness's `instanceof MizanError && error.code
  === 'NOT_FOUND'` pattern to work; the previous MizanError only carried
  status + raw body, leaving callers to parse the body themselves.

examples/django-react-site/Dockerfile.test:
- Backend image now copies and installs cores/mizan-python before
  installing mizan-django (which imports from mizan_core after the
  Layer 1 extraction).

harness/src/fixtures.tsx:
- useRun helper updated for the new mutation-hook shape: pulls
  { mutate } off the hook result instead of treating the hook return
  as a callable. Same for ValidationError fixture.

mizan.spec.ts:
- DjangoError → MizanError (kernel error class is backend-agnostic).
- Form tests removed (forms codegen deferred per Blazr scope).
- Channel test marked test.skip (channels deferred per Blazr scope).

.gitignore: ignore Playwright test-results/.

Final verification across all surfaces:
- mizan-core unit:       15/15
- mizan-django unit:     348 pass, 21 skip
- mizan-fastapi unit:    11/11
- mizan-ts edge-compat:  34/34 (cross-language HMAC pin)
- harness e2e (Playwright): 14/15 (1 skip = channels deferred)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 17:38:52 -04:00
parent 2982741aad
commit aaaf80cdbf
5 changed files with 39 additions and 45 deletions

View File

@@ -62,66 +62,66 @@ test.describe('generated function hooks', () => {
// ─── Error handling ─────────────────────────────────────────────────────────
test.describe('error codes from generated hooks', () => {
test('non-existent function → DjangoError NOT_FOUND', async ({ page }) => {
test('non-existent function → MizanError NOT_FOUND', async ({ page }) => {
await fixture(page, 'not-found')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(error!.code).toBe('NOT_FOUND')
})
test('wrong input types → DjangoError VALIDATION_ERROR', async ({ page }) => {
test('wrong input types → MizanError VALIDATION_ERROR', async ({ page }) => {
await fixture(page, 'validation-error')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(error!.code).toBe('VALIDATION_ERROR')
})
test('useWhoami anonymous → auth error', async ({ page }) => {
await fixture(page, 'auth-required')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
})
test('useStaffOnly anonymous → UNAUTHORIZED', async ({ page }) => {
await fixture(page, 'staff-only')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
})
test('useSuperuserOnly anonymous → UNAUTHORIZED', async ({ page }) => {
await fixture(page, 'superuser-only')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
})
test('useVerifiedOnly anonymous → FORBIDDEN', async ({ page }) => {
await fixture(page, 'verified-only')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
})
test('useNotImplementedFn → NOT_IMPLEMENTED', async ({ page }) => {
await fixture(page, 'not-implemented')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(error!.code).toBe('NOT_IMPLEMENTED')
})
test('useBuggyFn → INTERNAL_ERROR', async ({ page }) => {
await fixture(page, 'internal-error')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(error!.code).toBe('INTERNAL_ERROR')
})
test('usePermissionCheckFn wrong secret → FORBIDDEN', async ({ page }) => {
await fixture(page, 'permission-error')
const error = await getError(page)
expect(error!.type).toBe('DjangoError')
expect(error!.type).toBe('MizanError')
expect(error!.code).toBe('FORBIDDEN')
})
})
@@ -139,39 +139,12 @@ test.describe('generated context hooks', () => {
})
})
// ─── Form hooks ─────────────────────────────────────────────────────────────
test.describe('generated form hooks', () => {
test('useLoginForm loads schema with field definitions', async ({ page }) => {
await fixture(page, 'form-login-schema')
const result = await getResult(page)
expect(result.fields).toBeDefined()
expect(result.fields.login).toBeDefined()
expect(result.fields.password).toBeDefined()
})
test('useContactForm loads schema with mizanFormMeta', async ({ page }) => {
await fixture(page, 'form-contact-schema')
const result = await getResult(page)
expect(result.title).toBe('Contact Us')
expect(result.subtitle).toBe("We'd love to hear from you")
expect(result.submit_label).toBe('Send Message')
expect(result.meta.live_validation).toBe(true)
})
test('useContactForm submit returns on_submit_success data', async ({ page }) => {
await fixture(page, 'form-contact-submit')
const result = await getResult(page)
expect(result.success).toBe(true)
expect(result.data.received).toBe(true)
expect(result.data.from).toBe('test@example.com')
})
})
// ─── Form hooks ─── (removed; forms codegen deferred per Blazr scope) ──────
// ─── Channel hooks ──────────────────────────────────────────────────────────
test.describe('generated channel hooks', () => {
test('useChatChannel receives echoed message', async ({ page }) => {
test.skip('useChatChannel receives echoed message', async ({ page }) => { // channels deferred per Blazr scope
await page.goto(`${BASE}#channel-chat`)
await page.waitForFunction(
() => {