@majkapp/plugin-kit 3.2.1 → 3.3.1
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/bin/promptable-cli.js +91 -0
- package/docs/API.md +394 -0
- package/docs/CONFIG.md +428 -0
- package/docs/CONTEXT.md +500 -0
- package/docs/FULL.md +848 -0
- package/docs/FUNCTIONS.md +623 -0
- package/docs/HOOKS.md +532 -0
- package/docs/INDEX.md +605 -0
- package/docs/LIFECYCLE.md +490 -0
- package/docs/SCREENS.md +547 -0
- package/docs/SERVICES.md +350 -0
- package/docs/TESTING.md +593 -0
- package/docs/mcp-execution-api.md +490 -0
- package/package.json +18 -3
package/docs/TESTING.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Test your plugin with `@majkapp/plugin-test`. Write unit tests for functions and UI tests for screens.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @majkapp/plugin-test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
// package.json
|
|
13
|
+
{
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test:plugin:functions": "majk-test 'tests/plugin/functions/**/*.test.js'",
|
|
16
|
+
"test:plugin:ui": "majk-test 'tests/plugin/ui/**/*.test.js'",
|
|
17
|
+
"test:plugin": "majk-test 'tests/plugin/**/*.test.js'"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Function Testing
|
|
23
|
+
|
|
24
|
+
### Basic Function Test
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// tests/plugin/functions/unit/health.test.js
|
|
28
|
+
import { test } from '@majkapp/plugin-test';
|
|
29
|
+
import { invoke, mock } from '@majkapp/plugin-test';
|
|
30
|
+
import assert from 'assert';
|
|
31
|
+
|
|
32
|
+
test('health check returns ok status', async () => {
|
|
33
|
+
// Create mock context
|
|
34
|
+
const context = mock().build();
|
|
35
|
+
|
|
36
|
+
// Invoke function
|
|
37
|
+
const result = await invoke('health', {}, { context });
|
|
38
|
+
|
|
39
|
+
// Assert output
|
|
40
|
+
assert.strictEqual(result.status, 'ok');
|
|
41
|
+
assert.ok(result.timestamp);
|
|
42
|
+
assert.ok(new Date(result.timestamp).getTime() > 0);
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run test:
|
|
47
|
+
```bash
|
|
48
|
+
npx majk-test tests/plugin/functions/unit/health.test.js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output:
|
|
52
|
+
```
|
|
53
|
+
✓ health check returns ok status (3ms)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Test with Input Parameters
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// tests/plugin/functions/unit/analytics.test.js
|
|
60
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
61
|
+
import assert from 'assert';
|
|
62
|
+
|
|
63
|
+
test('getAnalytics returns metrics for 7d period', async () => {
|
|
64
|
+
const context = mock()
|
|
65
|
+
.storage({
|
|
66
|
+
sessions: [
|
|
67
|
+
{ userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 }, // 1 day ago
|
|
68
|
+
{ userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago
|
|
69
|
+
{ userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 } // 3 days ago
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
.build();
|
|
73
|
+
|
|
74
|
+
const result = await invoke('getAnalytics', { period: '7d' }, { context });
|
|
75
|
+
|
|
76
|
+
assert.strictEqual(result.period, '7d');
|
|
77
|
+
assert.strictEqual(result.metrics.activeUsers, 2); // user1 and user2
|
|
78
|
+
assert.strictEqual(result.metrics.totalSessions, 3);
|
|
79
|
+
assert.strictEqual(result.metrics.avgDuration, 450);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('getAnalytics filters old sessions for 24h period', async () => {
|
|
83
|
+
const context = mock()
|
|
84
|
+
.storage({
|
|
85
|
+
sessions: [
|
|
86
|
+
{ userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 }, // 1 day ago
|
|
87
|
+
{ userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago - filtered out
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
.build();
|
|
91
|
+
|
|
92
|
+
const result = await invoke('getAnalytics', { period: '24h' }, { context });
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(result.metrics.activeUsers, 1); // Only user1
|
|
95
|
+
assert.strictEqual(result.metrics.totalSessions, 1);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Test with MAJK Data
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// tests/plugin/functions/unit/dashboard.test.js
|
|
103
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
104
|
+
import assert from 'assert';
|
|
105
|
+
|
|
106
|
+
test('getDashboardData aggregates MAJK entities', async () => {
|
|
107
|
+
const context = mock()
|
|
108
|
+
.withMajkData({
|
|
109
|
+
conversations: [
|
|
110
|
+
{ id: 'conv1', title: 'Test 1', updatedAt: '2024-01-02T00:00:00Z', messages: [] },
|
|
111
|
+
{ id: 'conv2', title: 'Test 2', updatedAt: '2024-01-01T00:00:00Z', messages: [] }
|
|
112
|
+
],
|
|
113
|
+
todos: [
|
|
114
|
+
{ id: 'todo1', title: 'Task 1', status: 'pending' },
|
|
115
|
+
{ id: 'todo2', title: 'Task 2', status: 'completed' },
|
|
116
|
+
{ id: 'todo3', title: 'Task 3', status: 'pending' }
|
|
117
|
+
],
|
|
118
|
+
projects: [
|
|
119
|
+
{ id: 'proj1', name: 'Project 1' }
|
|
120
|
+
]
|
|
121
|
+
})
|
|
122
|
+
.build();
|
|
123
|
+
|
|
124
|
+
const result = await invoke('getDashboardData', {}, { context });
|
|
125
|
+
|
|
126
|
+
assert.strictEqual(result.conversations.total, 2);
|
|
127
|
+
assert.strictEqual(result.conversations.recent.length, 2);
|
|
128
|
+
assert.strictEqual(result.conversations.recent[0].id, 'conv1'); // Most recent first
|
|
129
|
+
|
|
130
|
+
assert.strictEqual(result.todos.total, 3);
|
|
131
|
+
assert.strictEqual(result.todos.pending, 2);
|
|
132
|
+
assert.strictEqual(result.todos.completed, 1);
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(result.projects.total, 1);
|
|
135
|
+
assert.ok(result.timestamp);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Test Error Handling
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// tests/plugin/functions/unit/error-handling.test.js
|
|
143
|
+
import { test, invoke, mock } from '@majkapp/plugin-test';
|
|
144
|
+
import assert from 'assert';
|
|
145
|
+
|
|
146
|
+
test('function handles missing data gracefully', async () => {
|
|
147
|
+
const context = mock()
|
|
148
|
+
.storage({}) // Empty storage
|
|
149
|
+
.build();
|
|
150
|
+
|
|
151
|
+
const result = await invoke('getAnalytics', { period: '7d' }, { context });
|
|
152
|
+
|
|
153
|
+
// Should return zero values, not crash
|
|
154
|
+
assert.strictEqual(result.metrics.activeUsers, 0);
|
|
155
|
+
assert.strictEqual(result.metrics.totalSessions, 0);
|
|
156
|
+
assert.strictEqual(result.metrics.avgDuration, 0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('function throws error for invalid input', async () => {
|
|
160
|
+
const context = mock().build();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await invoke('getAnalytics', { period: 'invalid' }, { context });
|
|
164
|
+
assert.fail('Should have thrown error');
|
|
165
|
+
} catch (error) {
|
|
166
|
+
assert.ok(error.message.includes('period'));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## UI Testing
|
|
172
|
+
|
|
173
|
+
### Basic UI Test
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// tests/plugin/ui/unit/dashboard.test.js
|
|
177
|
+
import { test } from '@majkapp/plugin-test';
|
|
178
|
+
import { bot, mock } from '@majkapp/plugin-test';
|
|
179
|
+
import assert from 'assert';
|
|
180
|
+
|
|
181
|
+
test('dashboard displays health status', async () => {
|
|
182
|
+
// Mock function responses
|
|
183
|
+
const context = mock()
|
|
184
|
+
.withMockHandler('health', async () => ({
|
|
185
|
+
status: 'ok',
|
|
186
|
+
timestamp: new Date().toISOString()
|
|
187
|
+
}))
|
|
188
|
+
.withMockHandler('getAnalytics', async () => ({
|
|
189
|
+
period: '7d',
|
|
190
|
+
metrics: {
|
|
191
|
+
activeUsers: 42,
|
|
192
|
+
totalSessions: 150,
|
|
193
|
+
avgDuration: 320
|
|
194
|
+
}
|
|
195
|
+
}))
|
|
196
|
+
.build();
|
|
197
|
+
|
|
198
|
+
// Start browser bot
|
|
199
|
+
const b = await bot(context);
|
|
200
|
+
|
|
201
|
+
// Navigate to screen
|
|
202
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
203
|
+
|
|
204
|
+
// Wait for data to load - check data-majk-state
|
|
205
|
+
await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
|
|
206
|
+
|
|
207
|
+
// Verify displayed values using data-majk-id
|
|
208
|
+
const healthText = await b.getText('[data-majk-id="healthStatus"]');
|
|
209
|
+
assert.ok(healthText.includes('ok'), `Expected 'ok' in "${healthText}"`);
|
|
210
|
+
|
|
211
|
+
const usersText = await b.getText('[data-majk-id="activeUsersCount"]');
|
|
212
|
+
assert.ok(usersText.includes('42'), `Expected '42' in "${usersText}"`);
|
|
213
|
+
|
|
214
|
+
// Take screenshot for visual verification
|
|
215
|
+
await b.screenshot('dashboard-loaded.png');
|
|
216
|
+
|
|
217
|
+
await b.close();
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Test Button Clicks
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// tests/plugin/ui/unit/interactions.test.js
|
|
225
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
226
|
+
import assert from 'assert';
|
|
227
|
+
|
|
228
|
+
test('clicking refresh button updates health status', async () => {
|
|
229
|
+
let healthCallCount = 0;
|
|
230
|
+
|
|
231
|
+
const context = mock()
|
|
232
|
+
.withMockHandler('health', async () => {
|
|
233
|
+
healthCallCount++;
|
|
234
|
+
return {
|
|
235
|
+
status: 'ok',
|
|
236
|
+
timestamp: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
})
|
|
239
|
+
.build();
|
|
240
|
+
|
|
241
|
+
const b = await bot(context);
|
|
242
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
243
|
+
|
|
244
|
+
// Wait for initial load
|
|
245
|
+
await b.waitForSelector('[data-majk-id="refreshHealthButton"]');
|
|
246
|
+
assert.strictEqual(healthCallCount, 1, 'Initial health check should have been called');
|
|
247
|
+
|
|
248
|
+
// Click refresh button
|
|
249
|
+
await b.click('[data-majk-id="refreshHealthButton"]');
|
|
250
|
+
|
|
251
|
+
// Wait for loading state
|
|
252
|
+
await b.waitForSelector('[data-majk-id="refreshHealthButton"][data-majk-state="loading"]');
|
|
253
|
+
|
|
254
|
+
// Wait for data to reload
|
|
255
|
+
await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
|
|
256
|
+
|
|
257
|
+
// Verify second health check was called
|
|
258
|
+
assert.strictEqual(healthCallCount, 2, 'Health check should have been called again');
|
|
259
|
+
|
|
260
|
+
await b.close();
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Test State Changes
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// tests/plugin/ui/unit/toggle.test.js
|
|
268
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
269
|
+
import assert from 'assert';
|
|
270
|
+
|
|
271
|
+
test('toggle refresh button changes state', async () => {
|
|
272
|
+
const context = mock()
|
|
273
|
+
.withMockHandler('health', async () => ({
|
|
274
|
+
status: 'ok',
|
|
275
|
+
timestamp: new Date().toISOString()
|
|
276
|
+
}))
|
|
277
|
+
.build();
|
|
278
|
+
|
|
279
|
+
const b = await bot(context);
|
|
280
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
281
|
+
|
|
282
|
+
// Wait for page load
|
|
283
|
+
await b.waitForSelector('[data-majk-id="toggleRefreshButton"]');
|
|
284
|
+
|
|
285
|
+
// Initial state should be 'refreshing'
|
|
286
|
+
let state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
|
|
287
|
+
assert.strictEqual(state, 'refreshing');
|
|
288
|
+
|
|
289
|
+
// Click to pause
|
|
290
|
+
await b.click('[data-majk-id="toggleRefreshButton"]');
|
|
291
|
+
|
|
292
|
+
// Verify state changed to 'paused'
|
|
293
|
+
state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
|
|
294
|
+
assert.strictEqual(state, 'paused');
|
|
295
|
+
|
|
296
|
+
// Click again to resume
|
|
297
|
+
await b.click('[data-majk-id="toggleRefreshButton"]');
|
|
298
|
+
|
|
299
|
+
// Verify state changed back to 'refreshing'
|
|
300
|
+
state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
|
|
301
|
+
assert.strictEqual(state, 'refreshing');
|
|
302
|
+
|
|
303
|
+
await b.close();
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Test List Rendering
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// tests/plugin/ui/unit/events-list.test.js
|
|
311
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
312
|
+
import assert from 'assert';
|
|
313
|
+
|
|
314
|
+
test('events list displays events', async () => {
|
|
315
|
+
const context = mock()
|
|
316
|
+
.withMockHandler('getEvents', async () => ({
|
|
317
|
+
events: [
|
|
318
|
+
{
|
|
319
|
+
id: 'evt1',
|
|
320
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
321
|
+
entityType: 'conversation',
|
|
322
|
+
eventType: 'created',
|
|
323
|
+
entityId: 'conv1',
|
|
324
|
+
entitySummary: { title: 'Test Conversation' }
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'evt2',
|
|
328
|
+
timestamp: '2024-01-02T00:00:00Z',
|
|
329
|
+
entityType: 'todo',
|
|
330
|
+
eventType: 'updated',
|
|
331
|
+
entityId: 'todo1',
|
|
332
|
+
entitySummary: { title: 'Test Todo' }
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
count: 2
|
|
336
|
+
}))
|
|
337
|
+
.build();
|
|
338
|
+
|
|
339
|
+
const b = await bot(context);
|
|
340
|
+
await b.goto('/plugin-screens/my-plugin/events');
|
|
341
|
+
|
|
342
|
+
// Wait for events to load
|
|
343
|
+
await b.waitForSelector('[data-majk-id="eventsContainer"][data-majk-state="populated"]');
|
|
344
|
+
|
|
345
|
+
// Verify event items are rendered
|
|
346
|
+
const eventItems = await b.querySelectorAll('[data-majk-id="eventItem"]');
|
|
347
|
+
assert.strictEqual(eventItems.length, 2);
|
|
348
|
+
|
|
349
|
+
// Verify count display
|
|
350
|
+
const countText = await b.getText('[data-majk-id="displayedEventsCount"]');
|
|
351
|
+
assert.ok(countText.includes('2'));
|
|
352
|
+
|
|
353
|
+
await b.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('events list shows empty state when no events', async () => {
|
|
357
|
+
const context = mock()
|
|
358
|
+
.withMockHandler('getEvents', async () => ({
|
|
359
|
+
events: [],
|
|
360
|
+
count: 0
|
|
361
|
+
}))
|
|
362
|
+
.build();
|
|
363
|
+
|
|
364
|
+
const b = await bot(context);
|
|
365
|
+
await b.goto('/plugin-screens/my-plugin/events');
|
|
366
|
+
|
|
367
|
+
// Wait for empty state
|
|
368
|
+
await b.waitForSelector('[data-majk-id="eventsContainer"][data-majk-state="empty"]');
|
|
369
|
+
|
|
370
|
+
// Verify empty message is shown
|
|
371
|
+
const emptyMessage = await b.isVisible('[data-majk-id="emptyMessage"]');
|
|
372
|
+
assert.ok(emptyMessage);
|
|
373
|
+
|
|
374
|
+
const text = await b.getText('[data-majk-id="emptyMessage"]');
|
|
375
|
+
assert.ok(text.includes('No events'));
|
|
376
|
+
|
|
377
|
+
await b.close();
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Test Error States
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// tests/plugin/ui/unit/error-handling.test.js
|
|
385
|
+
import { test, bot, mock } from '@majkapp/plugin-test';
|
|
386
|
+
import assert from 'assert';
|
|
387
|
+
|
|
388
|
+
test('dashboard displays error when health check fails', async () => {
|
|
389
|
+
const context = mock()
|
|
390
|
+
.withMockHandler('health', async () => {
|
|
391
|
+
throw new Error('Health check failed');
|
|
392
|
+
})
|
|
393
|
+
.build();
|
|
394
|
+
|
|
395
|
+
const b = await bot(context);
|
|
396
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
397
|
+
|
|
398
|
+
// Wait for error state
|
|
399
|
+
await b.waitForSelector('[data-majk-id="errorMessage"][data-majk-state="error"]');
|
|
400
|
+
|
|
401
|
+
// Verify error message is displayed
|
|
402
|
+
const errorText = await b.getText('[data-majk-id="errorMessage"]');
|
|
403
|
+
assert.ok(errorText.includes('Health check failed'));
|
|
404
|
+
|
|
405
|
+
await b.screenshot('dashboard-error-state.png');
|
|
406
|
+
|
|
407
|
+
await b.close();
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Bot API Reference
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const b = await bot(context);
|
|
415
|
+
|
|
416
|
+
// Navigation
|
|
417
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
418
|
+
|
|
419
|
+
// Waiting
|
|
420
|
+
await b.waitForSelector('[data-majk-id="element"]');
|
|
421
|
+
await b.waitForSelector('[data-majk-id="element"][data-majk-state="populated"]');
|
|
422
|
+
|
|
423
|
+
// Clicking
|
|
424
|
+
await b.click('[data-majk-id="button"]');
|
|
425
|
+
|
|
426
|
+
// Text content
|
|
427
|
+
const text = await b.getText('[data-majk-id="element"]');
|
|
428
|
+
|
|
429
|
+
// Attributes
|
|
430
|
+
const state = await b.getAttribute('[data-majk-id="element"]', 'data-majk-state');
|
|
431
|
+
|
|
432
|
+
// Visibility
|
|
433
|
+
const isVisible = await b.isVisible('[data-majk-id="element"]');
|
|
434
|
+
|
|
435
|
+
// Query elements
|
|
436
|
+
const elements = await b.querySelectorAll('[data-majk-id="item"]');
|
|
437
|
+
|
|
438
|
+
// Screenshots
|
|
439
|
+
await b.screenshot('test-result.png'); // Saved to tests/__screenshots__/
|
|
440
|
+
|
|
441
|
+
// Cleanup
|
|
442
|
+
await b.close();
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Mock API Reference
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// Basic mock
|
|
449
|
+
const context = mock().build();
|
|
450
|
+
|
|
451
|
+
// With storage
|
|
452
|
+
const context = mock()
|
|
453
|
+
.storage({
|
|
454
|
+
key1: 'value1',
|
|
455
|
+
key2: { nested: 'data' }
|
|
456
|
+
})
|
|
457
|
+
.build();
|
|
458
|
+
|
|
459
|
+
// With MAJK data
|
|
460
|
+
const context = mock()
|
|
461
|
+
.withMajkData({
|
|
462
|
+
conversations: [{ id: 'conv1', title: 'Test' }],
|
|
463
|
+
todos: [{ id: 'todo1', status: 'pending' }],
|
|
464
|
+
projects: [{ id: 'proj1', name: 'Project' }],
|
|
465
|
+
teammates: [{ id: 'tm1', name: 'Assistant' }],
|
|
466
|
+
mcpServers: [{ id: 'mcp1', name: 'Server' }]
|
|
467
|
+
})
|
|
468
|
+
.build();
|
|
469
|
+
|
|
470
|
+
// With mock function handlers (for UI tests)
|
|
471
|
+
const context = mock()
|
|
472
|
+
.withMockHandler('health', async (input) => ({
|
|
473
|
+
status: 'ok',
|
|
474
|
+
timestamp: new Date().toISOString()
|
|
475
|
+
}))
|
|
476
|
+
.withMockHandler('getAnalytics', async (input) => ({
|
|
477
|
+
period: input.period,
|
|
478
|
+
metrics: { activeUsers: 42, totalSessions: 150, avgDuration: 320 }
|
|
479
|
+
}))
|
|
480
|
+
.build();
|
|
481
|
+
|
|
482
|
+
// Combined
|
|
483
|
+
const context = mock()
|
|
484
|
+
.storage({ sessions: [] })
|
|
485
|
+
.withMajkData({ conversations: [] })
|
|
486
|
+
.withMockHandler('health', async () => ({ status: 'ok' }))
|
|
487
|
+
.build();
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Project Structure
|
|
491
|
+
|
|
492
|
+
```
|
|
493
|
+
my-plugin/
|
|
494
|
+
├── tests/
|
|
495
|
+
│ └── plugin/
|
|
496
|
+
│ ├── functions/
|
|
497
|
+
│ │ └── unit/
|
|
498
|
+
│ │ ├── health.test.js
|
|
499
|
+
│ │ ├── analytics.test.js
|
|
500
|
+
│ │ └── dashboard-data.test.js
|
|
501
|
+
│ └── ui/
|
|
502
|
+
│ └── unit/
|
|
503
|
+
│ ├── dashboard.test.js
|
|
504
|
+
│ ├── events-list.test.js
|
|
505
|
+
│ └── interactions.test.js
|
|
506
|
+
└── __screenshots__/ # Generated by UI tests
|
|
507
|
+
├── dashboard-loaded.png
|
|
508
|
+
└── error-state.png
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Running Tests
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
# All tests
|
|
515
|
+
npm run test:plugin
|
|
516
|
+
|
|
517
|
+
# Function tests only
|
|
518
|
+
npm run test:plugin:functions
|
|
519
|
+
|
|
520
|
+
# UI tests only
|
|
521
|
+
npm run test:plugin:ui
|
|
522
|
+
|
|
523
|
+
# Single test file
|
|
524
|
+
npx majk-test tests/plugin/functions/unit/health.test.js
|
|
525
|
+
|
|
526
|
+
# Watch mode (re-run on file change)
|
|
527
|
+
npx majk-test --watch 'tests/plugin/**/*.test.js'
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Best Practices
|
|
531
|
+
|
|
532
|
+
### 1. Use data-majk-* Attributes
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// Good: Testable with data-majk-id
|
|
536
|
+
<button data-majk-id="saveButton">Save</button>
|
|
537
|
+
|
|
538
|
+
// Bad: Hard to test reliably
|
|
539
|
+
<button className="btn-save">Save</button>
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### 2. Test All States
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
test('component handles loading state', async () => { /* ... */ });
|
|
546
|
+
test('component handles empty state', async () => { /* ... */ });
|
|
547
|
+
test('component handles populated state', async () => { /* ... */ });
|
|
548
|
+
test('component handles error state', async () => { /* ... */ });
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 3. Verify Mock Calls
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
test('function is called with correct parameters', async () => {
|
|
555
|
+
let capturedInput;
|
|
556
|
+
|
|
557
|
+
const context = mock()
|
|
558
|
+
.withMockHandler('myFunction', async (input) => {
|
|
559
|
+
capturedInput = input; // Capture input for verification
|
|
560
|
+
return { success: true };
|
|
561
|
+
})
|
|
562
|
+
.build();
|
|
563
|
+
|
|
564
|
+
const b = await bot(context);
|
|
565
|
+
await b.goto('/plugin-screens/my-plugin/page');
|
|
566
|
+
await b.click('[data-majk-id="submitButton"]');
|
|
567
|
+
|
|
568
|
+
await b.waitForSelector('[data-majk-id="success"]');
|
|
569
|
+
|
|
570
|
+
assert.deepStrictEqual(capturedInput, { expectedData: 'value' });
|
|
571
|
+
|
|
572
|
+
await b.close();
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 4. Take Screenshots for Visual Verification
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
test('dashboard renders correctly', async () => {
|
|
580
|
+
const b = await bot(context);
|
|
581
|
+
await b.goto('/plugin-screens/my-plugin/dashboard');
|
|
582
|
+
await b.waitForSelector('[data-majk-id="dashboardContainer"][data-majk-state="populated"]');
|
|
583
|
+
|
|
584
|
+
await b.screenshot('dashboard-populated.png');
|
|
585
|
+
|
|
586
|
+
await b.close();
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Next Steps
|
|
591
|
+
|
|
592
|
+
Run `npx @majkapp/plugin-test` - Complete testing reference
|
|
593
|
+
Run `npx @majkapp/plugin-kit --config` - Set up your project for testing
|