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>
166 lines
5.2 KiB
TypeScript
166 lines
5.2 KiB
TypeScript
/**
|
|
* Tests for ChannelConnection
|
|
*
|
|
* These tests verify the ChannelConnection class API.
|
|
* Unit tests for class structure don't require a real backend.
|
|
* Integration tests for actual WebSocket connections require the backend.
|
|
*
|
|
* Backend must be running for integration tests: docker-compose up
|
|
*/
|
|
|
|
import { ChannelConnection, RPCError } from '../connection'
|
|
import { describeIntegration, WS_URL } from '../../testing'
|
|
|
|
describe('ChannelConnection (unit tests)', () => {
|
|
describe('construction', () => {
|
|
it('should start in disconnected state', () => {
|
|
const connection = new ChannelConnection({ url: 'ws://localhost/ws/' })
|
|
expect(connection.status).toBe('disconnected')
|
|
})
|
|
|
|
})
|
|
|
|
describe('status change handlers', () => {
|
|
it('should allow subscribing to status changes', () => {
|
|
const connection = new ChannelConnection({ url: 'ws://localhost/ws/' })
|
|
const handler = jest.fn()
|
|
|
|
const unsubscribe = connection.onStatusChange(handler)
|
|
|
|
expect(typeof unsubscribe).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('message handlers', () => {
|
|
it('should allow subscribing to messages', () => {
|
|
const connection = new ChannelConnection({ url: 'ws://localhost/ws/' })
|
|
const handler = jest.fn()
|
|
|
|
const unsubscribe = connection.onMessage(handler)
|
|
|
|
expect(typeof unsubscribe).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('send queueing', () => {
|
|
it('should queue messages when not connected', () => {
|
|
const connection = new ChannelConnection({
|
|
url: 'ws://localhost/ws/',
|
|
reconnect: false,
|
|
})
|
|
|
|
// This shouldn't throw
|
|
connection.send({
|
|
action: 'subscribe',
|
|
channel: 'test',
|
|
params: {},
|
|
})
|
|
|
|
// Status should still be disconnected (or connecting if it auto-connected)
|
|
expect(['disconnected', 'connecting']).toContain(connection.status)
|
|
})
|
|
})
|
|
|
|
describe('rpc', () => {
|
|
it('should queue rpc messages when not connected', () => {
|
|
const connection = new ChannelConnection({
|
|
url: 'ws://localhost/ws/',
|
|
reconnect: false,
|
|
})
|
|
|
|
const promise = connection.rpc('test_fn', { arg: 'value' })
|
|
|
|
expect(promise).toBeInstanceOf(Promise)
|
|
})
|
|
})
|
|
})
|
|
|
|
describeIntegration('ChannelConnection (integration)', () => {
|
|
describe('real WebSocket connection', () => {
|
|
it('should connect to real backend WebSocket', async () => {
|
|
const connection = new ChannelConnection({
|
|
url: WS_URL,
|
|
reconnect: false,
|
|
})
|
|
|
|
const statusChanges: string[] = []
|
|
connection.onStatusChange((status) => {
|
|
statusChanges.push(status)
|
|
})
|
|
|
|
// Connect
|
|
connection.connect()
|
|
|
|
// Wait for connection
|
|
await new Promise<void>((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error('Connection timeout'))
|
|
}, 5000)
|
|
|
|
const unsubscribe = connection.onStatusChange((status) => {
|
|
if (status === 'connected') {
|
|
clearTimeout(timeout)
|
|
unsubscribe()
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
|
|
expect(connection.status).toBe('connected')
|
|
|
|
// Cleanup
|
|
connection.disconnect()
|
|
})
|
|
|
|
it('should disconnect cleanly', async () => {
|
|
const connection = new ChannelConnection({
|
|
url: WS_URL,
|
|
reconnect: false,
|
|
})
|
|
|
|
// Connect first
|
|
connection.connect()
|
|
await new Promise<void>((resolve) => {
|
|
const unsubscribe = connection.onStatusChange((status) => {
|
|
if (status === 'connected') {
|
|
unsubscribe()
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
|
|
// Now disconnect
|
|
connection.disconnect()
|
|
|
|
// Should be disconnected
|
|
expect(connection.status).toBe('disconnected')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('RPCError', () => {
|
|
it('should be an Error subclass', () => {
|
|
const error = new RPCError('TEST_CODE', 'Test message')
|
|
|
|
expect(error).toBeInstanceOf(Error)
|
|
expect(error).toBeInstanceOf(RPCError)
|
|
})
|
|
|
|
it('should have correct properties', () => {
|
|
const error = new RPCError('VALIDATION_ERROR', 'Field is required', { field: 'email' })
|
|
|
|
expect(error.code).toBe('VALIDATION_ERROR')
|
|
expect(error.message).toBe('Field is required')
|
|
expect(error.details).toEqual({ field: 'email' })
|
|
expect(error.name).toBe('RPCError')
|
|
})
|
|
|
|
it('should work without details', () => {
|
|
const error = new RPCError('NOT_FOUND', 'Function not found')
|
|
|
|
expect(error.code).toBe('NOT_FOUND')
|
|
expect(error.message).toBe('Function not found')
|
|
expect(error.details).toBeUndefined()
|
|
})
|
|
})
|