@donkeylabs/server 2.0.19 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/caching-strategies.md +677 -0
- package/docs/dev-experience.md +656 -0
- package/docs/hot-reload-limitations.md +166 -0
- package/docs/load-testing.md +974 -0
- package/docs/plugin-registry-design.md +1064 -0
- package/docs/production.md +1229 -0
- package/docs/workflows.md +90 -3
- package/package.json +1 -1
- package/src/admin/routes.ts +153 -0
- package/src/core/cron.ts +90 -9
- package/src/core/index.ts +31 -0
- package/src/core/job-adapter-kysely.ts +176 -73
- package/src/core/job-adapter-sqlite.ts +10 -0
- package/src/core/jobs.ts +112 -17
- package/src/core/migrations/workflows/002_add_metadata_column.ts +28 -0
- package/src/core/process-adapter-kysely.ts +62 -21
- package/src/core/storage-adapter-local.test.ts +199 -0
- package/src/core/storage.test.ts +197 -0
- package/src/core/workflow-adapter-kysely.ts +66 -19
- package/src/core/workflow-executor.ts +239 -0
- package/src/core/workflow-proxy.ts +238 -0
- package/src/core/workflow-socket.ts +449 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +758 -0
- package/src/core/workflows.ts +705 -595
- package/src/core.ts +17 -6
- package/src/index.ts +14 -0
- package/src/testing/database.test.ts +263 -0
- package/src/testing/database.ts +173 -0
- package/src/testing/e2e.test.ts +189 -0
- package/src/testing/e2e.ts +272 -0
- package/src/testing/index.ts +18 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// packages/server/src/testing/e2e.ts
|
|
2
|
+
/**
|
|
3
|
+
* E2E Testing Utilities for DonkeyLabs Applications
|
|
4
|
+
*
|
|
5
|
+
* Simplifies Playwright integration for testing your DonkeyLabs app.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // playwright.config.ts
|
|
10
|
+
* import { defineE2EConfig } from "@donkeylabs/server";
|
|
11
|
+
*
|
|
12
|
+
* export default defineE2EConfig({
|
|
13
|
+
* baseURL: "http://localhost:3000",
|
|
14
|
+
* serverEntry: "./src/server/index.ts",
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // tests/auth.spec.ts
|
|
21
|
+
* import { test, expect } from "@donkeylabs/server";
|
|
22
|
+
*
|
|
23
|
+
* test("user can sign up", async ({ page, api }) => {
|
|
24
|
+
* await page.goto("/signup");
|
|
25
|
+
* await page.fill('[name="email"]', "test@example.com");
|
|
26
|
+
* await page.fill('[name="password"]', "password123");
|
|
27
|
+
* await page.click('button[type="submit"]');
|
|
28
|
+
*
|
|
29
|
+
* await expect(page).toHaveURL("/dashboard");
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// Only import types - actual @playwright/test is a peer dependency
|
|
35
|
+
// Users should import { test, expect } from "@playwright/test" directly
|
|
36
|
+
type PlaywrightTestConfig = {
|
|
37
|
+
testDir?: string;
|
|
38
|
+
timeout?: number;
|
|
39
|
+
expect?: { timeout?: number };
|
|
40
|
+
fullyParallel?: boolean;
|
|
41
|
+
forbidOnly?: boolean;
|
|
42
|
+
retries?: number;
|
|
43
|
+
workers?: number | undefined;
|
|
44
|
+
reporter?: any[];
|
|
45
|
+
use?: Record<string, any>;
|
|
46
|
+
projects?: any[];
|
|
47
|
+
webServer?: {
|
|
48
|
+
command: string;
|
|
49
|
+
url: string;
|
|
50
|
+
reuseExistingServer?: boolean;
|
|
51
|
+
timeout?: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface E2EConfig {
|
|
56
|
+
/** Base URL of your application */
|
|
57
|
+
baseURL: string;
|
|
58
|
+
|
|
59
|
+
/** Server entry point file */
|
|
60
|
+
serverEntry?: string;
|
|
61
|
+
|
|
62
|
+
/** Port for test server */
|
|
63
|
+
port?: number;
|
|
64
|
+
|
|
65
|
+
/** Database to use for testing (isolated per test) */
|
|
66
|
+
database?: "sqlite" | "postgres" | "mysql";
|
|
67
|
+
|
|
68
|
+
/** Auto-start dev server */
|
|
69
|
+
autoStart?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Test timeout in milliseconds */
|
|
72
|
+
timeout?: number;
|
|
73
|
+
|
|
74
|
+
/** Browsers to test */
|
|
75
|
+
browsers?: ("chromium" | "firefox" | "webkit")[];
|
|
76
|
+
|
|
77
|
+
/** Mobile viewport testing */
|
|
78
|
+
testMobile?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface E2EFixtures {
|
|
82
|
+
/** API client for making HTTP requests */
|
|
83
|
+
api: {
|
|
84
|
+
get: (route: string) => Promise<any>;
|
|
85
|
+
post: (route: string, data: any) => Promise<any>;
|
|
86
|
+
put: (route: string, data: any) => Promise<any>;
|
|
87
|
+
delete: (route: string) => Promise<any>;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Database instance for direct queries */
|
|
91
|
+
db: any;
|
|
92
|
+
|
|
93
|
+
/** Helper to seed test data */
|
|
94
|
+
seed: (data: { users?: any[]; [key: string]: any[] | undefined }) => Promise<void>;
|
|
95
|
+
|
|
96
|
+
/** Helper to cleanup test data */
|
|
97
|
+
cleanup: () => Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Define E2E test configuration for Playwright
|
|
102
|
+
*/
|
|
103
|
+
export function defineE2EConfig(config: E2EConfig): PlaywrightTestConfig {
|
|
104
|
+
const port = config.port || 3333;
|
|
105
|
+
const baseURL = config.baseURL || `http://localhost:${port}`;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
testDir: "./e2e",
|
|
109
|
+
timeout: config.timeout || 30000,
|
|
110
|
+
expect: {
|
|
111
|
+
timeout: 5000,
|
|
112
|
+
},
|
|
113
|
+
fullyParallel: true,
|
|
114
|
+
forbidOnly: !!process.env.CI,
|
|
115
|
+
retries: process.env.CI ? 2 : 0,
|
|
116
|
+
workers: process.env.CI ? 1 : undefined,
|
|
117
|
+
reporter: [
|
|
118
|
+
["html"],
|
|
119
|
+
["list"],
|
|
120
|
+
],
|
|
121
|
+
use: {
|
|
122
|
+
baseURL,
|
|
123
|
+
trace: "on-first-retry",
|
|
124
|
+
screenshot: "only-on-failure",
|
|
125
|
+
video: "on-first-retry",
|
|
126
|
+
},
|
|
127
|
+
projects: [
|
|
128
|
+
{
|
|
129
|
+
name: "chromium",
|
|
130
|
+
use: {
|
|
131
|
+
browserName: "chromium",
|
|
132
|
+
viewport: { width: 1280, height: 720 },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
...(config.browsers?.includes("firefox") ? [{
|
|
136
|
+
name: "firefox",
|
|
137
|
+
use: { browserName: "firefox" as const },
|
|
138
|
+
}] : []),
|
|
139
|
+
...(config.browsers?.includes("webkit") ? [{
|
|
140
|
+
name: "webkit",
|
|
141
|
+
use: { browserName: "webkit" as const },
|
|
142
|
+
}] : []),
|
|
143
|
+
...(config.testMobile ? [
|
|
144
|
+
{
|
|
145
|
+
name: "Mobile Chrome",
|
|
146
|
+
use: {
|
|
147
|
+
browserName: "chromium" as const,
|
|
148
|
+
...devices["Pixel 5"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "Mobile Safari",
|
|
153
|
+
use: {
|
|
154
|
+
browserName: "webkit" as const,
|
|
155
|
+
...devices["iPhone 12"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
] : []),
|
|
159
|
+
],
|
|
160
|
+
webServer: config.autoStart !== false ? {
|
|
161
|
+
command: config.serverEntry
|
|
162
|
+
? `bun ${config.serverEntry}`
|
|
163
|
+
: "bun run dev",
|
|
164
|
+
url: baseURL,
|
|
165
|
+
reuseExistingServer: !process.env.CI,
|
|
166
|
+
timeout: 120 * 1000,
|
|
167
|
+
} : undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Import devices from playwright
|
|
172
|
+
const devices = {
|
|
173
|
+
"Pixel 5": {
|
|
174
|
+
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36",
|
|
175
|
+
viewport: { width: 393, height: 727 },
|
|
176
|
+
deviceScaleFactor: 2.75,
|
|
177
|
+
isMobile: true,
|
|
178
|
+
hasTouch: true,
|
|
179
|
+
},
|
|
180
|
+
"iPhone 12": {
|
|
181
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15",
|
|
182
|
+
viewport: { width: 390, height: 664 },
|
|
183
|
+
deviceScaleFactor: 3,
|
|
184
|
+
isMobile: true,
|
|
185
|
+
hasTouch: true,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create E2E fixtures for Playwright tests
|
|
191
|
+
* Use this in your playwright.config.ts fixtures
|
|
192
|
+
*/
|
|
193
|
+
export function createE2EFixtures(baseURL: string) {
|
|
194
|
+
return {
|
|
195
|
+
api: async ({}, use: (api: E2EFixtures["api"]) => Promise<void>) => {
|
|
196
|
+
const api: E2EFixtures["api"] = {
|
|
197
|
+
async get(route: string) {
|
|
198
|
+
const response = await fetch(`${baseURL}/${route}`);
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
throw new Error(`API Error: ${response.status}`);
|
|
201
|
+
}
|
|
202
|
+
return response.json();
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async post(route: string, data: any) {
|
|
206
|
+
const response = await fetch(`${baseURL}/${route}`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify(data),
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const error = await response.text();
|
|
213
|
+
throw new Error(`API Error: ${response.status} - ${error}`);
|
|
214
|
+
}
|
|
215
|
+
return response.json();
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
async put(route: string, data: any) {
|
|
219
|
+
const response = await fetch(`${baseURL}/${route}`, {
|
|
220
|
+
method: "PUT",
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
body: JSON.stringify(data),
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`API Error: ${response.status}`);
|
|
226
|
+
}
|
|
227
|
+
return response.json();
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async delete(route: string) {
|
|
231
|
+
const response = await fetch(`${baseURL}/${route}`, {
|
|
232
|
+
method: "DELETE",
|
|
233
|
+
});
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
throw new Error(`API Error: ${response.status}`);
|
|
236
|
+
}
|
|
237
|
+
return response.json();
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
await use(api);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
seed: async ({}, use: (fn: E2EFixtures["seed"]) => Promise<void>) => {
|
|
245
|
+
await use(async (data) => {
|
|
246
|
+
// Seed data via API
|
|
247
|
+
for (const [table, items] of Object.entries(data)) {
|
|
248
|
+
if (!items) continue;
|
|
249
|
+
for (const item of items) {
|
|
250
|
+
await fetch(`${baseURL}/${table}.create`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: { "Content-Type": "application/json" },
|
|
253
|
+
body: JSON.stringify(item),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
cleanup: async ({}, use: (fn: E2EFixtures["cleanup"]) => Promise<void>) => {
|
|
261
|
+
await use(async () => {
|
|
262
|
+
// Cleanup test data
|
|
263
|
+
await fetch(`${baseURL}/test.cleanup`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Note: Import test and expect directly from @playwright/test in your test files:
|
|
272
|
+
// import { test, expect } from "@playwright/test";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Testing utilities - separate subpath export to avoid loading Playwright at runtime
|
|
2
|
+
// Usage: import { createE2EFixtures } from "@donkeylabs/server/testing";
|
|
3
|
+
|
|
4
|
+
// E2E Testing - requires @playwright/test as peer dependency
|
|
5
|
+
export {
|
|
6
|
+
createE2EFixtures,
|
|
7
|
+
defineE2EConfig,
|
|
8
|
+
type E2EFixtures,
|
|
9
|
+
type E2EConfig,
|
|
10
|
+
} from "./e2e";
|
|
11
|
+
|
|
12
|
+
// Database Testing Utilities
|
|
13
|
+
export {
|
|
14
|
+
createTestDatabase,
|
|
15
|
+
resetTestDatabase,
|
|
16
|
+
seedTestData,
|
|
17
|
+
type TestDatabaseOptions,
|
|
18
|
+
} from "./database";
|