diff --git a/examples/django-react-site/.gitignore b/examples/django-react-site/.gitignore new file mode 100644 index 0000000..51511d1 --- /dev/null +++ b/examples/django-react-site/.gitignore @@ -0,0 +1 @@ +test-results/ diff --git a/examples/django-react-site/Dockerfile.test b/examples/django-react-site/Dockerfile.test index 0a6aea6..2bd8974 100644 --- a/examples/django-react-site/Dockerfile.test +++ b/examples/django-react-site/Dockerfile.test @@ -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 diff --git a/examples/django-react-site/harness/src/fixtures.tsx b/examples/django-react-site/harness/src/fixtures.tsx index 6a5c2f0..0024264 100644 --- a/examples/django-react-site/harness/src/fixtures.tsx +++ b/examples/django-react-site/harness/src/fixtures.tsx @@ -87,13 +87,13 @@ function Result({ data, error }: { data?: unknown; error?: unknown }) { // ─── Hook runner: calls a generated hook and renders result ───────────────── -function useRun(hook: () => (input?: any) => Promise, input?: any) { - const call = hook() +function useRun(hook: () => { mutate: (input?: any) => Promise }, input?: any) { + const { mutate } = hook() const [data, setData] = useState() const [error, setError] = useState() 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() - 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 } diff --git a/examples/django-react-site/mizan.spec.ts b/examples/django-react-site/mizan.spec.ts index 3a96c24..7ac6ad0 100644 --- a/examples/django-react-site/mizan.spec.ts +++ b/examples/django-react-site/mizan.spec.ts @@ -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( () => { diff --git a/frontends/mizan-base/src/index.ts b/frontends/mizan-base/src/index.ts index 9bb03ec..7ee6cf6 100644 --- a/frontends/mizan-base/src/index.ts +++ b/frontends/mizan-base/src/index.ts @@ -9,8 +9,24 @@ // === Error === export class MizanError extends Error { + public code: string + public details?: unknown + constructor(public status: number, public body: string) { super(`Mizan call failed (${status})`) + // Two envelope shapes are tolerated: + // FastAPI: {"error": {"code", "message", "details"}} + // Django: {"error": true, "code", "message", "details"} + try { + const parsed = JSON.parse(body) + const err = parsed?.error + const source = typeof err === 'object' && err !== null ? err : parsed + this.code = source?.code ?? `HTTP_${status}` + this.details = source?.details + if (source?.message) this.message = source.message + } catch { + this.code = `HTTP_${status}` + } } }