Full test infrastructure, code audit fixes, and real E2E integration tests
Test infrastructure: - Django standalone test runner (pytest-django, test settings, EmailUser model) - React unit tests via Vitest with jsdom, jest compat layer, path aliases - Playwright E2E tests using generated hooks in a real Chromium browser - Docker Compose test backend (Django + Redis) for integration testing - Desktop integration test app (PyWebView + Django + uvicorn) - Makefile with test/test-django/test-react/test-integration targets Library bugs found and fixed: - hasJWT truthiness: undefined !== null was true, skipping session init - process.env crash: CSR client referenced process.env in non-Node browsers - baseUrl not forwarded: DjareaProvider didn't pass baseUrl to CSR client - Relative URL handling: new URL() failed with relative base paths - call() race condition: HTTP requests fired before CSRF cookie was set - Session init await: added sessionRef promise so call() waits for session - path_prefix on schema export: both export commands failed with URL reverse - NullBooleanField removed: referenced field doesn't exist in Django 5.0+ - lru_cache on JWT settings: get_settings() now cached as intended - Channel message routing: broadcasts now include channel name and params - httpFunctionCall: fixed URL and request body format Generator fixes: - Removed 1,100 lines of REST/OpenAPI client generation (not part of Djarea) - Generator now works for djarea-only projects without django-ninja REST APIs - Generated DjangoContext now includes ChannelProvider when channels exist - Fixed env var passthrough for schema export commands - Deduplicated fetch logic into single runDjangoCommand helper Test quality: - Fixed 33 tautological Django tests with real assertions - Found hidden bug: benchmark functions were never registered - Found hidden bug: unicode lookalike test used plain ASCII - Deleted worthless React unit tests (duplicates, shape checks, Zod-tests-Zod) - Replaced jsdom integration tests with Playwright browser tests Example apps: - example/: Integration test backend with 33 server functions, 5 forms, 4 channels covering auth variations, contexts, class-based ServerFunction, error codes, DjareaFormMixin, formsets, and JWT - desktop/: PyWebView desktop app with file system access, SQLite CRUD, system introspection, and 39 real HTTP integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
186
e2e/djarea.spec.ts
Normal file
186
e2e/djarea.spec.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Djarea E2E Integration Tests
|
||||
*
|
||||
* Real Chromium → Real React app (generated hooks) → Real Django backend
|
||||
*
|
||||
* Every test uses the generated Djarea 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 DjareaFormMeta', 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user