- 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>
187 lines
7.5 KiB
TypeScript
187 lines
7.5 KiB
TypeScript
/**
|
|
* mizan E2E Integration Tests
|
|
*
|
|
* Real Chromium → Real React app (generated hooks) → Real Django backend
|
|
*
|
|
* Every test uses the generated mizan API, not raw call() or fetch().
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test'
|
|
|
|
const BASE = process.env.HARNESS_URL || 'http://localhost:5174'
|
|
|
|
async function fixture(page: any, name: string) {
|
|
await page.goto(`${BASE}#${name}`)
|
|
await page.waitForSelector('[data-testid="result"], [data-testid="error-type"]', { timeout: 10000 })
|
|
}
|
|
|
|
async function getResult(page: any): Promise<any> {
|
|
const el = page.locator('[data-testid="result"]')
|
|
if (await el.count() > 0) return JSON.parse(await el.textContent())
|
|
return null
|
|
}
|
|
|
|
async function getError(page: any) {
|
|
const typeEl = page.locator('[data-testid="error-type"]')
|
|
if (await typeEl.count() === 0) return null
|
|
return {
|
|
type: await typeEl.textContent(),
|
|
code: await page.locator('[data-testid="error-code"]').textContent(),
|
|
message: await page.locator('[data-testid="error-message"]').textContent(),
|
|
}
|
|
}
|
|
|
|
// ─── useEcho, useAdd, useMultiply ───────────────────────────────────────────
|
|
|
|
test.describe('generated function hooks', () => {
|
|
test('useEcho returns echoed text', async ({ page }) => {
|
|
await fixture(page, 'echo')
|
|
const result = await getResult(page)
|
|
expect(result.message).toContain('e2e-test')
|
|
})
|
|
|
|
test('useAdd returns correct sum', async ({ page }) => {
|
|
await fixture(page, 'add')
|
|
const result = await getResult(page)
|
|
expect(result.result).toBe(42)
|
|
})
|
|
|
|
test('useMultiply (class-based ServerFunction) returns product', async ({ page }) => {
|
|
await fixture(page, 'multiply')
|
|
const result = await getResult(page)
|
|
expect(result.product).toBe(42)
|
|
})
|
|
|
|
test('usePermissionCheckFn succeeds with correct secret', async ({ page }) => {
|
|
await fixture(page, 'permission-success')
|
|
const result = await getResult(page)
|
|
expect(result.message).toBe('access granted')
|
|
})
|
|
})
|
|
|
|
// ─── Error handling ─────────────────────────────────────────────────────────
|
|
|
|
test.describe('error codes from generated hooks', () => {
|
|
test('non-existent function → DjangoError NOT_FOUND', async ({ page }) => {
|
|
await fixture(page, 'not-found')
|
|
const error = await getError(page)
|
|
expect(error!.type).toBe('DjangoError')
|
|
expect(error!.code).toBe('NOT_FOUND')
|
|
})
|
|
|
|
test('wrong input types → DjangoError VALIDATION_ERROR', async ({ page }) => {
|
|
await fixture(page, 'validation-error')
|
|
const error = await getError(page)
|
|
expect(error!.type).toBe('DjangoError')
|
|
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(['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(['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(['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(['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!.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!.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!.code).toBe('FORBIDDEN')
|
|
})
|
|
})
|
|
|
|
// ─── Context hooks ──────────────────────────────────────────────────────────
|
|
|
|
test.describe('generated context hooks', () => {
|
|
test('useCurrentUser returns anonymous data', async ({ page }) => {
|
|
await page.goto(`${BASE}#context-current-user`)
|
|
// Context loads async, wait for result
|
|
await page.waitForSelector('[data-testid="result"]', { timeout: 10000 })
|
|
const result = await getResult(page)
|
|
expect(result.authenticated).toBe(false)
|
|
expect(result.email).toBe('')
|
|
})
|
|
})
|
|
|
|
// ─── 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')
|
|
})
|
|
})
|
|
|
|
// ─── Channel hooks ──────────────────────────────────────────────────────────
|
|
|
|
test.describe('generated channel hooks', () => {
|
|
test('useChatChannel receives echoed message', async ({ page }) => {
|
|
await page.goto(`${BASE}#channel-chat`)
|
|
await page.waitForFunction(
|
|
() => {
|
|
const el = document.querySelector('[data-testid="channel-message-count"]')
|
|
return el && parseInt(el.textContent || '0') > 0
|
|
},
|
|
{ timeout: 15000 }
|
|
)
|
|
const msg = JSON.parse(await page.locator('[data-testid="channel-last-message"]').textContent())
|
|
expect(msg.text).toBe('hello from e2e')
|
|
})
|
|
})
|