@rimori/playwright-testing 0.2.1 → 0.2.3-next.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/README.md CHANGED
@@ -17,6 +17,157 @@ The `@rimori/playwright-testing` package enables end-to-end testing of Rimori pl
17
17
  npm install --save-dev @rimori/playwright-testing @playwright/test
18
18
  # or
19
19
  pnpm add -D @rimori/playwright-testing @playwright/test
20
+ # or
21
+ yarn add -D @rimori/playwright-testing @playwright/test
22
+ ```
23
+
24
+ ## Setup Steps
25
+
26
+ To initialize Playwright testing in your Rimori plugin, follow these steps:
27
+
28
+ ### 1. Install Dependencies
29
+
30
+ Add the required dependencies to your `package.json`:
31
+
32
+ ```json
33
+ {
34
+ "devDependencies": {
35
+ "@playwright/test": "^1.40.0",
36
+ "@rimori/playwright-testing": "^0.2.1"
37
+ }
38
+ }
39
+ ```
40
+
41
+ Then run:
42
+
43
+ ```bash
44
+ npm install
45
+ # or
46
+ yarn install
47
+ # or
48
+ pnpm install
49
+ ```
50
+
51
+ ### 2. Create Playwright Configuration
52
+
53
+ Create a `playwright.config.ts` file in your plugin root:
54
+
55
+ ```typescript
56
+ import { defineConfig, devices } from '@playwright/test';
57
+
58
+ export default defineConfig({
59
+ testDir: './test',
60
+ fullyParallel: true,
61
+ forbidOnly: !!process.env.CI,
62
+ retries: process.env.CI ? 2 : 0,
63
+ workers: process.env.CI ? 1 : undefined,
64
+ reporter: 'html',
65
+ use: {
66
+ trace: 'on-first-retry',
67
+ headless: false,
68
+ screenshot: 'only-on-failure',
69
+ },
70
+ timeout: 30000,
71
+ expect: {
72
+ timeout: 5000,
73
+ },
74
+ projects: [
75
+ {
76
+ name: 'chromium',
77
+ use: { ...devices['Desktop Chrome'] },
78
+ },
79
+ ],
80
+ });
81
+ ```
82
+
83
+ ### 3. Add Test Scripts
84
+
85
+ Add test scripts to your `package.json`:
86
+
87
+ ```json
88
+ {
89
+ "scripts": {
90
+ "test": "playwright test",
91
+ "test:headed": "playwright test --headed",
92
+ "test:debug": "playwright test --debug",
93
+ "test:ui": "playwright test --ui",
94
+ "test:headed:debug": "playwright test --headed --debug"
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### 4. Create Test Directory and Files
100
+
101
+ Create a `test` directory in your plugin root and add your test files:
102
+
103
+ ```typescript
104
+ // test/my-plugin.test.ts
105
+ import { test, expect } from '@playwright/test';
106
+ import { RimoriTestEnvironment } from '@rimori/playwright-testing';
107
+
108
+ const pluginId = 'pl1234567890'; // Your plugin ID from rimori.config.ts
109
+ const pluginUrl = 'http://localhost:3002'; // Your dev server URL
110
+
111
+ test.describe('My Plugin', () => {
112
+ let env: RimoriTestEnvironment;
113
+
114
+ test.beforeEach(async ({ page }) => {
115
+ env = new RimoriTestEnvironment({ page, pluginId });
116
+
117
+ // Set up your mocks here
118
+ // env.ai.mockGetObject(...);
119
+ // env.plugin.mockGetSettings(...);
120
+
121
+ await env.setup();
122
+ await page.goto(`${pluginUrl}/#/your-page`);
123
+ });
124
+
125
+ test('should work correctly', async ({ page }) => {
126
+ await expect(page.getByText('Hello')).toBeVisible();
127
+ });
128
+ });
129
+ ```
130
+
131
+ ### 5. Get Your Plugin ID
132
+
133
+ Find your plugin ID in `rimori/rimori.config.ts`:
134
+
135
+ ```typescript
136
+ const config: RimoriPluginConfig = {
137
+ id: 'pl1234567890', // <-- This is your plugin ID
138
+ // ...
139
+ };
140
+ ```
141
+
142
+ ### 6. Run Tests
143
+
144
+ 1. **Start your dev server** in one terminal:
145
+
146
+ ```bash
147
+ npm run dev
148
+ # or
149
+ yarn dev
150
+ ```
151
+
152
+ 2. **Run tests** in another terminal:
153
+ ```bash
154
+ npm test
155
+ # or
156
+ yarn test
157
+ ```
158
+
159
+ ### Complete Example Setup
160
+
161
+ Here's a complete example of what your plugin structure should look like:
162
+
163
+ ```
164
+ your-plugin/
165
+ ├── package.json # With @playwright/test and test scripts
166
+ ├── playwright.config.ts # Playwright configuration
167
+ ├── rimori/
168
+ │ └── rimori.config.ts # Contains your plugin ID
169
+ └── test/
170
+ └── my-plugin.test.ts # Your test files
20
171
  ```
21
172
 
22
173
  ## Quick Start
@@ -1,92 +1,21 @@
1
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
- };
2
+ import { UserInfo, RimoriInfo, EventBusMessage, EventPayload } from '@rimori/client';
72
3
  type MessageChannelSimulatorArgs = {
73
4
  page: Page;
74
5
  pluginId: string;
6
+ rimoriInfo: RimoriInfo;
75
7
  queryParams?: Record<string, string>;
76
- rimoriInfo?: RimoriInfo;
77
8
  };
78
9
  type EventListener = (event: EventBusMessage) => void | Promise<void>;
10
+ type AutoResponder = (event: EventBusMessage) => unknown | Promise<unknown>;
79
11
  export declare class MessageChannelSimulator {
80
12
  private readonly page;
81
13
  private readonly pluginId;
82
14
  private readonly queryParams;
83
- private readonly baseUserInfo;
84
- private readonly providedInfo?;
15
+ private readonly rimoriInfo;
85
16
  private readonly listeners;
86
17
  private readonly autoResponders;
87
18
  private readonly pendingOutbound;
88
- private currentUserInfo;
89
- private currentRimoriInfo;
90
19
  private isReady;
91
20
  private instanceId;
92
21
  /**
@@ -106,26 +35,33 @@ export declare class MessageChannelSimulator {
106
35
  /**
107
36
  * Sends an event into the plugin as though the Rimori parent emitted it.
108
37
  */
109
- emit(topic: string, data: unknown, sender?: string): Promise<void>;
38
+ emit(topic: string, data: EventPayload, sender?: string): Promise<void>;
110
39
  /**
111
40
  * Registers a handler for events emitted from the plugin.
112
41
  */
113
42
  on(topic: string, handler: EventListener): () => void;
114
43
  /**
115
- * Overrides the default profile returned by the auto responders.
44
+ * Registers a one-time auto-responder for a request/response topic.
45
+ * When a request with an eventId comes in for this topic, the responder will
46
+ * be called once and then automatically removed.
47
+ *
48
+ * @param topic - The event topic to respond to
49
+ * @param responder - A function that returns the response data, or a value to return directly
50
+ * @returns A function to manually remove the responder before it's used
51
+ */
52
+ respondOnce(topic: string, responder: AutoResponder | unknown): () => void;
53
+ /**
54
+ * Overrides the user info.
116
55
  */
117
- setUserInfo(overrides: Partial<UserInfo>): void;
118
- getRimoriInfo(): RimoriInfo | null;
56
+ setUserInfo(userInfo: UserInfo): void;
57
+ getRimoriInfo(): RimoriInfo;
119
58
  private setupMessageChannel;
120
59
  private sendToPlugin;
121
60
  private flushPending;
122
61
  private handlePortMessage;
123
62
  private dispatchEvent;
124
63
  private maybeRespond;
125
- private buildRimoriInfo;
126
- private serializeRimoriInfo;
127
64
  private cloneUserInfo;
128
- private mergeUserInfo;
129
65
  private registerAutoResponders;
130
66
  private cloneRimoriInfo;
131
67
  }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MessageChannelSimulator = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
- const default_user_info_1 = require("../fixtures/default-user-info");
6
5
  class MessageChannelSimulator {
7
6
  /**
8
7
  * Creates a simulator that mimics the Rimori host for plugin tests.
@@ -15,22 +14,19 @@ class MessageChannelSimulator {
15
14
  this.listeners = new Map();
16
15
  this.autoResponders = new Map();
17
16
  this.pendingOutbound = [];
18
- this.currentRimoriInfo = null;
19
17
  this.isReady = false;
20
18
  this.instanceId = (0, node_crypto_1.randomUUID)();
21
19
  this.page = page;
22
20
  this.pluginId = pluginId;
23
21
  this.queryParams = queryParams ?? {};
24
- this.baseUserInfo = this.cloneUserInfo(default_user_info_1.DEFAULT_USER_INFO);
25
- this.currentUserInfo = this.cloneUserInfo(default_user_info_1.DEFAULT_USER_INFO);
26
- this.providedInfo = rimoriInfo ? this.cloneRimoriInfo(rimoriInfo) : undefined;
22
+ this.rimoriInfo = this.cloneRimoriInfo(rimoriInfo);
27
23
  this.registerAutoResponders();
28
24
  }
29
25
  get defaultUserInfo() {
30
- return this.cloneUserInfo(this.baseUserInfo);
26
+ return this.cloneUserInfo(this.rimoriInfo.profile);
31
27
  }
32
28
  get userInfo() {
33
- return this.cloneUserInfo(this.currentUserInfo);
29
+ return this.cloneUserInfo(this.rimoriInfo.profile);
34
30
  }
35
31
  /**
36
32
  * Injects the handshake shims so the plugin talks to this simulator.
@@ -112,6 +108,7 @@ class MessageChannelSimulator {
112
108
  const message = {
113
109
  event: {
114
110
  timestamp: new Date().toISOString(),
111
+ eventId: Math.floor(Math.random() * 1000000),
115
112
  sender,
116
113
  topic,
117
114
  data,
@@ -143,25 +140,52 @@ class MessageChannelSimulator {
143
140
  };
144
141
  }
145
142
  /**
146
- * Overrides the default profile returned by the auto responders.
143
+ * Registers a one-time auto-responder for a request/response topic.
144
+ * When a request with an eventId comes in for this topic, the responder will
145
+ * be called once and then automatically removed.
146
+ *
147
+ * @param topic - The event topic to respond to
148
+ * @param responder - A function that returns the response data, or a value to return directly
149
+ * @returns A function to manually remove the responder before it's used
147
150
  */
148
- setUserInfo(overrides) {
149
- this.currentUserInfo = this.mergeUserInfo(this.currentUserInfo, overrides);
150
- if (this.currentRimoriInfo) {
151
- this.currentRimoriInfo.profile = this.cloneUserInfo(this.currentUserInfo);
152
- }
151
+ respondOnce(topic, responder) {
152
+ let used = false;
153
+ const wrappedResponder = (event) => {
154
+ if (used) {
155
+ return undefined;
156
+ }
157
+ used = true;
158
+ // Remove from autoResponders after first use
159
+ this.autoResponders.delete(topic);
160
+ // If responder is a function, call it with the event, otherwise return the value directly
161
+ if (typeof responder === 'function') {
162
+ return responder(event);
163
+ }
164
+ return responder;
165
+ };
166
+ this.autoResponders.set(topic, wrappedResponder);
167
+ // Return a function to manually remove the responder
168
+ return () => {
169
+ if (!used) {
170
+ this.autoResponders.delete(topic);
171
+ used = true;
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * Overrides the user info.
177
+ */
178
+ setUserInfo(userInfo) {
179
+ this.rimoriInfo.profile = userInfo;
153
180
  }
154
181
  getRimoriInfo() {
155
- return this.currentRimoriInfo ? this.cloneRimoriInfo(this.currentRimoriInfo) : null;
182
+ return this.cloneRimoriInfo(this.rimoriInfo);
156
183
  }
157
184
  async setupMessageChannel() {
158
185
  if (this.isReady) {
159
186
  return;
160
187
  }
161
- const rimoriInfo = this.buildRimoriInfo();
162
- this.currentRimoriInfo = rimoriInfo;
163
- const serialized = this.serializeRimoriInfo(rimoriInfo);
164
- await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo: info, }) => {
188
+ await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo, }) => {
165
189
  const channel = new MessageChannel();
166
190
  channel.port1.onmessage = (event) => {
167
191
  // @ts-expect-error binding injected via exposeBinding
@@ -176,10 +200,7 @@ class MessageChannelSimulator {
176
200
  pluginId,
177
201
  instanceId,
178
202
  queryParams,
179
- rimoriInfo: {
180
- ...info,
181
- expiration: new Date(info.expiration),
182
- },
203
+ rimoriInfo,
183
204
  },
184
205
  ports: [channel.port2],
185
206
  });
@@ -188,7 +209,7 @@ class MessageChannelSimulator {
188
209
  pluginId: this.pluginId,
189
210
  queryParams: this.queryParams,
190
211
  instanceId: this.instanceId,
191
- rimoriInfo: serialized,
212
+ rimoriInfo: this.rimoriInfo,
192
213
  });
193
214
  this.isReady = true;
194
215
  await this.flushPending();
@@ -215,21 +236,33 @@ class MessageChannelSimulator {
215
236
  return;
216
237
  }
217
238
  if ('event' in payload && payload.event) {
218
- console.log('[MessageChannelSimulator] handlePortMessage - received event:', payload.event.topic, 'from:', payload.event.sender);
239
+ // console.log(
240
+ // '[MessageChannelSimulator] handlePortMessage - received event:',
241
+ // payload.event.topic,
242
+ // 'from:',
243
+ // payload.event.sender,
244
+ // );
219
245
  await this.dispatchEvent(payload.event);
220
246
  await this.maybeRespond(payload.event);
221
247
  return;
222
248
  }
223
249
  }
224
250
  async dispatchEvent(event) {
225
- console.log('[MessageChannelSimulator] dispatchEvent - topic:', event.topic, 'sender:', event.sender, 'listeners:', this.listeners.has(event.topic) ? this.listeners.get(event.topic)?.size : 0);
251
+ // console.log(
252
+ // '[MessageChannelSimulator] dispatchEvent - topic:',
253
+ // event.topic,
254
+ // 'sender:',
255
+ // event.sender,
256
+ // 'listeners:',
257
+ // this.listeners.has(event.topic) ? this.listeners.get(event.topic)?.size : 0,
258
+ // );
226
259
  const handlers = this.listeners.get(event.topic);
227
260
  if (!handlers?.size) {
228
261
  console.log('[MessageChannelSimulator] No handlers found for topic:', event.topic);
229
262
  console.log('[MessageChannelSimulator] Available topics:', Array.from(this.listeners.keys()));
230
263
  return;
231
264
  }
232
- console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
265
+ // console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
233
266
  for (const handler of handlers) {
234
267
  await handler(event);
235
268
  }
@@ -252,90 +285,16 @@ class MessageChannelSimulator {
252
285
  },
253
286
  });
254
287
  }
255
- buildRimoriInfo() {
256
- if (this.providedInfo) {
257
- const clone = this.cloneRimoriInfo(this.providedInfo);
258
- clone.profile = this.cloneUserInfo(this.currentUserInfo);
259
- clone.pluginId = this.pluginId;
260
- clone.tablePrefix = clone.tablePrefix || `${this.pluginId}_`;
261
- return clone;
262
- }
263
- return {
264
- url: 'http://localhost:3500',
265
- key: 'rimori-sdk-key',
266
- backendUrl: 'http://localhost:3501',
267
- token: 'rimori-token',
268
- expiration: new Date(Date.now() + 60 * 60 * 1000),
269
- tablePrefix: `${this.pluginId}_`,
270
- pluginId: this.pluginId,
271
- guild: {
272
- id: 'guild-test',
273
- longTermGoalOverride: '',
274
- allowUserPluginSettings: true,
275
- },
276
- installedPlugins: [
277
- {
278
- id: this.pluginId,
279
- title: 'Test Plugin',
280
- description: 'Playwright testing plugin',
281
- logo: '',
282
- url: 'https://plugins.rimori.localhost',
283
- },
284
- ],
285
- profile: this.cloneUserInfo(this.currentUserInfo),
286
- };
287
- }
288
- serializeRimoriInfo(info) {
289
- return {
290
- ...info,
291
- expiration: info.expiration.toISOString(),
292
- };
293
- }
294
288
  cloneUserInfo(input) {
295
289
  return JSON.parse(JSON.stringify(input));
296
290
  }
297
- mergeUserInfo(current, overrides) {
298
- const clone = this.cloneUserInfo(current);
299
- if (overrides.mother_tongue) {
300
- clone.mother_tongue = {
301
- ...clone.mother_tongue,
302
- ...overrides.mother_tongue,
303
- };
304
- }
305
- if (overrides.target_language) {
306
- clone.target_language = {
307
- ...clone.target_language,
308
- ...overrides.target_language,
309
- };
310
- }
311
- if (overrides.study_buddy) {
312
- clone.study_buddy = {
313
- ...clone.study_buddy,
314
- ...overrides.study_buddy,
315
- };
316
- }
317
- const { mother_tongue, target_language, study_buddy, ...rest } = overrides;
318
- for (const [key, value] of Object.entries(rest)) {
319
- if (value === undefined) {
320
- continue;
321
- }
322
- clone[key] = value;
323
- }
324
- return clone;
325
- }
326
291
  registerAutoResponders() {
327
- this.autoResponders.set('global.supabase.requestAccess', () => this.buildRimoriInfo());
328
- this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.currentUserInfo));
329
- this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.currentUserInfo));
292
+ this.autoResponders.set('global.supabase.requestAccess', () => this.cloneRimoriInfo(this.rimoriInfo));
293
+ this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
294
+ this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
330
295
  }
331
296
  cloneRimoriInfo(info) {
332
- return {
333
- ...info,
334
- expiration: new Date(info.expiration),
335
- guild: { ...info.guild },
336
- installedPlugins: info.installedPlugins.map((plugin) => ({ ...plugin })),
337
- profile: this.cloneUserInfo(info.profile),
338
- };
297
+ return JSON.parse(JSON.stringify(info));
339
298
  }
340
299
  }
341
300
  exports.MessageChannelSimulator = MessageChannelSimulator;