@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/INDEX.md
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# @majkapp/plugin-kit
|
|
2
|
+
|
|
3
|
+
Build MAJK plugins with functions, UI screens, and services. Test with `@majkapp/plugin-test`.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import { definePlugin } from '@majkapp/plugin-kit';
|
|
10
|
+
|
|
11
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
12
|
+
.pluginRoot(__dirname) // REQUIRED FIRST - enables resource loading
|
|
13
|
+
|
|
14
|
+
.function('health', {
|
|
15
|
+
description: 'Check plugin health status',
|
|
16
|
+
input: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {},
|
|
19
|
+
additionalProperties: false
|
|
20
|
+
},
|
|
21
|
+
output: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
status: { type: 'string', enum: ['ok', 'error'] },
|
|
25
|
+
timestamp: { type: 'string', format: 'date-time' }
|
|
26
|
+
},
|
|
27
|
+
required: ['status', 'timestamp']
|
|
28
|
+
},
|
|
29
|
+
handler: async (input, ctx) => {
|
|
30
|
+
// ctx.logger - scoped logging (debug, info, warn, error)
|
|
31
|
+
ctx.logger.info('Health check requested');
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
status: 'ok',
|
|
35
|
+
timestamp: new Date().toISOString()
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
tags: ['monitoring']
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
.screenReact({
|
|
42
|
+
id: 'my-plugin-main',
|
|
43
|
+
name: 'My Plugin',
|
|
44
|
+
description: 'Main plugin screen',
|
|
45
|
+
route: '/plugin-screens/my-plugin/main',
|
|
46
|
+
pluginPath: '/index.html' // Path to built React app
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
.build();
|
|
50
|
+
|
|
51
|
+
export = plugin;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Testing Your Function
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// tests/plugin/functions/unit/health.test.js
|
|
58
|
+
import { test } from '@majkapp/plugin-test';
|
|
59
|
+
import { invoke, mock } from '@majkapp/plugin-test';
|
|
60
|
+
import assert from 'assert';
|
|
61
|
+
|
|
62
|
+
test('health check returns ok status', async () => {
|
|
63
|
+
// Create mock context - no real MAJK instance needed
|
|
64
|
+
const context = mock().build();
|
|
65
|
+
|
|
66
|
+
// Invoke function with mock context
|
|
67
|
+
const result = await invoke('health', {}, { context });
|
|
68
|
+
|
|
69
|
+
// Verify output matches schema
|
|
70
|
+
assert.strictEqual(result.status, 'ok');
|
|
71
|
+
assert.ok(result.timestamp);
|
|
72
|
+
assert.ok(new Date(result.timestamp).getTime() > 0);
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run test:
|
|
77
|
+
```bash
|
|
78
|
+
cd my-plugin
|
|
79
|
+
npx majk-test tests/plugin/functions/unit/health.test.js
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Output:
|
|
83
|
+
```
|
|
84
|
+
✓ health check returns ok status (2ms)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Project Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
my-plugin/
|
|
91
|
+
├── src/
|
|
92
|
+
│ ├── index.ts # Plugin definition
|
|
93
|
+
│ └── core/ # Business logic (no plugin deps)
|
|
94
|
+
│ └── analytics.ts # Pure functions, easy to test
|
|
95
|
+
├── ui/
|
|
96
|
+
│ ├── src/
|
|
97
|
+
│ │ ├── App.tsx # React app
|
|
98
|
+
│ │ ├── components/ # UI components
|
|
99
|
+
│ │ └── generated/ # Auto-generated hooks
|
|
100
|
+
│ │ ├── hooks.ts # useHealth(), etc.
|
|
101
|
+
│ │ └── client.ts # RPC client
|
|
102
|
+
│ └── index.html
|
|
103
|
+
├── tests/
|
|
104
|
+
│ └── plugin/
|
|
105
|
+
│ ├── functions/unit/ # Function tests
|
|
106
|
+
│ └── ui/unit/ # UI tests with bot
|
|
107
|
+
├── vite.config.js # EXACT format required (see below)
|
|
108
|
+
├── package.json # REQUIRED: majk metadata section
|
|
109
|
+
└── tsconfig.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## package.json (REQUIRED)
|
|
113
|
+
|
|
114
|
+
**CRITICAL:** Your package.json MUST include the `majk` metadata section and use an npm organization scope:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"name": "@yourorg/my-plugin",
|
|
119
|
+
"version": "1.0.0",
|
|
120
|
+
"description": "My awesome MAJK plugin",
|
|
121
|
+
"main": "index.js",
|
|
122
|
+
"keywords": ["majk", "plugin"],
|
|
123
|
+
"majk": {
|
|
124
|
+
"type": "in-process",
|
|
125
|
+
"entry": "index.js"
|
|
126
|
+
},
|
|
127
|
+
"scripts": {
|
|
128
|
+
"generate": "npx plugin-kit generate -e ./index.js -o ./ui/src/generated",
|
|
129
|
+
"dev": "vite",
|
|
130
|
+
"build": "tsc && npm run generate && vite build",
|
|
131
|
+
"preview": "vite preview",
|
|
132
|
+
"test": "npm run test:plugin",
|
|
133
|
+
"test:plugin": "npm run test:plugin:functions && npm run test:plugin:ui",
|
|
134
|
+
"test:plugin:functions": "npx majk-test --type functions",
|
|
135
|
+
"test:plugin:ui": "node tests/plugin/ui/unit/basic-navigation.test.js",
|
|
136
|
+
"clean": "rm -rf dist index.js index.d.ts ui/src/generated"
|
|
137
|
+
},
|
|
138
|
+
"dependencies": {
|
|
139
|
+
"@majkapp/plugin-kit": "^3.3.0",
|
|
140
|
+
"@majkapp/plugin-theme": "^1.0.0",
|
|
141
|
+
"@majkapp/plugin-ui": "^1.4.0",
|
|
142
|
+
"react": "^18.3.1",
|
|
143
|
+
"react-dom": "^18.3.1"
|
|
144
|
+
},
|
|
145
|
+
"devDependencies": {
|
|
146
|
+
"@majkapp/plugin-test": "^1.0.2",
|
|
147
|
+
"@types/react": "^18.3.1",
|
|
148
|
+
"@types/react-dom": "^18.3.1",
|
|
149
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
150
|
+
"typescript": "^5.3.3",
|
|
151
|
+
"vite": "^6.0.3"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Package Naming (REQUIRED)
|
|
157
|
+
|
|
158
|
+
**Your plugin name MUST use an npm organization scope** (format: `@org/plugin-name`).
|
|
159
|
+
|
|
160
|
+
Replace `@yourorg` with your organization:
|
|
161
|
+
- If you have an npm organization: `@mycompany/my-plugin`
|
|
162
|
+
- If you don't have one: Use `@majkical/my-plugin`
|
|
163
|
+
|
|
164
|
+
Example: `@majkical/weather-widget`
|
|
165
|
+
|
|
166
|
+
### majk Metadata Section
|
|
167
|
+
|
|
168
|
+
The `majk` object tells MAJK how to load your plugin:
|
|
169
|
+
|
|
170
|
+
**`type`**: `"in-process"` or `"remote"`
|
|
171
|
+
- `"in-process"` - Plugin runs in MAJK's Node.js process (most common)
|
|
172
|
+
- `"remote"` - Plugin runs in separate process (for isolation)
|
|
173
|
+
|
|
174
|
+
**`entry`**: Path to compiled plugin file (relative to package root)
|
|
175
|
+
- Usually `"index.js"` (compiled from `src/index.ts`)
|
|
176
|
+
- MAJK loads this file to get your plugin definition
|
|
177
|
+
|
|
178
|
+
### Important Scripts
|
|
179
|
+
|
|
180
|
+
**`generate`** - Generate TypeScript client from plugin
|
|
181
|
+
```bash
|
|
182
|
+
npm run generate # Regenerate after adding/changing functions
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**`build`** - Complete build pipeline
|
|
186
|
+
```bash
|
|
187
|
+
npm run build # 1. Compile TS → index.js
|
|
188
|
+
# 2. Generate client
|
|
189
|
+
# 3. Build React UI → dist/
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**`test:plugin:functions`** - Test plugin functions
|
|
193
|
+
```bash
|
|
194
|
+
npm run test:plugin:functions # Run all function tests
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**`test:plugin:ui`** - Test plugin UI with bot
|
|
198
|
+
```bash
|
|
199
|
+
npm run test:plugin:ui # Run UI tests with browser automation
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## vite.config.js (REQUIRED FORMAT)
|
|
203
|
+
|
|
204
|
+
**CRITICAL:** Use this EXACT format for your vite.config.js:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
import { defineConfig } from 'vite';
|
|
208
|
+
import react from '@vitejs/plugin-react';
|
|
209
|
+
|
|
210
|
+
export default defineConfig({
|
|
211
|
+
plugins: [react()],
|
|
212
|
+
root: 'ui', // React source is in ui/ directory
|
|
213
|
+
base: '', // IMPORTANT: Empty string, NOT './'
|
|
214
|
+
server: {
|
|
215
|
+
port: 3000,
|
|
216
|
+
strictPort: false,
|
|
217
|
+
},
|
|
218
|
+
build: {
|
|
219
|
+
outDir: '../dist', // Build output to dist/ (parent of ui/)
|
|
220
|
+
assetsDir: 'assets',
|
|
221
|
+
emptyOutDir: true,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Why `base: ''`?**
|
|
227
|
+
- MAJK loads plugins in iframes with dynamic paths
|
|
228
|
+
- Empty base ensures assets load correctly
|
|
229
|
+
- `base: './'` will cause 404 errors for assets
|
|
230
|
+
|
|
231
|
+
## Best Practice: Decoupled Business Logic
|
|
232
|
+
|
|
233
|
+
**ALWAYS separate business logic from plugin code.** This makes testing easier and allows logic reuse.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// src/core/analytics.ts - Pure business logic, NO plugin dependencies
|
|
237
|
+
export interface AnalyticsData {
|
|
238
|
+
period: '24h' | '7d' | '30d';
|
|
239
|
+
metrics: {
|
|
240
|
+
activeUsers: number;
|
|
241
|
+
totalSessions: number;
|
|
242
|
+
avgDuration: number;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function calculateAnalytics(
|
|
247
|
+
sessions: Array<{ userId: string; duration: number; timestamp: Date }>,
|
|
248
|
+
period: '24h' | '7d' | '30d'
|
|
249
|
+
): AnalyticsData {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : 2592000000;
|
|
252
|
+
|
|
253
|
+
const recentSessions = sessions.filter(s =>
|
|
254
|
+
now - s.timestamp.getTime() < periodMs
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const uniqueUsers = new Set(recentSessions.map(s => s.userId)).size;
|
|
258
|
+
const totalDuration = recentSessions.reduce((sum, s) => sum + s.duration, 0);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
period,
|
|
262
|
+
metrics: {
|
|
263
|
+
activeUsers: uniqueUsers,
|
|
264
|
+
totalSessions: recentSessions.length,
|
|
265
|
+
avgDuration: recentSessions.length > 0
|
|
266
|
+
? Math.round(totalDuration / recentSessions.length)
|
|
267
|
+
: 0
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Test this with pure Jest/Vitest - NO plugin-test needed
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// src/index.ts - Plugin function wraps business logic
|
|
277
|
+
import { calculateAnalytics } from './core/analytics';
|
|
278
|
+
|
|
279
|
+
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
|
|
280
|
+
.pluginRoot(__dirname)
|
|
281
|
+
|
|
282
|
+
.function('getAnalytics', {
|
|
283
|
+
description: 'Get analytics for specified time period',
|
|
284
|
+
input: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
period: { type: 'string', enum: ['24h', '7d', '30d'] }
|
|
288
|
+
},
|
|
289
|
+
required: ['period'],
|
|
290
|
+
additionalProperties: false
|
|
291
|
+
},
|
|
292
|
+
output: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {
|
|
295
|
+
period: { type: 'string' },
|
|
296
|
+
metrics: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
activeUsers: { type: 'number' },
|
|
300
|
+
totalSessions: { type: 'number' },
|
|
301
|
+
avgDuration: { type: 'number' }
|
|
302
|
+
},
|
|
303
|
+
required: ['activeUsers', 'totalSessions', 'avgDuration']
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
required: ['period', 'metrics']
|
|
307
|
+
},
|
|
308
|
+
handler: async (input, ctx) => {
|
|
309
|
+
// Get data from storage (plugin layer)
|
|
310
|
+
const sessions = await ctx.storage.get('sessions') || [];
|
|
311
|
+
|
|
312
|
+
// Pure business logic (easy to test)
|
|
313
|
+
const analytics = calculateAnalytics(sessions, input.period);
|
|
314
|
+
|
|
315
|
+
ctx.logger.info(`Analytics calculated for ${input.period}`, analytics);
|
|
316
|
+
|
|
317
|
+
return analytics;
|
|
318
|
+
},
|
|
319
|
+
tags: ['analytics']
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Best Practice: Service Interfaces for External Dependencies
|
|
324
|
+
|
|
325
|
+
When using 3rd party services (AWS, databases, APIs), create interfaces for easy mocking.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// src/core/storage-service.ts - Interface for abstraction
|
|
329
|
+
export interface StorageService {
|
|
330
|
+
save(key: string, data: any): Promise<void>;
|
|
331
|
+
load(key: string): Promise<any>;
|
|
332
|
+
delete(key: string): Promise<void>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/core/aws-storage.ts - Real implementation
|
|
336
|
+
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
337
|
+
|
|
338
|
+
export class AWSStorageService implements StorageService {
|
|
339
|
+
constructor(private s3: S3Client, private bucket: string) {}
|
|
340
|
+
|
|
341
|
+
async save(key: string, data: any): Promise<void> {
|
|
342
|
+
await this.s3.send(new PutObjectCommand({
|
|
343
|
+
Bucket: this.bucket,
|
|
344
|
+
Key: key,
|
|
345
|
+
Body: JSON.stringify(data)
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async load(key: string): Promise<any> {
|
|
350
|
+
const result = await this.s3.send(new GetObjectCommand({
|
|
351
|
+
Bucket: this.bucket,
|
|
352
|
+
Key: key
|
|
353
|
+
}));
|
|
354
|
+
const body = await result.Body?.transformToString();
|
|
355
|
+
return body ? JSON.parse(body) : null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async delete(key: string): Promise<void> {
|
|
359
|
+
// Implementation...
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/core/mock-storage.ts - Mock for testing
|
|
364
|
+
export class MockStorageService implements StorageService {
|
|
365
|
+
private data = new Map<string, any>();
|
|
366
|
+
|
|
367
|
+
async save(key: string, data: any): Promise<void> {
|
|
368
|
+
this.data.set(key, data);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async load(key: string): Promise<any> {
|
|
372
|
+
return this.data.get(key);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async delete(key: string): Promise<void> {
|
|
376
|
+
this.data.delete(key);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## React UI with Generated Hooks
|
|
382
|
+
|
|
383
|
+
The plugin-kit auto-generates React hooks from your functions.
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// ui/src/DashboardPage.tsx
|
|
387
|
+
import { useHealth, useGetAnalytics, useUiLog } from './generated/hooks';
|
|
388
|
+
import { PluginStatGrid, PluginActionPanel } from '@majkapp/plugin-ui';
|
|
389
|
+
import { useEffect } from 'react';
|
|
390
|
+
|
|
391
|
+
export function DashboardPage() {
|
|
392
|
+
// Auto-generated hooks - query hooks auto-fetch on mount
|
|
393
|
+
const { data: health, loading: healthLoading, refetch: refetchHealth } = useHealth();
|
|
394
|
+
const { data: analytics, loading: analyticsLoading } = useGetAnalytics(
|
|
395
|
+
{ period: '7d' },
|
|
396
|
+
{ refetchInterval: 30000 } // Auto-refresh every 30s
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Mutation hook for logging - call .mutate() manually
|
|
400
|
+
const { mutate: uiLog } = useUiLog();
|
|
401
|
+
|
|
402
|
+
// Log UI lifecycle events for debugging
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
uiLog({
|
|
405
|
+
level: 'info',
|
|
406
|
+
component: 'DashboardPage',
|
|
407
|
+
message: 'Dashboard mounted',
|
|
408
|
+
data: { timestamp: new Date().toISOString() }
|
|
409
|
+
});
|
|
410
|
+
}, [uiLog]);
|
|
411
|
+
|
|
412
|
+
// data-majk-id: Enables bot testing - identifies clickable elements
|
|
413
|
+
// data-majk-state: Tracks UI state for testing - empty/loading/populated
|
|
414
|
+
return (
|
|
415
|
+
<div data-majk-id="dashboardContainer">
|
|
416
|
+
<PluginActionPanel
|
|
417
|
+
actions={[
|
|
418
|
+
{
|
|
419
|
+
label: '🔄 Refresh Health',
|
|
420
|
+
onClick: () => {
|
|
421
|
+
uiLog({
|
|
422
|
+
level: 'info',
|
|
423
|
+
component: 'DashboardPage',
|
|
424
|
+
message: 'Health refresh triggered by user'
|
|
425
|
+
});
|
|
426
|
+
refetchHealth();
|
|
427
|
+
},
|
|
428
|
+
'data-majk-id': 'refreshHealthButton',
|
|
429
|
+
'data-majk-state': healthLoading ? 'loading' : 'idle'
|
|
430
|
+
}
|
|
431
|
+
]}
|
|
432
|
+
/>
|
|
433
|
+
|
|
434
|
+
<PluginStatGrid
|
|
435
|
+
stats={[
|
|
436
|
+
{
|
|
437
|
+
label: 'Health Status',
|
|
438
|
+
value: health?.status || 'unknown',
|
|
439
|
+
'data-majk-id': 'healthStatus',
|
|
440
|
+
'data-majk-state': healthLoading ? 'loading' : health ? 'populated' : 'empty'
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
label: 'Active Users (7d)',
|
|
444
|
+
value: analytics?.metrics.activeUsers || 0,
|
|
445
|
+
'data-majk-id': 'activeUsersCount',
|
|
446
|
+
'data-majk-state': analyticsLoading ? 'loading' : analytics ? 'populated' : 'empty'
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
label: 'Total Sessions',
|
|
450
|
+
value: analytics?.metrics.totalSessions || 0,
|
|
451
|
+
'data-majk-id': 'totalSessionsCount'
|
|
452
|
+
}
|
|
453
|
+
]}
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Testing Your UI
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// tests/plugin/ui/unit/dashboard.test.js
|
|
464
|
+
import { test } from '@majkapp/plugin-test';
|
|
465
|
+
import { bot, invoke, mock } from '@majkapp/plugin-test';
|
|
466
|
+
import assert from 'assert';
|
|
467
|
+
|
|
468
|
+
test('dashboard displays health status', async () => {
|
|
469
|
+
// Create mock context with handlers for functions
|
|
470
|
+
const context = mock()
|
|
471
|
+
.withMockHandler('health', async () => ({
|
|
472
|
+
status: 'ok',
|
|
473
|
+
timestamp: new Date().toISOString()
|
|
474
|
+
}))
|
|
475
|
+
.withMockHandler('getAnalytics', async () => ({
|
|
476
|
+
period: '7d',
|
|
477
|
+
metrics: {
|
|
478
|
+
activeUsers: 42,
|
|
479
|
+
totalSessions: 150,
|
|
480
|
+
avgDuration: 320
|
|
481
|
+
}
|
|
482
|
+
}))
|
|
483
|
+
.build();
|
|
484
|
+
|
|
485
|
+
// Start browser bot
|
|
486
|
+
const b = await bot(context);
|
|
487
|
+
|
|
488
|
+
// Navigate to screen
|
|
489
|
+
await b.goto('/plugin-screens/my-plugin/main');
|
|
490
|
+
|
|
491
|
+
// Wait for data to load - check data-majk-state
|
|
492
|
+
await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
|
|
493
|
+
|
|
494
|
+
// Verify displayed values
|
|
495
|
+
const healthText = await b.getText('[data-majk-id="healthStatus"]');
|
|
496
|
+
assert.ok(healthText.includes('ok'));
|
|
497
|
+
|
|
498
|
+
const usersText = await b.getText('[data-majk-id="activeUsersCount"]');
|
|
499
|
+
assert.ok(usersText.includes('42'));
|
|
500
|
+
|
|
501
|
+
// Test interaction - click refresh button
|
|
502
|
+
await b.click('[data-majk-id="refreshHealthButton"]');
|
|
503
|
+
|
|
504
|
+
// Verify loading state
|
|
505
|
+
const buttonState = await b.getAttribute('[data-majk-id="refreshHealthButton"]', 'data-majk-state');
|
|
506
|
+
assert.strictEqual(buttonState, 'loading');
|
|
507
|
+
|
|
508
|
+
await b.close();
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Key Concepts
|
|
513
|
+
|
|
514
|
+
### Query vs Mutation Hooks
|
|
515
|
+
|
|
516
|
+
Generated hooks follow React Query patterns:
|
|
517
|
+
|
|
518
|
+
**Query Hooks** - Auto-fetch data on mount:
|
|
519
|
+
- `useHealth()` - fetches immediately
|
|
520
|
+
- `useGetAnalytics({ period: '7d' })` - fetches immediately with params
|
|
521
|
+
- Returns: `{ data, error, loading, refetch }`
|
|
522
|
+
|
|
523
|
+
**Mutation Hooks** - Call manually:
|
|
524
|
+
- `useUiLog()` - call `.mutate(input)` when needed
|
|
525
|
+
- `useClearEvents()` - call `.mutate({})` on button click
|
|
526
|
+
- Returns: `{ mutate, data, error, loading }`
|
|
527
|
+
|
|
528
|
+
### data-majk-* Attributes
|
|
529
|
+
|
|
530
|
+
**Critical for testing.** The bot uses these to find and verify elements.
|
|
531
|
+
|
|
532
|
+
- `data-majk-id="uniqueId"` - Identifies clickable elements, key stats
|
|
533
|
+
- `data-majk-state="loading|empty|populated|error"` - Tracks UI state for assertions
|
|
534
|
+
|
|
535
|
+
### uiLog Function
|
|
536
|
+
|
|
537
|
+
**Best practice:** Create a `uiLog` function in your plugin for frontend debugging.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
.function('uiLog', {
|
|
541
|
+
description: 'Log messages from UI for debugging',
|
|
542
|
+
input: {
|
|
543
|
+
type: 'object',
|
|
544
|
+
properties: {
|
|
545
|
+
level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
|
|
546
|
+
component: { type: 'string' },
|
|
547
|
+
message: { type: 'string' },
|
|
548
|
+
data: { type: 'object' }
|
|
549
|
+
},
|
|
550
|
+
required: ['level', 'component', 'message'],
|
|
551
|
+
additionalProperties: false
|
|
552
|
+
},
|
|
553
|
+
output: {
|
|
554
|
+
type: 'object',
|
|
555
|
+
properties: {
|
|
556
|
+
success: { type: 'boolean' }
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
handler: async (input, ctx) => {
|
|
560
|
+
const prefix = `[UI:${input.component}]`;
|
|
561
|
+
ctx.logger[input.level](`${prefix} ${input.message}`, input.data);
|
|
562
|
+
return { success: true };
|
|
563
|
+
},
|
|
564
|
+
tags: ['debugging']
|
|
565
|
+
})
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Then use in React:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// Log errors
|
|
572
|
+
try {
|
|
573
|
+
await someAsyncOperation();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
uiLog({
|
|
576
|
+
level: 'error',
|
|
577
|
+
component: 'DashboardPage',
|
|
578
|
+
message: 'Failed to load data',
|
|
579
|
+
data: { error: error.message }
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Log user actions
|
|
584
|
+
onClick={() => {
|
|
585
|
+
uiLog({
|
|
586
|
+
level: 'info',
|
|
587
|
+
component: 'SettingsPage',
|
|
588
|
+
message: 'User clicked save button',
|
|
589
|
+
data: { settingsChanged: ['theme', 'notifications'] }
|
|
590
|
+
});
|
|
591
|
+
handleSave();
|
|
592
|
+
}}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Next Steps
|
|
596
|
+
|
|
597
|
+
Run `npx @majkapp/plugin-kit --functions` - Deep dive into function patterns
|
|
598
|
+
Run `npx @majkapp/plugin-kit --screens` - Screen configuration details
|
|
599
|
+
Run `npx @majkapp/plugin-kit --hooks` - Generated hooks reference
|
|
600
|
+
Run `npx @majkapp/plugin-kit --context` - ctx.majk and ctx.logger APIs
|
|
601
|
+
Run `npx @majkapp/plugin-kit --services` - Service grouping patterns
|
|
602
|
+
Run `npx @majkapp/plugin-kit --lifecycle` - onReady and cleanup
|
|
603
|
+
Run `npx @majkapp/plugin-kit --testing` - Complete testing guide
|
|
604
|
+
Run `npx @majkapp/plugin-kit --config` - vite.config.js and project setup
|
|
605
|
+
Run `npx @majkapp/plugin-kit --full` - Complete reference
|