/**
* 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 {
DjangoContext,
useEcho,
useAdd,
useMultiply,
useWhoami,
useStaffOnly,
useSuperuserOnly,
useVerifiedOnly,
useNotImplementedFn,
useBuggyFn,
usePermissionCheckFn,
useCurrentUser,
DjangoError,
useMizan,
} from './api/generated.django'
import { useContactForm, useLoginForm } from './api/generated.forms'
import { useChatChannel } from './api/generated.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
case 'add': return
case 'multiply': return
case 'not-found': return
case 'validation-error': return
case 'auth-required': return
case 'staff-only': return
case 'superuser-only': return
case 'verified-only': return
case 'not-implemented': return
case 'internal-error': return
case 'permission-error': return
case 'permission-success': return
case 'context-current-user': return
case 'form-login-schema': return
case 'form-contact-schema': return
case 'form-contact-submit': return
case 'channel-chat': return
default: return
Harness ready. Set #hash.
}
}
// ─── Result helper ──────────────────────────────────────────────────────────
function Result({ data, error }: { data?: unknown; error?: unknown }) {
return (
<>
{data !== undefined && (
{JSON.stringify(data)}
)}
{error !== undefined && error !== null && (
<>
{error instanceof DjangoError ? 'DjangoError' : 'Error'}
{error instanceof DjangoError ? error.code : ''}
{error instanceof Error ? error.message : String(error)}
>
)}
>
)
}
// ─── Hook runner: calls a generated hook and renders result ─────────────────
function useRun(hook: () => (input?: any) => Promise, input?: any) {
const call = hook()
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
call(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
}
function Add() {
const { data, error } = useRun(useAdd, { a: 17, b: 25 })
return
}
function Multiply() {
const { data, error } = useRun(useMultiply, { x: 6, y: 7 })
return
}
function NotFound() {
// Deliberately call a non-existent function via the raw primitive
const { call } = useMizan()
const [error, setError] = useState()
useEffect(() => { call('does_not_exist').catch(setError) }, [call])
return
}
function ValidationError() {
// Send wrong types to add (strings instead of numbers)
const call = useAdd()
const [error, setError] = useState()
useEffect(() => { (call as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [call])
return
}
function AuthRequired() {
const { data, error } = useRun(useWhoami)
return
}
function StaffOnly() {
const { data, error } = useRun(useStaffOnly)
return
}
function SuperuserOnly() {
const { data, error } = useRun(useSuperuserOnly)
return
}
function VerifiedOnly() {
const { data, error } = useRun(useVerifiedOnly)
return
}
function NotImplemented() {
const { data, error } = useRun(useNotImplementedFn)
return
}
function InternalError() {
const { data, error } = useRun(useBuggyFn)
return
}
function PermissionError_() {
const { data, error } = useRun(usePermissionCheckFn, { secret: 'wrong' })
return
}
function PermissionSuccess() {
const { data, error } = useRun(usePermissionCheckFn, { secret: 'open-sesame' })
return
}
// ─── Context fixtures ───────────────────────────────────────────────────────
function ContextCurrentUser() {
// useCurrentUser throws if context not loaded yet, so catch that
try {
const user = useCurrentUser()
return {JSON.stringify(user)}
} catch {
return loading context...
}
}
// ─── Form fixtures (using generated form hooks) ─────────────────────────────
function FormLoginSchema() {
const form = useLoginForm()
if (form.loading) return loading...
return {JSON.stringify(form.schema)}
}
function FormContactSchema() {
const form = useContactForm()
if (form.loading) return loading...
return {JSON.stringify(form.schema)}
}
function FormContactSubmit() {
const form = useContactForm()
const [result, setResult] = useState()
const [submitted, setSubmitted] = useState(false)
useEffect(() => {
if (!form.loading && !submitted) {
form.set('name', 'Test User')
form.set('email', 'test@example.com')
form.set('message', 'Hello from e2e')
setSubmitted(true)
}
}, [form.loading, submitted, form])
useEffect(() => {
if (submitted && !result) {
form.submit().then(setResult)
}
}, [submitted, result, form])
if (!result) return loading...
return {JSON.stringify(result)}
}
// ─── Channel fixtures ───────────────────────────────────────────────────────
function ChannelChatFixture() {
// DjangoContext already includes ChannelProvider
return
}
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 (
{chat.status}
{chat.messages.length}
{chat.messages.length > 0 && (
{JSON.stringify(chat.messages[chat.messages.length - 1])}
)}
)
}