- desktop/ → examples/django-react-desktop-app/ - e2e/ → examples/django-react-site/ - example/ → examples/django-react-site/backend/ - Update Dockerfile.test, Makefile, playwright config, and django.config.mjs path references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
265 lines
9.1 KiB
TypeScript
265 lines
9.1 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 {
|
|
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 <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 />
|
|
case 'form-login-schema': return <FormLoginSchema />
|
|
case 'form-contact-schema': return <FormContactSchema />
|
|
case 'form-contact-submit': return <FormContactSubmit />
|
|
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 DjangoError ? 'DjangoError' : 'Error'}
|
|
</div>
|
|
<div data-testid="error-code">
|
|
{error instanceof DjangoError ? 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: () => (input?: any) => Promise<T>, input?: any) {
|
|
const call = hook()
|
|
const [data, setData] = useState<T>()
|
|
const [error, setError] = useState<unknown>()
|
|
|
|
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 <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 call = useAdd()
|
|
const [error, setError] = useState<unknown>()
|
|
useEffect(() => { (call as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [call])
|
|
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>
|
|
}
|
|
}
|
|
|
|
// ─── Form fixtures (using generated form hooks) ─────────────────────────────
|
|
|
|
function FormLoginSchema() {
|
|
const form = useLoginForm()
|
|
if (form.loading) return <div>loading...</div>
|
|
return <pre data-testid="result">{JSON.stringify(form.schema)}</pre>
|
|
}
|
|
|
|
function FormContactSchema() {
|
|
const form = useContactForm()
|
|
if (form.loading) return <div>loading...</div>
|
|
return <pre data-testid="result">{JSON.stringify(form.schema)}</pre>
|
|
}
|
|
|
|
function FormContactSubmit() {
|
|
const form = useContactForm()
|
|
const [result, setResult] = useState<unknown>()
|
|
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 <div>loading...</div>
|
|
return <pre data-testid="result">{JSON.stringify(result)}</pre>
|
|
}
|
|
|
|
// ─── Channel fixtures ───────────────────────────────────────────────────────
|
|
|
|
function ChannelChatFixture() {
|
|
// DjangoContext 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>
|
|
)
|
|
}
|