@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.
@@ -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