Files
mizan/examples/django-react-site/harness/src/fixtures.tsx
Ryth Azhur aaaf80cdbf 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>
2026-05-06 17:38:52 -04:00

224 lines
7.7 KiB
TypeScript

/**
* E2E Test Fixtures
*
* Each fixture uses GENERATED mizan hooks (not raw call()).
* Playwright reads the DOM to verify behavior.
*
* URL hash selects the fixture: #echo, #add, #multiply, etc.
*/
import { useState, useEffect, useRef } from 'react'
// Generated typed hooks — the actual mizan API
import {
MizanContext,
useEcho,
useAdd,
useMultiply,
useWhoami,
useStaffOnly,
useSuperuserOnly,
useVerifiedOnly,
useNotImplementedFn,
useBuggyFn,
usePermissionCheckFn,
useCurrentUser,
MizanError,
useMizan,
} from './api'
import { useChatChannel } from './api/channels.hooks'
// ─── Fixture router ─────────────────────────────────────────────────────────
export function Fixtures() {
const [hash, setHash] = useState(window.location.hash.slice(1))
useEffect(() => {
const onHash = () => setHash(window.location.hash.slice(1))
window.addEventListener('hashchange', onHash)
return () => window.removeEventListener('hashchange', onHash)
}, [])
switch (hash) {
case 'echo': return <Echo />
case 'add': return <Add />
case 'multiply': return <Multiply />
case 'not-found': return <NotFound />
case 'validation-error': return <ValidationError />
case 'auth-required': return <AuthRequired />
case 'staff-only': return <StaffOnly />
case 'superuser-only': return <SuperuserOnly />
case 'verified-only': return <VerifiedOnly />
case 'not-implemented': return <NotImplemented />
case 'internal-error': return <InternalError />
case 'permission-error': return <PermissionError_ />
case 'permission-success': return <PermissionSuccess />
case 'context-current-user': return <ContextCurrentUser />
// Form fixtures removed — forms codegen deferred per Blazr scope
case 'channel-chat': return <ChannelChatFixture />
default: return <div data-testid="ready">Harness ready. Set #hash.</div>
}
}
// ─── Result helper ──────────────────────────────────────────────────────────
function Result({ data, error }: { data?: unknown; error?: unknown }) {
return (
<>
{data !== undefined && (
<pre data-testid="result">{JSON.stringify(data)}</pre>
)}
{error !== undefined && error !== null && (
<>
<div data-testid="error-type">
{error instanceof MizanError ? 'MizanError' : 'Error'}
</div>
<div data-testid="error-code">
{error instanceof MizanError ? error.code : ''}
</div>
<pre data-testid="error-message">
{error instanceof Error ? error.message : String(error)}
</pre>
</>
)}
</>
)
}
// ─── Hook runner: calls a generated hook and renders result ─────────────────
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(() => {
mutate(input).then(setData).catch(setError)
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return { data, error }
}
// ─── Server function fixtures ───────────────────────────────────────────────
function Echo() {
const { data, error } = useRun(useEcho, { text: 'e2e-test' })
return <Result data={data} error={error} />
}
function Add() {
const { data, error } = useRun(useAdd, { a: 17, b: 25 })
return <Result data={data} error={error} />
}
function Multiply() {
const { data, error } = useRun(useMultiply, { x: 6, y: 7 })
return <Result data={data} error={error} />
}
function NotFound() {
// Deliberately call a non-existent function via the raw primitive
const { call } = useMizan()
const [error, setError] = useState<unknown>()
useEffect(() => { call('does_not_exist').catch(setError) }, [call])
return <Result error={error} />
}
function ValidationError() {
// Send wrong types to add (strings instead of numbers)
const { mutate } = useAdd()
const [error, setError] = useState<unknown>()
useEffect(() => { (mutate as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [mutate])
return <Result error={error} />
}
function AuthRequired() {
const { data, error } = useRun(useWhoami)
return <Result data={data} error={error} />
}
function StaffOnly() {
const { data, error } = useRun(useStaffOnly)
return <Result data={data} error={error} />
}
function SuperuserOnly() {
const { data, error } = useRun(useSuperuserOnly)
return <Result data={data} error={error} />
}
function VerifiedOnly() {
const { data, error } = useRun(useVerifiedOnly)
return <Result data={data} error={error} />
}
function NotImplemented() {
const { data, error } = useRun(useNotImplementedFn)
return <Result data={data} error={error} />
}
function InternalError() {
const { data, error } = useRun(useBuggyFn)
return <Result data={data} error={error} />
}
function PermissionError_() {
const { data, error } = useRun(usePermissionCheckFn, { secret: 'wrong' })
return <Result data={data} error={error} />
}
function PermissionSuccess() {
const { data, error } = useRun(usePermissionCheckFn, { secret: 'open-sesame' })
return <Result data={data} error={error} />
}
// ─── Context fixtures ───────────────────────────────────────────────────────
function ContextCurrentUser() {
// useCurrentUser throws if context not loaded yet, so catch that
try {
const user = useCurrentUser()
return <pre data-testid="result">{JSON.stringify(user)}</pre>
} catch {
return <div>loading context...</div>
}
}
// ─── Channel fixtures ───────────────────────────────────────────────────────
function ChannelChatFixture() {
// MizanContext already includes ChannelProvider
return <ChannelChat />
}
function ChannelChat() {
const chat = useChatChannel({ room: 'e2e' })
const [sent, setSent] = useState(false)
const prevStatus = useRef(chat.status)
useEffect(() => {
// Send once when status transitions to 'connected' (meaning subscribed)
// The hook maps subscribed → 'connected', but we need to wait for it
// to go through 'connecting' first (before subscription is confirmed)
const wasConnecting = prevStatus.current === 'connecting'
prevStatus.current = chat.status
if (wasConnecting && chat.status === 'connected' && !sent) {
chat.send({ text: 'hello from e2e' })
setSent(true)
}
}, [chat.status, sent, chat])
return (
<div>
<div data-testid="channel-status">{chat.status}</div>
<div data-testid="channel-message-count">{chat.messages.length}</div>
{chat.messages.length > 0 && (
<pre data-testid="channel-last-message">
{JSON.stringify(chat.messages[chat.messages.length - 1])}
</pre>
)}
</div>
)
}