@rimori/playwright-testing 0.2.0
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/README.md +498 -0
- package/dist/_old/wait-helpers.d.ts +0 -0
- package/dist/_old/wait-helpers.js +28 -0
- package/dist/core/MessageChannelSimulator.d.ts +132 -0
- package/dist/core/MessageChannelSimulator.js +341 -0
- package/dist/core/RimoriTestEnvironment.d.ts +168 -0
- package/dist/core/RimoriTestEnvironment.js +446 -0
- package/dist/fixtures/default-user-info.d.ts +3 -0
- package/dist/fixtures/default-user-info.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/dist/test/translator.test.d.ts +1 -0
- package/dist/test/translator.test.js +134 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# @rimori/playwright
|
|
2
|
+
|
|
3
|
+
Playwright testing utilities for Rimori plugins. This package provides a complete testing environment that simulates how plugins run within the Rimori application, including MessageChannel communication, API mocking, and event handling.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@rimori/playwright` package enables end-to-end testing of Rimori plugins by:
|
|
8
|
+
|
|
9
|
+
- **Simulating iframe environment**: Makes plugins think they're running in an iframe (not standalone mode)
|
|
10
|
+
- **MessageChannel simulation**: Mimics the parent-iframe communication used in production
|
|
11
|
+
- **API mocking**: Provides mock handlers for Supabase and backend endpoints
|
|
12
|
+
- **Event handling**: Simulates Rimori events like main panel actions and sidebar actions
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --save-dev @rimori/playwright @playwright/test
|
|
18
|
+
# or
|
|
19
|
+
pnpm add -D @rimori/playwright @playwright/test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { test, expect } from '@playwright/test';
|
|
26
|
+
import { RimoriTestEnvironment } from '@rimori/playwright';
|
|
27
|
+
|
|
28
|
+
const pluginId = 'pl7720512027';
|
|
29
|
+
const pluginUrl = 'http://localhost:3009';
|
|
30
|
+
|
|
31
|
+
test.describe('My Plugin', () => {
|
|
32
|
+
let env: RimoriTestEnvironment;
|
|
33
|
+
|
|
34
|
+
test.beforeEach(async ({ page }) => {
|
|
35
|
+
env = new RimoriTestEnvironment({ page, pluginId });
|
|
36
|
+
|
|
37
|
+
// Set up mocks
|
|
38
|
+
env.ai.mockGetObject({ result: 'data' });
|
|
39
|
+
|
|
40
|
+
// Initialize the test environment
|
|
41
|
+
await env.setup();
|
|
42
|
+
await page.goto(`${pluginUrl}/#/my-page`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should work correctly', async ({ page }) => {
|
|
46
|
+
await expect(page.getByText('Hello')).toBeVisible();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Core Concepts
|
|
52
|
+
|
|
53
|
+
### MessageChannel Simulation
|
|
54
|
+
|
|
55
|
+
Plugins communicate with the Rimori parent application via MessageChannel. The `RimoriTestEnvironment` automatically sets up a MessageChannel simulation. This ensures plugins run in iframe mode, not standalone mode, matching production behavior.
|
|
56
|
+
|
|
57
|
+
### Test Environment Setup
|
|
58
|
+
|
|
59
|
+
The test environment:
|
|
60
|
+
|
|
61
|
+
- Sets default handlers for common routes (plugin_settings, etc.)
|
|
62
|
+
- Initializes MessageChannel communication
|
|
63
|
+
- Provides default RimoriInfo with test credentials
|
|
64
|
+
- Routes requests to appropriate mock handlers
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### RimoriTestEnvironment
|
|
69
|
+
|
|
70
|
+
Main test environment class that provides mocking capabilities and MessageChannel simulation.
|
|
71
|
+
|
|
72
|
+
#### Constructor
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
new RimoriTestEnvironment({
|
|
76
|
+
page: Page,
|
|
77
|
+
pluginId: string,
|
|
78
|
+
queryParams?: Record<string, string>,
|
|
79
|
+
userInfo?: Record<string, unknown>,
|
|
80
|
+
installedPlugins?: Plugin[],
|
|
81
|
+
guildOverrides?: Record<string, unknown>
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const env = new RimoriTestEnvironment({
|
|
89
|
+
page,
|
|
90
|
+
pluginId: 'pl1234567890',
|
|
91
|
+
queryParams: { applicationMode: 'sidebar' },
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Methods
|
|
96
|
+
|
|
97
|
+
##### `setup(): Promise<void>`
|
|
98
|
+
|
|
99
|
+
Initializes the test environment. Must be called before navigating to the plugin page.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
await env.setup();
|
|
103
|
+
await page.goto(pluginUrl);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### AI Mocking (`env.ai`)
|
|
107
|
+
|
|
108
|
+
Mock AI/LLM backend endpoints.
|
|
109
|
+
|
|
110
|
+
#### `mockGetText(values: unknown, options?: MockOptions)`
|
|
111
|
+
|
|
112
|
+
Mocks a non-streaming text generation response.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
env.ai.mockGetText({ result: 'Generated text' });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `mockGetSteamedText(text: string, options?: MockOptions)`
|
|
119
|
+
|
|
120
|
+
Mocks a streaming text response formatted as SSE (Server-Sent Events).
|
|
121
|
+
|
|
122
|
+
**Note**: Due to Playwright's `route.fulfill()` limitations, all SSE chunks are sent at once (no visible delays). The client will still parse it correctly as SSE.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
env.ai.mockGetSteamedText('This is the streaming response text.');
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `mockGetObject(value: unknown, options?: MockOptions)`
|
|
129
|
+
|
|
130
|
+
Mocks structured object generation (e.g., translation results).
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
env.ai.mockGetObject(
|
|
134
|
+
{
|
|
135
|
+
type: 'noun',
|
|
136
|
+
translation_swedish: 'träd',
|
|
137
|
+
translation_mother_tongue: 'tree',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
matcher: (request) => {
|
|
141
|
+
const body = request.postDataJSON();
|
|
142
|
+
return body?.instructions?.includes('Look up the word') ?? false;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `mockGetVoice(values: Buffer, options?: MockOptions)`
|
|
149
|
+
|
|
150
|
+
Mocks text-to-speech voice generation.
|
|
151
|
+
|
|
152
|
+
#### `mockGetTextFromVoice(text: string, options?: MockOptions)`
|
|
153
|
+
|
|
154
|
+
Mocks speech-to-text transcription.
|
|
155
|
+
|
|
156
|
+
### Plugin Settings (`env.plugin`)
|
|
157
|
+
|
|
158
|
+
Mock plugin settings endpoints.
|
|
159
|
+
|
|
160
|
+
#### `mockGetSettings(settingsRow, options?)`
|
|
161
|
+
|
|
162
|
+
Mocks GET request for plugin settings.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Return existing settings
|
|
166
|
+
env.plugin.mockGetSettings({
|
|
167
|
+
id: 'settings-id',
|
|
168
|
+
plugin_id: pluginId,
|
|
169
|
+
guild_id: 'guild-id',
|
|
170
|
+
settings: { theme: 'dark' },
|
|
171
|
+
is_guild_setting: false,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Return null to simulate no settings (triggers INSERT flow)
|
|
175
|
+
env.plugin.mockGetSettings(null);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `mockSetSettings(response?, options?)`
|
|
179
|
+
|
|
180
|
+
Mocks PATCH request for updating settings. Returns empty array by default (triggers INSERT).
|
|
181
|
+
|
|
182
|
+
#### `mockInsertSettings(response?, options?)`
|
|
183
|
+
|
|
184
|
+
Mocks POST request for inserting new settings.
|
|
185
|
+
|
|
186
|
+
### Event Handling (`env.event`)
|
|
187
|
+
|
|
188
|
+
Simulate Rimori events and actions.
|
|
189
|
+
|
|
190
|
+
#### `triggerOnSidePanelAction(payload: MainPanelAction)`
|
|
191
|
+
|
|
192
|
+
Triggers a side panel action event. Sets up a listener that responds when the plugin calls `onSidePanelAction()`.
|
|
193
|
+
|
|
194
|
+
**Important**: Call this BEFORE navigating to the page, so the listener is ready when the plugin initializes.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
await env.event.triggerOnSidePanelAction({
|
|
198
|
+
plugin_id: pluginId,
|
|
199
|
+
action_key: 'translate',
|
|
200
|
+
action: 'translate',
|
|
201
|
+
text: 'tree',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await page.goto(`${pluginUrl}/#/sidebar/translate`);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### `triggerOnMainPanelAction(payload: MainPanelAction)`
|
|
208
|
+
|
|
209
|
+
Triggers a main panel action event. Sets up a listener that responds when the plugin calls `onMainPanelAction()`.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
await env.event.triggerOnMainPanelAction({
|
|
213
|
+
plugin_id: pluginId,
|
|
214
|
+
action_key: 'open',
|
|
215
|
+
action: 'open',
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Mock Options
|
|
220
|
+
|
|
221
|
+
All mock methods accept an optional `MockOptions` parameter:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface MockOptions {
|
|
225
|
+
// Delay before response (milliseconds)
|
|
226
|
+
delay?: number;
|
|
227
|
+
|
|
228
|
+
// Request matcher function
|
|
229
|
+
matcher?: (request: Request) => boolean;
|
|
230
|
+
|
|
231
|
+
// HTTP method override
|
|
232
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
233
|
+
|
|
234
|
+
// Custom response headers
|
|
235
|
+
headers?: Record<string, string>;
|
|
236
|
+
|
|
237
|
+
// Simulate network error
|
|
238
|
+
error?: 'aborted' | 'connectionfailed' | 'timedout' | /* ... */;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Example with matcher:**
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
env.ai.mockGetObject(
|
|
246
|
+
{ result: 'data' },
|
|
247
|
+
{
|
|
248
|
+
matcher: (request) => {
|
|
249
|
+
const body = request.postDataJSON();
|
|
250
|
+
return body?.instructions?.includes('specific text') ?? false;
|
|
251
|
+
},
|
|
252
|
+
delay: 500, // Simulate network delay
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Common Patterns
|
|
258
|
+
|
|
259
|
+
### Testing Settings Flow
|
|
260
|
+
|
|
261
|
+
The plugin settings flow involves GET → PATCH → POST:
|
|
262
|
+
|
|
263
|
+
1. **GET** - Check if settings exist (returns null if not found)
|
|
264
|
+
2. **PATCH** - Try to update (returns empty array if no rows updated)
|
|
265
|
+
3. **POST** - Insert new settings
|
|
266
|
+
|
|
267
|
+
The test environment sets up default handlers for all three, but you can override them:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Override to return existing settings
|
|
271
|
+
env.plugin.mockGetSettings({
|
|
272
|
+
id: 'existing-id',
|
|
273
|
+
plugin_id: pluginId,
|
|
274
|
+
settings: { existing: 'data' },
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Override to simulate successful update (don't trigger INSERT)
|
|
278
|
+
env.plugin.mockSetSettings([{ id: 'updated-id' }]);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Testing Action Events
|
|
282
|
+
|
|
283
|
+
Action events work differently for main panel vs sidebar:
|
|
284
|
+
|
|
285
|
+
**Side Panel Action:**
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Plugin is on a sidebar page, uses onSidePanelAction()
|
|
289
|
+
await env.event.triggerOnSidePanelAction({
|
|
290
|
+
plugin_id: pluginId,
|
|
291
|
+
action_key: 'translate',
|
|
292
|
+
action: 'translate',
|
|
293
|
+
text: 'word',
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Main Panel Action:**
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Plugin is on a main panel page, uses onMainPanelAction()
|
|
301
|
+
await env.event.triggerOnMainPanelAction({
|
|
302
|
+
plugin_id: pluginId,
|
|
303
|
+
action_key: 'open',
|
|
304
|
+
action: 'open',
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Mocking Multiple Responses for Same Endpoint
|
|
309
|
+
|
|
310
|
+
Use matchers to provide different responses for the same endpoint:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// First request - word lookup
|
|
314
|
+
env.ai.mockGetObject(
|
|
315
|
+
{ type: 'noun', translation: 'hund' },
|
|
316
|
+
{
|
|
317
|
+
matcher: (req) => {
|
|
318
|
+
return req.postDataJSON()?.instructions?.includes('Look up') ?? false;
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Second request - example sentence
|
|
324
|
+
env.ai.mockGetObject(
|
|
325
|
+
{ example_sentence: { target_language: 'Jag har en hund.' } },
|
|
326
|
+
{
|
|
327
|
+
matcher: (req) => {
|
|
328
|
+
return req.postDataJSON()?.instructions?.includes('example sentence') ?? false;
|
|
329
|
+
},
|
|
330
|
+
delay: 1000, // Simulate slower response
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Examples
|
|
336
|
+
|
|
337
|
+
### Complete Translation Plugin Test
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { test, expect } from '@playwright/test';
|
|
341
|
+
import { RimoriTestEnvironment } from '@rimori/playwright';
|
|
342
|
+
|
|
343
|
+
const pluginId = 'pl7720512027';
|
|
344
|
+
const pluginUrl = 'http://localhost:3009';
|
|
345
|
+
|
|
346
|
+
test.describe('Translator Plugin', () => {
|
|
347
|
+
let env: RimoriTestEnvironment;
|
|
348
|
+
|
|
349
|
+
test.beforeEach(async ({ page }) => {
|
|
350
|
+
env = new RimoriTestEnvironment({ page, pluginId });
|
|
351
|
+
|
|
352
|
+
// Mock translation lookup
|
|
353
|
+
env.ai.mockGetObject(
|
|
354
|
+
{
|
|
355
|
+
gramatically_corrected_input_text: 'tree',
|
|
356
|
+
detected_language: 'English',
|
|
357
|
+
text_type: 'noun',
|
|
358
|
+
translation_swedish: 'träd',
|
|
359
|
+
translation_mother_tongue: 'tree',
|
|
360
|
+
en_ett_word: 'ett',
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
matcher: (req) => {
|
|
364
|
+
return req.postDataJSON()?.instructions?.includes('Look up') ?? false;
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Mock example sentence (with delay)
|
|
370
|
+
env.ai.mockGetObject(
|
|
371
|
+
{
|
|
372
|
+
example_sentence: {
|
|
373
|
+
target_language: 'Jag ser ett träd.',
|
|
374
|
+
english: 'I see a tree.',
|
|
375
|
+
},
|
|
376
|
+
explanation: 'A tall perennial plant.',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
delay: 1000,
|
|
380
|
+
matcher: (req) => {
|
|
381
|
+
return req.postDataJSON()?.instructions?.includes('example') ?? false;
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await env.setup();
|
|
387
|
+
await page.goto(`${pluginUrl}/#/sidebar/translate`);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('translates word correctly', async ({ page }) => {
|
|
391
|
+
await page.getByRole('textbox').fill('tree');
|
|
392
|
+
await page.getByRole('button', { name: 'Look up word' }).click();
|
|
393
|
+
|
|
394
|
+
await expect(page.getByText('träd')).toBeVisible();
|
|
395
|
+
await expect(page.getByText('ett')).toBeVisible();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Testing with Side Panel Actions
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
test('handles side panel action', async ({ page }) => {
|
|
404
|
+
// Set up action BEFORE navigating
|
|
405
|
+
await env.event.triggerOnSidePanelAction({
|
|
406
|
+
plugin_id: pluginId,
|
|
407
|
+
action_key: 'translate',
|
|
408
|
+
action: 'translate',
|
|
409
|
+
text: 'tree',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await page.goto(`${pluginUrl}/#/sidebar/translate`);
|
|
413
|
+
|
|
414
|
+
// Plugin receives the action and starts translation
|
|
415
|
+
await expect(page.getByText('träd')).toBeVisible();
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Testing Streaming Responses
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
test('handles streaming chat responses', async ({ page }) => {
|
|
423
|
+
// Mock streaming response for chat
|
|
424
|
+
env.ai.mockGetSteamedText('This is the AI response that will be streamed.');
|
|
425
|
+
|
|
426
|
+
await env.setup();
|
|
427
|
+
await page.goto(`${pluginUrl}/#/sidebar/translate`);
|
|
428
|
+
|
|
429
|
+
// Type a question
|
|
430
|
+
await page.getByRole('textbox', { name: 'Ask questions...' }).fill('Explain this');
|
|
431
|
+
await page.keyboard.press('Enter');
|
|
432
|
+
|
|
433
|
+
// Response should appear (formatted as SSE)
|
|
434
|
+
await expect(page.getByText('This is the AI response')).toBeVisible();
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Default Behavior
|
|
439
|
+
|
|
440
|
+
The test environment automatically provides:
|
|
441
|
+
|
|
442
|
+
- **Default RimoriInfo**: Test credentials, guild info, user profile
|
|
443
|
+
- **Default route handlers**:
|
|
444
|
+
- `GET /plugin_settings` → returns `null` (no settings)
|
|
445
|
+
- `PATCH /plugin_settings` → returns `[]` (no rows updated, triggers INSERT)
|
|
446
|
+
- `POST /plugin_settings` → returns success response
|
|
447
|
+
- **MessageChannel communication**: Fully set up and ready
|
|
448
|
+
|
|
449
|
+
You can override any of these defaults by calling the appropriate mock methods.
|
|
450
|
+
|
|
451
|
+
## Limitations
|
|
452
|
+
|
|
453
|
+
### Streaming Responses
|
|
454
|
+
|
|
455
|
+
Due to Playwright's `route.fulfill()` requiring a complete response body, streaming responses (via `mockGetSteamedText`) send all SSE chunks at once. The client will parse them correctly as SSE, but incremental timing/delays won't be visible in the UI.
|
|
456
|
+
|
|
457
|
+
For true streaming with visible delays, use a real HTTP server instead of route mocking.
|
|
458
|
+
|
|
459
|
+
### Standalone Mode
|
|
460
|
+
|
|
461
|
+
The test environment forces iframe mode (not standalone). Plugins that rely on standalone mode behavior may need different test setups.
|
|
462
|
+
|
|
463
|
+
## Troubleshooting
|
|
464
|
+
|
|
465
|
+
### "No route handler found"
|
|
466
|
+
|
|
467
|
+
If you see this error, add a mock for the missing route:
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
env.plugin.mockGetSettings(null); // or env.ai.mockGetObject(...), etc.
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Plugin not receiving events
|
|
474
|
+
|
|
475
|
+
Make sure to call `triggerOnSidePanelAction` or `triggerOnMainPanelAction` BEFORE navigating:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// ✅ Correct
|
|
479
|
+
await env.event.triggerOnSidePanelAction(payload);
|
|
480
|
+
await page.goto(pluginUrl);
|
|
481
|
+
|
|
482
|
+
// ❌ Wrong - listener not ready
|
|
483
|
+
await page.goto(pluginUrl);
|
|
484
|
+
await env.event.triggerOnSidePanelAction(payload);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Settings not being saved
|
|
488
|
+
|
|
489
|
+
The default flow is: GET → PATCH (empty) → POST. If your test expects different behavior, override the handlers:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// Simulate settings already exist
|
|
493
|
+
env.plugin.mockGetSettings({ id: 'existing', settings: {...} });
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## License
|
|
497
|
+
|
|
498
|
+
Apache License 2.0
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// import { expect, type Page } from "@playwright/test";
|
|
3
|
+
// import type { RimoriTestEnvironment } from "../core/RimoriTestEnvironment";
|
|
4
|
+
// import type { EventHandler } from "../types/event-bus";
|
|
5
|
+
// export async function waitForEvent(
|
|
6
|
+
// environment: RimoriTestEnvironment,
|
|
7
|
+
// topic: string,
|
|
8
|
+
// timeout = 10000
|
|
9
|
+
// ): Promise<unknown> {
|
|
10
|
+
// return new Promise((resolve, reject) => {
|
|
11
|
+
// let timer: NodeJS.Timeout | undefined;
|
|
12
|
+
// const handler: EventHandler = ({ data, event }) => {
|
|
13
|
+
// if (event.topic !== topic) {
|
|
14
|
+
// return;
|
|
15
|
+
// }
|
|
16
|
+
// if (timer) {
|
|
17
|
+
// clearTimeout(timer);
|
|
18
|
+
// }
|
|
19
|
+
// environment.offEvent(topic, handler);
|
|
20
|
+
// resolve(data);
|
|
21
|
+
// };
|
|
22
|
+
// timer = setTimeout(() => {
|
|
23
|
+
// environment.offEvent(topic, handler);
|
|
24
|
+
// reject(new Error(`Timed out waiting for event ${topic}`));
|
|
25
|
+
// }, timeout);
|
|
26
|
+
// environment.onEvent(topic, handler);
|
|
27
|
+
// });
|
|
28
|
+
// }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
type Language = {
|
|
3
|
+
code: string;
|
|
4
|
+
name: string;
|
|
5
|
+
native: string;
|
|
6
|
+
capitalized: string;
|
|
7
|
+
uppercase: string;
|
|
8
|
+
};
|
|
9
|
+
type StudyBuddy = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
avatarUrl: string;
|
|
14
|
+
voiceId: string;
|
|
15
|
+
aiPersonality: string;
|
|
16
|
+
};
|
|
17
|
+
export type UserInfo = {
|
|
18
|
+
mother_tongue: Language;
|
|
19
|
+
target_language: Language;
|
|
20
|
+
skill_level_reading: string;
|
|
21
|
+
skill_level_writing: string;
|
|
22
|
+
skill_level_grammar: string;
|
|
23
|
+
skill_level_speaking: string;
|
|
24
|
+
skill_level_listening: string;
|
|
25
|
+
skill_level_understanding: string;
|
|
26
|
+
goal_longterm: string;
|
|
27
|
+
goal_weekly: string;
|
|
28
|
+
study_buddy: StudyBuddy;
|
|
29
|
+
story_genre: string;
|
|
30
|
+
study_duration: number;
|
|
31
|
+
motivation_type: string;
|
|
32
|
+
onboarding_completed: boolean;
|
|
33
|
+
context_menu_on_select: boolean;
|
|
34
|
+
user_name?: string;
|
|
35
|
+
target_country: string;
|
|
36
|
+
target_city?: string;
|
|
37
|
+
};
|
|
38
|
+
type RimoriGuild = {
|
|
39
|
+
id: string;
|
|
40
|
+
longTermGoalOverride: string;
|
|
41
|
+
allowUserPluginSettings: boolean;
|
|
42
|
+
};
|
|
43
|
+
type PluginInfo = {
|
|
44
|
+
id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
description: string;
|
|
47
|
+
logo: string;
|
|
48
|
+
url: string;
|
|
49
|
+
};
|
|
50
|
+
type RimoriInfo = {
|
|
51
|
+
url: string;
|
|
52
|
+
key: string;
|
|
53
|
+
backendUrl: string;
|
|
54
|
+
token: string;
|
|
55
|
+
expiration: Date;
|
|
56
|
+
tablePrefix: string;
|
|
57
|
+
pluginId: string;
|
|
58
|
+
guild: RimoriGuild;
|
|
59
|
+
installedPlugins: PluginInfo[];
|
|
60
|
+
profile: UserInfo;
|
|
61
|
+
mainPanelPlugin?: PluginInfo;
|
|
62
|
+
sidePanelPlugin?: PluginInfo;
|
|
63
|
+
};
|
|
64
|
+
type EventBusMessage = {
|
|
65
|
+
timestamp: string;
|
|
66
|
+
sender: string;
|
|
67
|
+
topic: string;
|
|
68
|
+
data: unknown;
|
|
69
|
+
debug: boolean;
|
|
70
|
+
eventId?: number;
|
|
71
|
+
};
|
|
72
|
+
type MessageChannelSimulatorArgs = {
|
|
73
|
+
page: Page;
|
|
74
|
+
pluginId: string;
|
|
75
|
+
queryParams?: Record<string, string>;
|
|
76
|
+
rimoriInfo?: RimoriInfo;
|
|
77
|
+
};
|
|
78
|
+
type EventListener = (event: EventBusMessage) => void | Promise<void>;
|
|
79
|
+
export declare class MessageChannelSimulator {
|
|
80
|
+
private readonly page;
|
|
81
|
+
private readonly pluginId;
|
|
82
|
+
private readonly queryParams;
|
|
83
|
+
private readonly baseUserInfo;
|
|
84
|
+
private readonly providedInfo?;
|
|
85
|
+
private readonly listeners;
|
|
86
|
+
private readonly autoResponders;
|
|
87
|
+
private readonly pendingOutbound;
|
|
88
|
+
private currentUserInfo;
|
|
89
|
+
private currentRimoriInfo;
|
|
90
|
+
private isReady;
|
|
91
|
+
private instanceId;
|
|
92
|
+
/**
|
|
93
|
+
* Creates a simulator that mimics the Rimori host for plugin tests.
|
|
94
|
+
* @param param
|
|
95
|
+
* @param param.page - Playwright page hosting the plugin iframe.
|
|
96
|
+
* @param param.pluginId - Target plugin identifier.
|
|
97
|
+
* @param param.queryParams - Query parameters forwarded to the plugin init.
|
|
98
|
+
*/
|
|
99
|
+
constructor({ page, pluginId, queryParams, rimoriInfo }: MessageChannelSimulatorArgs);
|
|
100
|
+
get defaultUserInfo(): UserInfo;
|
|
101
|
+
get userInfo(): UserInfo;
|
|
102
|
+
/**
|
|
103
|
+
* Injects the handshake shims so the plugin talks to this simulator.
|
|
104
|
+
*/
|
|
105
|
+
initialize(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Sends an event into the plugin as though the Rimori parent emitted it.
|
|
108
|
+
*/
|
|
109
|
+
emit(topic: string, data: unknown, sender?: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Registers a handler for events emitted from the plugin.
|
|
112
|
+
*/
|
|
113
|
+
on(topic: string, handler: EventListener): () => void;
|
|
114
|
+
/**
|
|
115
|
+
* Overrides the default profile returned by the auto responders.
|
|
116
|
+
*/
|
|
117
|
+
setUserInfo(overrides: Partial<UserInfo>): void;
|
|
118
|
+
getRimoriInfo(): RimoriInfo | null;
|
|
119
|
+
private setupMessageChannel;
|
|
120
|
+
private sendToPlugin;
|
|
121
|
+
private flushPending;
|
|
122
|
+
private handlePortMessage;
|
|
123
|
+
private dispatchEvent;
|
|
124
|
+
private maybeRespond;
|
|
125
|
+
private buildRimoriInfo;
|
|
126
|
+
private serializeRimoriInfo;
|
|
127
|
+
private cloneUserInfo;
|
|
128
|
+
private mergeUserInfo;
|
|
129
|
+
private registerAutoResponders;
|
|
130
|
+
private cloneRimoriInfo;
|
|
131
|
+
}
|
|
132
|
+
export {};
|