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:
1
examples/django-react-site/.gitignore
vendored
Normal file
1
examples/django-react-site/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
test-results/
|
||||
@@ -7,6 +7,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install mizan-core (shared Python protocol primitives) first
|
||||
COPY cores/mizan-python/ /app/mizan-core/
|
||||
RUN pip install --no-cache-dir /app/mizan-core
|
||||
|
||||
# Install mizan from local source with channels support
|
||||
COPY backends/mizan-django/ /app/django/
|
||||
RUN pip install --no-cache-dir /app/django[channels] daphne
|
||||
|
||||
@@ -87,13 +87,13 @@ function Result({ data, error }: { data?: unknown; error?: unknown }) {
|
||||
|
||||
// ─── Hook runner: calls a generated hook and renders result ─────────────────
|
||||
|
||||
function useRun<T>(hook: () => (input?: any) => Promise<T>, input?: any) {
|
||||
const call = hook()
|
||||
function useRun<T>(hook: () => { mutate: (input?: any) => Promise<T> }, input?: any) {
|
||||
const { mutate } = hook()
|
||||
const [data, setData] = useState<T>()
|
||||
const [error, setError] = useState<unknown>()
|
||||
|
||||
useEffect(() => {
|
||||
call(input).then(setData).catch(setError)
|
||||
mutate(input).then(setData).catch(setError)
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { data, error }
|
||||
@@ -126,9 +126,9 @@ function NotFound() {
|
||||
|
||||
function ValidationError() {
|
||||
// Send wrong types to add (strings instead of numbers)
|
||||
const call = useAdd()
|
||||
const { mutate } = useAdd()
|
||||
const [error, setError] = useState<unknown>()
|
||||
useEffect(() => { (call as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [call])
|
||||
useEffect(() => { (mutate as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [mutate])
|
||||
return <Result error={error} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user