Move desktop and e2e into examples/ directory
- 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>
This commit is contained in:
22
examples/django-react-site/harness/django.config.mjs
Normal file
22
examples/django-react-site/harness/django.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
|
||||
export default {
|
||||
projectId: 'e2e-harness',
|
||||
|
||||
source: {
|
||||
django: {
|
||||
managePath: path.join(root, 'examples/django-react-site/backend/manage.py'),
|
||||
command: [path.join(root, 'django/.venv/bin/python')],
|
||||
env: {
|
||||
PYTHONPATH: `${path.join(root, 'django/src')}:${path.join(root, 'examples/django-react-site/backend')}`,
|
||||
DJANGO_SETTINGS_MODULE: 'testapp.settings',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
output: 'src/api/generated.ts',
|
||||
}
|
||||
5
examples/django-react-site/harness/index.html
Normal file
5
examples/django-react-site/harness/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /><title>mizan E2E Harness</title></head>
|
||||
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
|
||||
</html>
|
||||
22
examples/django-react-site/harness/package.json
Normal file
22
examples/django-react-site/harness/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "mizan-e2e-harness",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite --port 5174"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rythazhur/mizan": "file:../../react",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
|
||||
import { useChannel, type ChannelSubscription } from 'mizan/channels'
|
||||
|
||||
import type { ChatParams, ChatReactMessage, ChatDjangoMessage, NotificationsDjangoMessage, PresenceDjangoMessage, PrivateDjangoMessage } from './generated.channels'
|
||||
|
||||
// ============================================================================
|
||||
// Channel Hooks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook for the chat channel.
|
||||
*/
|
||||
export function useChatChannel(params: ChatParams): ChannelSubscription<ChatParams, ChatDjangoMessage, ChatReactMessage> {
|
||||
return useChannel('chat', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the notifications channel.
|
||||
*/
|
||||
export function useNotificationsChannel(): ChannelSubscription<Record<string, never>, NotificationsDjangoMessage, never> {
|
||||
return useChannel('notifications', {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the presence channel.
|
||||
*/
|
||||
export function usePresenceChannel(): ChannelSubscription<Record<string, never>, PresenceDjangoMessage, never> {
|
||||
return useChannel('presence', {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the private channel.
|
||||
*/
|
||||
export function usePrivateChannel(): ChannelSubscription<Record<string, never>, PrivateDjangoMessage, never> {
|
||||
return useChannel('private', {})
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "mizan Channels",
|
||||
"version": "1.0.0",
|
||||
"description": "Auto-generated schema for mizan channels"
|
||||
},
|
||||
"paths": {
|
||||
"/channels/chat/params": {
|
||||
"post": {
|
||||
"operationId": "chatParams",
|
||||
"summary": "Chat channel params",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BaseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChatParams"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/channels/chat/react": {
|
||||
"post": {
|
||||
"operationId": "chatReactMessage",
|
||||
"summary": "Chat React→Django message",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BaseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChatReactMessage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/channels/chat/django": {
|
||||
"post": {
|
||||
"operationId": "chatDjangoMessage",
|
||||
"summary": "Chat Django→React message",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ChatDjangoMessage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/channels/notifications/django": {
|
||||
"post": {
|
||||
"operationId": "notificationsDjangoMessage",
|
||||
"summary": "Notifications Django→React message",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationsDjangoMessage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/channels/presence/django": {
|
||||
"post": {
|
||||
"operationId": "presenceDjangoMessage",
|
||||
"summary": "Presence Django→React message",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PresenceDjangoMessage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/channels/private/django": {
|
||||
"post": {
|
||||
"operationId": "privateDjangoMessage",
|
||||
"summary": "Private Django→React message",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PrivateDjangoMessage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"BaseModel": {
|
||||
"properties": {},
|
||||
"title": "BaseModel",
|
||||
"type": "object"
|
||||
},
|
||||
"ChatParams": {
|
||||
"properties": {
|
||||
"room": {
|
||||
"title": "Room",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"room"
|
||||
],
|
||||
"title": "ChatParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ChatReactMessage": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"title": "ChatReactMessage",
|
||||
"type": "object"
|
||||
},
|
||||
"ChatDjangoMessage": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"title": "ChatDjangoMessage",
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationsDjangoMessage": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"title": "NotificationsDjangoMessage",
|
||||
"type": "object"
|
||||
},
|
||||
"PresenceDjangoMessage": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"title": "Value",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"value"
|
||||
],
|
||||
"title": "PresenceDjangoMessage",
|
||||
"type": "object"
|
||||
},
|
||||
"PrivateDjangoMessage": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"title": "PrivateDjangoMessage",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [],
|
||||
"x-mizan-channels": [
|
||||
{
|
||||
"name": "chat",
|
||||
"pascalName": "Chat",
|
||||
"hasParams": true,
|
||||
"hasReactMessage": true,
|
||||
"hasDjangoMessage": true,
|
||||
"paramsType": "ChatParams",
|
||||
"reactMessageType": "ChatReactMessage",
|
||||
"djangoMessageType": "ChatDjangoMessage"
|
||||
},
|
||||
{
|
||||
"name": "notifications",
|
||||
"pascalName": "Notifications",
|
||||
"hasParams": false,
|
||||
"hasReactMessage": false,
|
||||
"hasDjangoMessage": true,
|
||||
"djangoMessageType": "NotificationsDjangoMessage"
|
||||
},
|
||||
{
|
||||
"name": "presence",
|
||||
"pascalName": "Presence",
|
||||
"hasParams": false,
|
||||
"hasReactMessage": false,
|
||||
"hasDjangoMessage": true,
|
||||
"djangoMessageType": "PresenceDjangoMessage"
|
||||
},
|
||||
{
|
||||
"name": "private",
|
||||
"pascalName": "Private",
|
||||
"hasParams": false,
|
||||
"hasReactMessage": false,
|
||||
"hasDjangoMessage": true,
|
||||
"djangoMessageType": "PrivateDjangoMessage"
|
||||
}
|
||||
]
|
||||
}
|
||||
337
examples/django-react-site/harness/src/api/generated.channels.ts
Normal file
337
examples/django-react-site/harness/src/api/generated.channels.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
|
||||
// ============================================================================
|
||||
// OpenAPI Types (generated by openapi-typescript)
|
||||
// ============================================================================
|
||||
|
||||
export interface paths {
|
||||
"/channels/chat/params": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Chat channel params */
|
||||
post: operations["chatParams"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/channels/chat/react": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Chat React→Django message */
|
||||
post: operations["chatReactMessage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/channels/chat/django": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Chat Django→React message */
|
||||
post: operations["chatDjangoMessage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/channels/notifications/django": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Notifications Django→React message */
|
||||
post: operations["notificationsDjangoMessage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/channels/presence/django": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Presence Django→React message */
|
||||
post: operations["presenceDjangoMessage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/channels/private/django": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Private Django→React message */
|
||||
post: operations["privateDjangoMessage"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/** BaseModel */
|
||||
BaseModel: Record<string, never>;
|
||||
/** ChatParams */
|
||||
ChatParams: {
|
||||
/** Room */
|
||||
room: string;
|
||||
};
|
||||
/** ChatReactMessage */
|
||||
ChatReactMessage: {
|
||||
/** Text */
|
||||
text: string;
|
||||
};
|
||||
/** ChatDjangoMessage */
|
||||
ChatDjangoMessage: {
|
||||
/** Text */
|
||||
text: string;
|
||||
};
|
||||
/** NotificationsDjangoMessage */
|
||||
NotificationsDjangoMessage: {
|
||||
/** Text */
|
||||
text: string;
|
||||
};
|
||||
/** PresenceDjangoMessage */
|
||||
PresenceDjangoMessage: {
|
||||
/** Value */
|
||||
value: number;
|
||||
};
|
||||
/** PrivateDjangoMessage */
|
||||
PrivateDjangoMessage: {
|
||||
/** Text */
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
chatParams: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChatParams"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["BaseModel"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
chatReactMessage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChatReactMessage"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["BaseModel"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
chatDjangoMessage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ChatDjangoMessage"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
notificationsDjangoMessage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["NotificationsDjangoMessage"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
presenceDjangoMessage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PresenceDjangoMessage"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
privateDjangoMessage: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PrivateDjangoMessage"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Convenience Type Exports
|
||||
// ============================================================================
|
||||
|
||||
export type ChatParams = components["schemas"]["ChatParams"]
|
||||
export type ChatReactMessage = components["schemas"]["ChatReactMessage"]
|
||||
export type ChatDjangoMessage = components["schemas"]["ChatDjangoMessage"]
|
||||
export type NotificationsDjangoMessage = components["schemas"]["NotificationsDjangoMessage"]
|
||||
export type PresenceDjangoMessage = components["schemas"]["PresenceDjangoMessage"]
|
||||
export type PrivateDjangoMessage = components["schemas"]["PrivateDjangoMessage"]
|
||||
|
||||
// ============================================================================
|
||||
// Channel Registry
|
||||
// ============================================================================
|
||||
|
||||
export const CHANNELS = {
|
||||
chat: {
|
||||
name: 'chat',
|
||||
pascalName: 'Chat',
|
||||
hasParams: true,
|
||||
hasReactMessage: true,
|
||||
hasDjangoMessage: true,
|
||||
paramsType: 'ChatParams',
|
||||
reactMessageType: 'ChatReactMessage',
|
||||
djangoMessageType: 'ChatDjangoMessage',
|
||||
},
|
||||
notifications: {
|
||||
name: 'notifications',
|
||||
pascalName: 'Notifications',
|
||||
hasParams: false,
|
||||
hasReactMessage: false,
|
||||
hasDjangoMessage: true,
|
||||
djangoMessageType: 'NotificationsDjangoMessage',
|
||||
},
|
||||
presence: {
|
||||
name: 'presence',
|
||||
pascalName: 'Presence',
|
||||
hasParams: false,
|
||||
hasReactMessage: false,
|
||||
hasDjangoMessage: true,
|
||||
djangoMessageType: 'PresenceDjangoMessage',
|
||||
},
|
||||
private: {
|
||||
name: 'private',
|
||||
pascalName: 'Private',
|
||||
hasParams: false,
|
||||
hasReactMessage: false,
|
||||
hasDjangoMessage: true,
|
||||
djangoMessageType: 'PrivateDjangoMessage',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,62 @@
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
//
|
||||
// Server-side functions for SSR hydration.
|
||||
// These run in Next.js server components/layouts.
|
||||
|
||||
import type { currentUserOutput, greetOutput } from './generated.mizan'
|
||||
|
||||
// ============================================================================
|
||||
// Hydration Types
|
||||
// ============================================================================
|
||||
|
||||
/** Typed hydration data for SSR */
|
||||
export interface DjangoHydration {
|
||||
currentUser?: currentUserOutput
|
||||
greet?: greetOutput
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SSR Hydration Helper
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch hydration data for SSR.
|
||||
*
|
||||
* Call this in your server component:
|
||||
* const hydration = await getDjangoHydration(client)
|
||||
* return <DjangoContext hydration={hydration}>...</DjangoContext>
|
||||
*/
|
||||
export async function getDjangoHydration(
|
||||
client: { request: (method: string, url: string, body?: unknown) => Promise<Response> }
|
||||
): Promise<DjangoHydration> {
|
||||
const hydration: DjangoHydration = {}
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
client.request('POST', '/api/mizan/call/', { fn: 'current_user', args: {} }),
|
||||
client.request('POST', '/api/mizan/call/', { fn: 'greet', args: {} }),
|
||||
])
|
||||
|
||||
if (results[0].status === 'fulfilled') {
|
||||
const data = await (results[0] as PromiseFulfilledResult<Response>).value.json()
|
||||
if (data.error) {
|
||||
console.error('[getDjangoHydration] current_user failed:', data.code, data.message)
|
||||
} else {
|
||||
hydration.currentUser = data.data
|
||||
}
|
||||
} else {
|
||||
console.error('[getDjangoHydration] current_user request failed:', (results[0] as PromiseRejectedResult).reason)
|
||||
}
|
||||
if (results[1].status === 'fulfilled') {
|
||||
const data = await (results[1] as PromiseFulfilledResult<Response>).value.json()
|
||||
if (data.error) {
|
||||
console.error('[getDjangoHydration] greet failed:', data.code, data.message)
|
||||
} else {
|
||||
hydration.greet = data.data
|
||||
}
|
||||
} else {
|
||||
console.error('[getDjangoHydration] greet request failed:', (results[1] as PromiseRejectedResult).reason)
|
||||
}
|
||||
|
||||
return hydration
|
||||
}
|
||||
257
examples/django-react-site/harness/src/api/generated.django.tsx
Normal file
257
examples/django-react-site/harness/src/api/generated.django.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
'use client'
|
||||
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
|
||||
// This file provides typed wrappers around the mizan library.
|
||||
// - DjangoContext: Typed provider wrapping mizanProvider
|
||||
// - Typed hooks: useAuthStatus(), useUser(), etc.
|
||||
|
||||
import { type ReactNode, useCallback } from 'react'
|
||||
import {
|
||||
mizanProvider,
|
||||
usemizan,
|
||||
usemizanContext,
|
||||
usemizanCall,
|
||||
type mizanHydration,
|
||||
type Transport,
|
||||
} from 'mizan'
|
||||
import { ChannelProvider, ChannelConnection } from 'mizan/channels'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import type { addEmailSchemaOutput, addEmailValidateInput, addEmailValidateOutput, addInput, addOutput, buggyFnOutput, contactSchemaInput, contactSchemaOutput, contactSubmitOutput, contactValidateInput, contactValidateOutput, currentUserOutput, echoInput, echoOutput, greetInput, greetOutput, httpOnlyEchoInput, httpOnlyEchoOutput, itemFormsetSchemaInput, itemFormsetSchemaOutput, itemFormsetSubmitInput, itemFormsetSubmitOutput, itemFormsetValidateInput, itemFormsetValidateOutput, itemSchemaInput, itemSchemaOutput, itemSubmitOutput, itemValidateInput, itemValidateOutput, jwtObtainOutput, jwtRefreshInput, jwtRefreshOutput, loginSchemaOutput, loginSubmitInput, loginSubmitOutput, loginValidateInput, loginValidateOutput, multiplyInput, multiplyOutput, notImplementedFnOutput, permissionCheckFnInput, permissionCheckFnOutput, signupSchemaOutput, signupSubmitInput, signupSubmitOutput, signupValidateInput, signupValidateOutput, staffOnlyOutput, superuserOnlyOutput, verifiedOnlyOutput, whoamiOutput, wsWhoamiOutput } from './generated.mizan'
|
||||
|
||||
// ============================================================================
|
||||
// Hydration Types
|
||||
// ============================================================================
|
||||
|
||||
/** Typed hydration data for SSR */
|
||||
export interface DjangoHydration {
|
||||
currentUser?: currentUserOutput
|
||||
greet?: greetOutput
|
||||
}
|
||||
|
||||
/** Convert typed hydration to mizan format */
|
||||
function tomizanHydration(hydration?: DjangoHydration): mizanHydration | undefined {
|
||||
if (!hydration) return undefined
|
||||
const result: mizanHydration = {}
|
||||
if (hydration.currentUser !== undefined) result['current_user'] = hydration.currentUser
|
||||
if (hydration.greet !== undefined) result['greet'] = hydration.greet
|
||||
return result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Provider
|
||||
// ============================================================================
|
||||
|
||||
export interface DjangoContextProps {
|
||||
children: ReactNode
|
||||
/** SSR hydration data */
|
||||
hydration?: DjangoHydration
|
||||
/** WebSocket URL for RPC calls (default: /ws/) */
|
||||
wsUrl?: string
|
||||
/** Base URL for HTTP fallback (default: /api/mizan) */
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed Django context provider.
|
||||
*
|
||||
* Wraps mizanProvider with:
|
||||
* - Typed hydration
|
||||
* - Auto-fetch for registered contexts
|
||||
*
|
||||
* Usage:
|
||||
* <DjangoContext hydration={hydration}>
|
||||
* <App />
|
||||
* </DjangoContext>
|
||||
*/
|
||||
export function DjangoContext({
|
||||
children,
|
||||
hydration,
|
||||
wsUrl,
|
||||
baseUrl,
|
||||
}: DjangoContextProps) {
|
||||
const connectionRef = useRef<ChannelConnection | null>(null)
|
||||
if (!connectionRef.current) {
|
||||
connectionRef.current = new ChannelConnection({ url: wsUrl || '/ws/' })
|
||||
}
|
||||
|
||||
return (
|
||||
<mizanProvider
|
||||
hydration={tomizanHydration(hydration)}
|
||||
contexts={['current_user', 'greet']}
|
||||
wsUrl={wsUrl}
|
||||
baseUrl={baseUrl}
|
||||
connection={connectionRef.current}
|
||||
>
|
||||
<ChannelProvider connection={connectionRef.current} autoConnect={true}>
|
||||
{children}
|
||||
</ChannelProvider>
|
||||
</mizanProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Context Hooks (typed wrappers)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get current_user context data.
|
||||
* @throws if context not loaded yet
|
||||
*/
|
||||
export function useCurrentUser(): currentUserOutput {
|
||||
const data = usemizanContext<currentUserOutput>('current_user')
|
||||
if (data === undefined) {
|
||||
throw new Error('useCurrentUser: context not loaded yet')
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get greet context data.
|
||||
* @throws if context not loaded yet
|
||||
*/
|
||||
export function useGreet(): greetOutput {
|
||||
const data = usemizanContext<greetOutput>('greet')
|
||||
if (data === undefined) {
|
||||
throw new Error('useGreet: context not loaded yet')
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context refresh functions without subscribing to data changes.
|
||||
* Use this in components that only need to trigger refreshes.
|
||||
*/
|
||||
export function useDjangoRefresh() {
|
||||
const { refreshContext, refreshAllContexts } = usemizan()
|
||||
return {
|
||||
refreshCurrentUser: () => refreshContext('current_user'),
|
||||
refreshGreet: () => refreshContext('greet'),
|
||||
refreshAll: refreshAllContexts,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Function Hooks (typed wrappers)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Call echo server function.
|
||||
* Transport: websocket
|
||||
*/
|
||||
export function useEcho() {
|
||||
return usemizanCall<echoInput, echoOutput>('echo', 'websocket')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call add server function.
|
||||
* Transport: websocket
|
||||
*/
|
||||
export function useAdd() {
|
||||
return usemizanCall<addInput, addOutput>('add', 'websocket')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whoami server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useWhoami() {
|
||||
return usemizanCall<void, whoamiOutput>('whoami', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call http_only_echo server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useHttpOnlyEcho() {
|
||||
return usemizanCall<httpOnlyEchoInput, httpOnlyEchoOutput>('http_only_echo', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call staff_only server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useStaffOnly() {
|
||||
return usemizanCall<void, staffOnlyOutput>('staff_only', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call superuser_only server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useSuperuserOnly() {
|
||||
return usemizanCall<void, superuserOnlyOutput>('superuser_only', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call verified_only server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useVerifiedOnly() {
|
||||
return usemizanCall<void, verifiedOnlyOutput>('verified_only', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call multiply server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useMultiply() {
|
||||
return usemizanCall<multiplyInput, multiplyOutput>('multiply', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call not_implemented_fn server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useNotImplementedFn() {
|
||||
return usemizanCall<void, notImplementedFnOutput>('not_implemented_fn', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call buggy_fn server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useBuggyFn() {
|
||||
return usemizanCall<void, buggyFnOutput>('buggy_fn', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call permission_check_fn server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function usePermissionCheckFn() {
|
||||
return usemizanCall<permissionCheckFnInput, permissionCheckFnOutput>('permission_check_fn', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call ws_whoami server function.
|
||||
* Transport: websocket
|
||||
*/
|
||||
export function useWsWhoami() {
|
||||
return usemizanCall<void, wsWhoamiOutput>('ws_whoami', 'websocket')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call jwt_obtain server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useJwtObtain() {
|
||||
return usemizanCall<void, jwtObtainOutput>('jwt_obtain', 'http')
|
||||
}
|
||||
|
||||
/**
|
||||
* Call jwt_refresh server function.
|
||||
* Transport: http
|
||||
*/
|
||||
export function useJwtRefresh() {
|
||||
return usemizanCall<jwtRefreshInput, jwtRefreshOutput>('jwt_refresh', 'http')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports from mizan library
|
||||
// ============================================================================
|
||||
|
||||
export { usemizan, usemizanStatus, usePush, DjangoError } from 'mizan'
|
||||
export type { ConnectionStatus, PushMessage, PushListener } from 'mizan'
|
||||
File diff suppressed because it is too large
Load Diff
2123
examples/django-react-site/harness/src/api/generated.djarea.ts
Normal file
2123
examples/django-react-site/harness/src/api/generated.djarea.ts
Normal file
File diff suppressed because it is too large
Load Diff
226
examples/django-react-site/harness/src/api/generated.forms.ts
Normal file
226
examples/django-react-site/harness/src/api/generated.forms.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
|
||||
// Typed form hooks with Zod validation.
|
||||
// Zod schemas are generated from Django form field definitions.
|
||||
// Client-side validation matches Django constraints (required, max_length, email, etc.)
|
||||
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
useDjangoFormCore,
|
||||
useDjangoFormsetCore,
|
||||
type DjangoFormState,
|
||||
type DjangoFormsetState,
|
||||
type FormOptions,
|
||||
} from 'mizan'
|
||||
|
||||
// ============================================================================
|
||||
// Zod Schemas
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Zod schema for login form
|
||||
* Generated from Django form field definitions
|
||||
*/
|
||||
export const LoginSchema = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for signup form
|
||||
* Generated from Django form field definitions
|
||||
*/
|
||||
export const SignupSchema = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for add_email form
|
||||
* Generated from Django form field definitions
|
||||
*/
|
||||
export const AddEmailSchema = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for contact form
|
||||
* Generated from Django form field definitions
|
||||
*/
|
||||
export const ContactSchema = z.object({
|
||||
name: z.string().max(100),
|
||||
email: z.string().email('Invalid email address').max(320),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for item form
|
||||
* Generated from Django form field definitions
|
||||
*/
|
||||
export const ItemSchema = z.object({
|
||||
label: z.string().max(50),
|
||||
quantity: z.number().int().min(1),
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Form Data Types (inferred from Zod schemas)
|
||||
// ============================================================================
|
||||
|
||||
/** Form data type for login, inferred from Zod schema */
|
||||
export type LoginFormData = z.infer<typeof LoginSchema>
|
||||
|
||||
/** Form data type for signup, inferred from Zod schema */
|
||||
export type SignupFormData = z.infer<typeof SignupSchema>
|
||||
|
||||
/** Form data type for add_email, inferred from Zod schema */
|
||||
export type AddEmailFormData = z.infer<typeof AddEmailSchema>
|
||||
|
||||
/** Form data type for contact, inferred from Zod schema */
|
||||
export type ContactFormData = z.infer<typeof ContactSchema>
|
||||
|
||||
/** Form data type for item, inferred from Zod schema */
|
||||
export type ItemFormData = z.infer<typeof ItemSchema>
|
||||
|
||||
// ============================================================================
|
||||
// Form Hooks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Typed form hook for login
|
||||
*
|
||||
* Features:
|
||||
* - Full TypeScript inference for form fields
|
||||
* - Client-side Zod validation (instant feedback)
|
||||
* - Server-side Django validation (authoritative)
|
||||
*/
|
||||
export function useLoginForm(
|
||||
options?: FormOptions
|
||||
): DjangoFormState<LoginFormData> {
|
||||
return useDjangoFormCore<LoginFormData>({
|
||||
name: 'login',
|
||||
zodSchema: LoginSchema,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed form hook for signup
|
||||
*
|
||||
* Features:
|
||||
* - Full TypeScript inference for form fields
|
||||
* - Client-side Zod validation (instant feedback)
|
||||
* - Server-side Django validation (authoritative)
|
||||
*/
|
||||
export function useSignupForm(
|
||||
options?: FormOptions
|
||||
): DjangoFormState<SignupFormData> {
|
||||
return useDjangoFormCore<SignupFormData>({
|
||||
name: 'signup',
|
||||
zodSchema: SignupSchema,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed form hook for add_email
|
||||
*
|
||||
* Features:
|
||||
* - Full TypeScript inference for form fields
|
||||
* - Client-side Zod validation (instant feedback)
|
||||
* - Server-side Django validation (authoritative)
|
||||
*/
|
||||
export function useAddEmailForm(
|
||||
options?: FormOptions
|
||||
): DjangoFormState<AddEmailFormData> {
|
||||
return useDjangoFormCore<AddEmailFormData>({
|
||||
name: 'add_email',
|
||||
zodSchema: AddEmailSchema,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed form hook for contact
|
||||
*
|
||||
* Features:
|
||||
* - Full TypeScript inference for form fields
|
||||
* - Client-side Zod validation (instant feedback)
|
||||
* - Server-side Django validation (authoritative)
|
||||
*/
|
||||
export function useContactForm(
|
||||
options?: FormOptions
|
||||
): DjangoFormState<ContactFormData> {
|
||||
return useDjangoFormCore<ContactFormData>({
|
||||
name: 'contact',
|
||||
zodSchema: ContactSchema,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed form hook for item
|
||||
*
|
||||
* Features:
|
||||
* - Full TypeScript inference for form fields
|
||||
* - Client-side Zod validation (instant feedback)
|
||||
* - Server-side Django validation (authoritative)
|
||||
*/
|
||||
export function useItemForm(
|
||||
options?: FormOptions
|
||||
): DjangoFormState<ItemFormData> {
|
||||
return useDjangoFormCore<ItemFormData>({
|
||||
name: 'item',
|
||||
zodSchema: ItemSchema,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed formset hook for item
|
||||
*/
|
||||
export function useItemFormset(
|
||||
initialCount?: number,
|
||||
liveValidation?: boolean
|
||||
): DjangoFormsetState<ItemFormData> {
|
||||
return useDjangoFormsetCore<ItemFormData>({
|
||||
name: 'item',
|
||||
zodSchema: ItemSchema,
|
||||
initialCount,
|
||||
liveValidation,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Form Registry
|
||||
// ============================================================================
|
||||
|
||||
export const DJANGO_FORMS = {
|
||||
login: {
|
||||
name: 'login',
|
||||
schema: LoginSchema,
|
||||
hook: 'useLoginForm',
|
||||
hasFormset: false,
|
||||
},
|
||||
signup: {
|
||||
name: 'signup',
|
||||
schema: SignupSchema,
|
||||
hook: 'useSignupForm',
|
||||
hasFormset: false,
|
||||
},
|
||||
addEmail: {
|
||||
name: 'add_email',
|
||||
schema: AddEmailSchema,
|
||||
hook: 'useAddEmailForm',
|
||||
hasFormset: false,
|
||||
},
|
||||
contact: {
|
||||
name: 'contact',
|
||||
schema: ContactSchema,
|
||||
hook: 'useContactForm',
|
||||
hasFormset: false,
|
||||
},
|
||||
item: {
|
||||
name: 'item',
|
||||
schema: ItemSchema,
|
||||
hook: 'useItemForm',
|
||||
hasFormset: true,
|
||||
},
|
||||
} as const
|
||||
90
examples/django-react-site/harness/src/api/index.ts
Normal file
90
examples/django-react-site/harness/src/api/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* mizan API - Consolidated Exports
|
||||
*
|
||||
* Import everything from here:
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import {
|
||||
* DjangoContext,
|
||||
* useUser,
|
||||
* useEcho,
|
||||
* useChatChannel,
|
||||
* DjangoError,
|
||||
* } from '@/api'
|
||||
* ```
|
||||
*/
|
||||
|
||||
// AUTO-GENERATED by mizan - do not edit manually
|
||||
// Regenerate with: npm run schemas
|
||||
|
||||
// =============================================================================
|
||||
// mizan Provider & Hooks
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
getDjangoHydration,
|
||||
type DjangoHydration,
|
||||
} from './generated.django.server'
|
||||
|
||||
export {
|
||||
// Provider
|
||||
DjangoContext,
|
||||
type DjangoContextProps,
|
||||
|
||||
// Context hooks
|
||||
useCurrentUser,
|
||||
useGreet,
|
||||
|
||||
// Refresh hooks
|
||||
useDjangoRefresh,
|
||||
|
||||
// Function hooks
|
||||
useEcho,
|
||||
useAdd,
|
||||
useWhoami,
|
||||
useHttpOnlyEcho,
|
||||
useStaffOnly,
|
||||
useSuperuserOnly,
|
||||
useVerifiedOnly,
|
||||
useMultiply,
|
||||
useNotImplementedFn,
|
||||
useBuggyFn,
|
||||
usePermissionCheckFn,
|
||||
useWsWhoami,
|
||||
useJwtObtain,
|
||||
useJwtRefresh,
|
||||
|
||||
// Re-exports from mizan library
|
||||
usemizan,
|
||||
usemizanStatus,
|
||||
usePush,
|
||||
DjangoError,
|
||||
type ConnectionStatus,
|
||||
type PushMessage,
|
||||
type PushListener,
|
||||
} from './generated.django'
|
||||
|
||||
// =============================================================================
|
||||
// Channel Hooks
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
useChatChannel,
|
||||
useNotificationsChannel,
|
||||
usePresenceChannel,
|
||||
usePrivateChannel,
|
||||
} from './generated.channels.hooks'
|
||||
|
||||
// =============================================================================
|
||||
// Channel Types
|
||||
// =============================================================================
|
||||
|
||||
export type {
|
||||
ChatParams,
|
||||
ChatReactMessage,
|
||||
ChatDjangoMessage,
|
||||
NotificationsDjangoMessage,
|
||||
PresenceDjangoMessage,
|
||||
PrivateDjangoMessage,
|
||||
} from './generated.channels'
|
||||
264
examples/django-react-site/harness/src/fixtures.tsx
Normal file
264
examples/django-react-site/harness/src/fixtures.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* 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>
|
||||
)
|
||||
}
|
||||
13
examples/django-react-site/harness/src/main.tsx
Normal file
13
examples/django-react-site/harness/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { DjangoContext } from './api/generated.django'
|
||||
import { Fixtures } from './fixtures'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<DjangoContext baseUrl="/api/mizan">
|
||||
<Fixtures />
|
||||
</DjangoContext>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
11
examples/django-react-site/harness/tsconfig.json
Normal file
11
examples/django-react-site/harness/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
30
examples/django-react-site/harness/vite.config.ts
Normal file
30
examples/django-react-site/harness/vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
const reactPkg = path.resolve(__dirname, '../../react/src')
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
||||
'mizan/client/react': path.join(reactPkg, 'client/react.ts'),
|
||||
'mizan/client/nextjs': path.join(reactPkg, 'client/nextjs.tsx'),
|
||||
'mizan/client': path.join(reactPkg, 'client/index.ts'),
|
||||
'mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
||||
'mizan/allauth/nextjs': path.join(reactPkg, 'allauth/nextjs.tsx'),
|
||||
'mizan/allauth': path.join(reactPkg, 'allauth/index.ts'),
|
||||
'mizan': path.join(reactPkg, 'index.ts'),
|
||||
'@rythazhur/mizan/channels': path.join(reactPkg, 'channels/index.ts'),
|
||||
'@rythazhur/mizan/jwt': path.join(reactPkg, 'jwt/index.ts'),
|
||||
'@rythazhur/mizan': path.join(reactPkg, 'index.ts'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8000',
|
||||
'/ws': { target: 'ws://localhost:8000', ws: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user