@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.
@@ -0,0 +1,446 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RimoriTestEnvironment = void 0;
4
+ const default_user_info_1 = require("../fixtures/default-user-info");
5
+ const MessageChannelSimulator_1 = require("./MessageChannelSimulator");
6
+ class RimoriTestEnvironment {
7
+ constructor(options) {
8
+ this.backendRoutes = {};
9
+ this.supabaseRoutes = {};
10
+ this.messageChannelSimulator = null;
11
+ this.plugin = {
12
+ /**
13
+ * Mocks PATCH request for updating plugin_settings.
14
+ * @param response - The response for PATCH. Defaults to empty array (no rows updated).
15
+ * Should return array with updated row(s) like [{ id: '...' }] if update succeeds.
16
+ */
17
+ mockSetSettings: (response, options) => {
18
+ console.log('Mocking set settings for mockSetSettings', response, options);
19
+ console.warn('mockSetSettings is not tested');
20
+ // PATCH request returns array of updated rows
21
+ // Empty array means no rows matched (will trigger INSERT)
22
+ // Array with items means update succeeded
23
+ const defaultResponse = response ?? [];
24
+ this.addSupabaseRoute('plugin_settings', defaultResponse, { ...options, method: 'PATCH' });
25
+ },
26
+ /**
27
+ * Mocks GET request for fetching plugin_settings.
28
+ * @param settingsRow - The full row object from plugin_settings table, or null if not found.
29
+ * Should include: { id, plugin_id, guild_id, settings, is_guild_setting, user_id }.
30
+ * If null, simulates no settings exist (triggers INSERT flow).
31
+ */
32
+ mockGetSettings: (settingsRow, options) => {
33
+ console.log('Mocking get settings for mockGetSettings', settingsRow, options);
34
+ console.warn('mockGetSettings is not tested');
35
+ // GET request returns the full row or null (from maybeSingle())
36
+ // null means no settings exist, which triggers setSettings() -> INSERT
37
+ this.addSupabaseRoute('plugin_settings', settingsRow, options);
38
+ },
39
+ /**
40
+ * Mocks POST request for inserting plugin_settings.
41
+ * @param response - The response for POST. Defaults to success response with inserted row.
42
+ */
43
+ mockInsertSettings: (response, options) => {
44
+ console.log('Mocking insert settings for mockInsertSettings', response, options);
45
+ console.warn('mockInsertSettings is not tested');
46
+ // POST request returns the inserted row or success response
47
+ // Default to an object representing successful insert
48
+ const defaultResponse = response ?? {
49
+ id: 'mock-settings-id',
50
+ plugin_id: this.pluginId,
51
+ guild_id: this.rimoriInfo.guild.id,
52
+ };
53
+ this.addSupabaseRoute('plugin_settings', defaultResponse, { ...options, method: 'POST' });
54
+ },
55
+ mockGetUserInfo: (userInfo, options) => {
56
+ console.log('Mocking get user info for mockGetUserInfo', userInfo, options);
57
+ console.warn('mockGetUserInfo is not tested');
58
+ this.addSupabaseRoute('/user-info', { ...this.rimoriInfo.profile, ...userInfo }, { ...options, delay: 0 });
59
+ },
60
+ mockGetPluginInfo: (pluginInfo, options) => {
61
+ console.log('Mocking get plugin info for mockGetPluginInfo', pluginInfo, options);
62
+ console.warn('mockGetPluginInfo is not tested');
63
+ this.addSupabaseRoute('/plugin-info', pluginInfo, options);
64
+ },
65
+ };
66
+ this.db = {
67
+ mockFrom: () => { },
68
+ mockTable: () => { },
69
+ };
70
+ this.event = {
71
+ mockEmit: () => { },
72
+ mockRequest: () => { },
73
+ mockOn: () => { },
74
+ mockOnce: () => { },
75
+ mockRespond: () => { },
76
+ mockEmitAccomplishment: () => { },
77
+ mockOnAccomplishment: () => { },
78
+ mockEmitSidebarAction: () => { },
79
+ /**
80
+ * Triggers a side panel action event as the parent application would.
81
+ * This simulates how rimori-main's SidebarPluginHandler responds to plugin's 'action.requestSidebar' events.
82
+ * @param payload - The action payload containing plugin_id, action_key, and action parameters
83
+ */
84
+ triggerOnSidePanelAction: async (payload) => {
85
+ if (!this.messageChannelSimulator) {
86
+ throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
87
+ }
88
+ const topic = `${this.pluginId}.action.requestSidebar`;
89
+ console.log('[RimoriTestEnvironment] Setting up listener for topic:', topic, 'with payload:', payload);
90
+ const actionPayload = payload;
91
+ const off = this.messageChannelSimulator.on(topic, async (event) => {
92
+ console.log('[RimoriTestEnvironment] Received action.requestSidebar event:', event);
93
+ console.log('[RimoriTestEnvironment] Responding to action.requestSidebar with payload:', actionPayload);
94
+ await this.messageChannelSimulator.emit(topic, actionPayload, 'sidebar');
95
+ off();
96
+ });
97
+ console.log('[RimoriTestEnvironment] Listener set up for topic:', topic);
98
+ },
99
+ /**
100
+ * Triggers a main panel action event as the parent application would.
101
+ * This simulates how rimori-main's MainPluginHandler uses EventBus.respond to respond
102
+ * to plugin's 'action.requestMain' events. When the plugin calls onMainPanelAction(),
103
+ * it emits '{pluginId}.action.requestMain' and listens for the response.
104
+ * This method sets up a responder that automatically responds when the plugin emits this event.
105
+ * @param payload - The main panel action payload containing plugin_id, action_key, and action parameters
106
+ */
107
+ triggerOnMainPanelAction: async (payload) => {
108
+ if (!this.messageChannelSimulator) {
109
+ throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
110
+ }
111
+ // Listen for when the plugin emits 'action.requestMain' (which becomes '{pluginId}.action.requestMain')
112
+ // and respond with the MainPanelAction payload, matching rimori-main's EventBus.respond behavior
113
+ const topic = `${this.pluginId}.action.requestMain`;
114
+ console.log('[RimoriTestEnvironment] Setting up listener for topic:', topic, 'with payload:', payload);
115
+ // Store the payload in a closure so we can respond with it
116
+ const actionPayload = payload;
117
+ // Set up a one-time listener that responds when the plugin emits 'action.requestMain'
118
+ // The handler receives the event object from the plugin
119
+ const off = this.messageChannelSimulator.on(topic, async (event) => {
120
+ console.log('[RimoriTestEnvironment] Received action.requestMain event:', event);
121
+ console.log('[RimoriTestEnvironment] Responding to action.requestMain with payload:', actionPayload);
122
+ // When plugin emits 'action.requestMain', respond with the MainPanelAction data
123
+ // The sender is 'mainPanel' to match rimori-main's MainPluginHandler behavior
124
+ await this.messageChannelSimulator.emit(topic, actionPayload, 'mainPanel');
125
+ off(); // Remove listener after responding once (one-time response like EventBus.respond)
126
+ });
127
+ console.log('[RimoriTestEnvironment] Listener set up for topic:', topic);
128
+ },
129
+ };
130
+ this.ai = {
131
+ mockGetText: (values, options) => {
132
+ console.log('Mocking get text for mockGetText', values, options);
133
+ console.warn('mockGetText is not tested');
134
+ this.addBackendRoute('/llm-text', values, options);
135
+ },
136
+ /**
137
+ * Mocks a streaming text response from the LLM endpoint.
138
+ * The text will be formatted as SSE (Server-Sent Events) to simulate streaming.
139
+ *
140
+ * **Note**: Due to Playwright's route.fulfill() requiring a complete response body,
141
+ * all SSE chunks are sent at once (no delays). The client will still parse it as SSE correctly.
142
+ *
143
+ * @param text - The text to stream. Will be formatted as SSE chunks.
144
+ * @param options - Optional mock options.
145
+ */
146
+ mockGetSteamedText: (text, options) => {
147
+ console.log('Mocking get steamed text for mockGetSteamedText', text, options);
148
+ console.warn('mockGetSteamedText is not tested');
149
+ this.addBackendRoute('/ai/llm', text, { ...options, isStreaming: true });
150
+ },
151
+ mockGetVoice: (values, options) => {
152
+ console.log('Mocking get voice for mockGetVoice', values, options);
153
+ console.warn('mockGetVoice is not tested');
154
+ this.addBackendRoute('/voice/tts', values, options);
155
+ },
156
+ mockGetTextFromVoice: (text, options) => {
157
+ console.log('Mocking get text from voice for mockGetTextFromVoice', text, options);
158
+ console.warn('mockGetTextFromVoice is not tested');
159
+ this.addBackendRoute('/voice/stt', text, options);
160
+ },
161
+ mockGetObject: (value, options) => {
162
+ console.log('Mocking get object for mockGetObject', value, options);
163
+ console.warn('mockGetObject is not tested');
164
+ this.addBackendRoute('/ai/llm-object', value, { ...options, method: 'POST' });
165
+ },
166
+ };
167
+ this.runtime = {
168
+ mockFetchBackend: () => { },
169
+ };
170
+ this.community = {
171
+ sharedContent: {
172
+ mockGet: () => { },
173
+ mockGetList: () => { },
174
+ mockGetNew: () => { },
175
+ mockCreate: () => { },
176
+ mockUpdate: () => { },
177
+ mockComplete: () => { },
178
+ mockUpdateState: () => { },
179
+ mockRemove: () => { },
180
+ },
181
+ };
182
+ this.exercise = {
183
+ mockView: () => { },
184
+ mockAdd: () => { },
185
+ mockDelete: () => { },
186
+ };
187
+ this.navigation = {
188
+ mockToDashboard: () => { },
189
+ };
190
+ this.page = options.page;
191
+ this.pluginId = options.pluginId;
192
+ // TODO move to a function
193
+ this.rimoriInfo = {
194
+ key: 'rimori-testing-key',
195
+ token: 'rimori-testing-token',
196
+ url: 'http://localhost:3500',
197
+ backendUrl: 'http://localhost:3501',
198
+ expiration: new Date(Date.now() + 60 * 60 * 1000),
199
+ tablePrefix: options.pluginId,
200
+ pluginId: options.pluginId,
201
+ guild: {
202
+ id: 'guild-test-id',
203
+ // @ts-ignore
204
+ name: 'Test Guild',
205
+ city: 'Test City',
206
+ country: 'Testland',
207
+ description: 'A dummy guild used for testing purposes.',
208
+ // @ts-ignore
209
+ createdAt: '2024-01-01T00:00:00.000Z',
210
+ updatedAt: '2024-01-02T00:00:00.000Z',
211
+ inviteCode: 'INVITE123',
212
+ isPublic: true,
213
+ isShadowGuild: false,
214
+ allowUserPluginSettings: true,
215
+ primaryLanguage: 'en',
216
+ ownerId: 'test-owner-user-id',
217
+ scope: 'test-scope',
218
+ longTermGoalOverride: '',
219
+ },
220
+ installedPlugins: options.installedPlugins ?? [],
221
+ profile: default_user_info_1.DEFAULT_USER_INFO,
222
+ mainPanelPlugin: undefined,
223
+ sidePanelPlugin: undefined,
224
+ };
225
+ this.interceptRoutes();
226
+ }
227
+ interceptRoutes() {
228
+ this.page.route(`${this.rimoriInfo.backendUrl}/**`, (route) => this.handleRoute(route, this.backendRoutes));
229
+ this.page.route(`${this.rimoriInfo.url}/**`, (route) => this.handleRoute(route, this.supabaseRoutes));
230
+ }
231
+ async setup() {
232
+ console.log('Setting up RimoriTestEnvironment');
233
+ // Add default handlers for common routes that plugins typically access
234
+ // These can be overridden by explicit mock calls
235
+ if (!this.supabaseRoutes[this.createRouteKey('GET', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
236
+ // Default: no settings exist (null) - triggers INSERT flow
237
+ // Can be overridden with mockGetSettings() to return existing settings
238
+ this.plugin.mockGetSettings(null);
239
+ }
240
+ if (!this.supabaseRoutes[this.createRouteKey('PATCH', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
241
+ // Default PATCH handler for plugin_settings - returns empty array (no rows updated)
242
+ // This triggers INSERT (POST) flow
243
+ // Can be overridden with mockSetSettings() to simulate successful update
244
+ this.plugin.mockSetSettings([]);
245
+ }
246
+ if (!this.supabaseRoutes[this.createRouteKey('POST', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
247
+ // Default POST handler for plugin_settings - simulates successful insert
248
+ // Can be overridden with mockInsertSettings() to customize response
249
+ this.plugin.mockInsertSettings();
250
+ }
251
+ // Initialize MessageChannelSimulator to simulate parent-iframe communication
252
+ // This makes the plugin think it's running in an iframe (not standalone mode)
253
+ // Convert RimoriInfo from CommunicationHandler format to MessageChannelSimulator format
254
+ this.messageChannelSimulator = new MessageChannelSimulator_1.MessageChannelSimulator({
255
+ page: this.page,
256
+ pluginId: this.pluginId,
257
+ queryParams: {},
258
+ rimoriInfo: {
259
+ ...this.rimoriInfo,
260
+ guild: {
261
+ id: this.rimoriInfo.guild.id,
262
+ longTermGoalOverride: 'longTermGoalOverride' in this.rimoriInfo.guild ? this.rimoriInfo.guild.longTermGoalOverride : '',
263
+ allowUserPluginSettings: this.rimoriInfo.guild.allowUserPluginSettings,
264
+ },
265
+ installedPlugins: this.rimoriInfo.installedPlugins.map((p) => ({
266
+ id: p.id,
267
+ title: p.info?.title || '',
268
+ description: p.info?.description || '',
269
+ logo: p.info?.logo || '',
270
+ url: p.pages?.external_hosted_url || '',
271
+ })),
272
+ mainPanelPlugin: this.rimoriInfo.mainPanelPlugin
273
+ ? {
274
+ id: this.rimoriInfo.mainPanelPlugin.id,
275
+ title: this.rimoriInfo.mainPanelPlugin.info?.title || '',
276
+ description: this.rimoriInfo.mainPanelPlugin.info?.description || '',
277
+ logo: this.rimoriInfo.mainPanelPlugin.info?.logo || '',
278
+ url: this.rimoriInfo.mainPanelPlugin.pages?.external_hosted_url || '',
279
+ }
280
+ : undefined,
281
+ sidePanelPlugin: this.rimoriInfo.sidePanelPlugin
282
+ ? {
283
+ id: this.rimoriInfo.sidePanelPlugin.id,
284
+ title: this.rimoriInfo.sidePanelPlugin.info?.title || '',
285
+ description: this.rimoriInfo.sidePanelPlugin.info?.description || '',
286
+ logo: this.rimoriInfo.sidePanelPlugin.info?.logo || '',
287
+ url: this.rimoriInfo.sidePanelPlugin.pages?.external_hosted_url || '',
288
+ }
289
+ : undefined,
290
+ },
291
+ });
292
+ // Initialize the simulator - this injects the necessary shims
293
+ // to intercept window.parent.postMessage calls and set up MessageChannel communication
294
+ await this.messageChannelSimulator.initialize();
295
+ }
296
+ /**
297
+ * Formats text as SSE (Server-Sent Events) response.
298
+ * Since Playwright's route.fulfill() requires complete body, we format as SSE without delays.
299
+ */
300
+ formatAsSSE(text) {
301
+ const chunks = [];
302
+ // Start event
303
+ chunks.push(`data: ${JSON.stringify({ type: 'start' })}\n\n`);
304
+ // Text start event
305
+ chunks.push(`data: ${JSON.stringify({ type: 'text-start', id: '1' })}\n\n`);
306
+ // Text delta events (one chunk per character for simplicity)
307
+ for (let i = 0; i < text.length; i++) {
308
+ chunks.push(`data: ${JSON.stringify({ type: 'text-delta', delta: text[i] })}\n\n`);
309
+ }
310
+ // Text end event
311
+ chunks.push(`data: ${JSON.stringify({ type: 'text-end', id: '1' })}\n\n`);
312
+ // Finish event
313
+ chunks.push(`data: ${JSON.stringify({ type: 'finish' })}\n\n`);
314
+ // Done marker
315
+ chunks.push('data: [DONE]\n\n');
316
+ return chunks.join('');
317
+ }
318
+ /**
319
+ * Normalizes a URL by removing query parameters and fragments for consistent matching.
320
+ */
321
+ normalizeUrl(url) {
322
+ try {
323
+ const urlObj = new URL(url);
324
+ return `${urlObj.origin}${urlObj.pathname}`;
325
+ }
326
+ catch {
327
+ return url;
328
+ }
329
+ }
330
+ /**
331
+ * Creates a route key combining HTTP method and normalized URL.
332
+ */
333
+ createRouteKey(method, url) {
334
+ const normalizedUrl = this.normalizeUrl(url);
335
+ return `${method} ${normalizedUrl}`;
336
+ }
337
+ async handleRoute(route, routes) {
338
+ console.warn('handleRoute is not tested');
339
+ const request = route.request();
340
+ const requestUrl = request.url();
341
+ const method = request.method().toUpperCase();
342
+ const routeKey = this.createRouteKey(method, requestUrl);
343
+ console.log('Handling route', routeKey);
344
+ const mocks = routes[routeKey];
345
+ if (!mocks || mocks.length === 0) {
346
+ console.error('No route handler found for route', routeKey);
347
+ route.abort('not_found');
348
+ return;
349
+ }
350
+ // Find the first matching mock based on matcher function
351
+ // Priority: mocks with matchers that match > mocks without matchers (as fallback)
352
+ let matchingMock;
353
+ let fallbackMock;
354
+ for (const mock of mocks) {
355
+ if (mock.options?.matcher) {
356
+ try {
357
+ if (mock.options.matcher(request)) {
358
+ matchingMock = mock;
359
+ break;
360
+ }
361
+ }
362
+ catch (error) {
363
+ console.error('Error in matcher function:', error);
364
+ }
365
+ }
366
+ else if (!fallbackMock) {
367
+ // Keep the first mock without a matcher as fallback
368
+ fallbackMock = mock;
369
+ }
370
+ }
371
+ // Use matching mock if found, otherwise use fallback
372
+ matchingMock = matchingMock ?? fallbackMock;
373
+ if (!matchingMock) {
374
+ console.error('No matching mock found for route', routeKey);
375
+ route.abort('not_found');
376
+ return;
377
+ }
378
+ // Handle the matched mock
379
+ const options = matchingMock.options;
380
+ await new Promise((resolve) => setTimeout(resolve, options?.delay ?? 0));
381
+ if (options?.error) {
382
+ return await route.abort(options.error);
383
+ }
384
+ // Handle streaming responses (for mockGetSteamedText)
385
+ // Since Playwright requires complete body, we format as SSE without delays
386
+ if (matchingMock.isStreaming && typeof matchingMock.value === 'string') {
387
+ const body = this.formatAsSSE(matchingMock.value);
388
+ return await route.fulfill({
389
+ status: 200,
390
+ headers: { 'Content-Type': 'text/event-stream' },
391
+ body,
392
+ });
393
+ }
394
+ // Regular JSON response
395
+ route.fulfill({
396
+ status: 200,
397
+ body: JSON.stringify(matchingMock.value),
398
+ });
399
+ }
400
+ /**
401
+ * Adds a supabase route to the supabase routes object.
402
+ * @param path - The path of the route.
403
+ * @param values - The values to return in the response.
404
+ * @param options - The options for the route. Method defaults to 'GET' if not specified.
405
+ */
406
+ addSupabaseRoute(path, values, options) {
407
+ console.warn('addSupabaseRoute is not tested');
408
+ const method = options?.method ?? 'GET';
409
+ const fullPath = `${this.rimoriInfo.url}/rest/v1/${path}`;
410
+ const routeKey = this.createRouteKey(method, fullPath);
411
+ if (!this.supabaseRoutes[routeKey]) {
412
+ this.supabaseRoutes[routeKey] = [];
413
+ }
414
+ this.supabaseRoutes[routeKey].push({
415
+ value: values,
416
+ method,
417
+ options,
418
+ });
419
+ }
420
+ /**
421
+ * Adds a backend route to the backend routes object.
422
+ * @param path - The path of the route.
423
+ * @param values - The values to return in the response.
424
+ * @param options - The options for the route. Method defaults to 'POST' if not specified.
425
+ * @param isStreaming - Optional flag to mark this as a streaming response.
426
+ */
427
+ addBackendRoute(path, values, options) {
428
+ console.warn('addBackendRoute is not tested');
429
+ const method = options?.method ?? 'POST';
430
+ const fullPath = `${this.rimoriInfo.backendUrl}${path.startsWith('/') ? path : '/' + path}`;
431
+ const routeKey = this.createRouteKey(method, fullPath);
432
+ if (!this.backendRoutes[routeKey]) {
433
+ this.backendRoutes[routeKey] = [];
434
+ }
435
+ const { isStreaming, ...mockOptions } = options || {};
436
+ this.backendRoutes[routeKey].push({
437
+ value: values,
438
+ method,
439
+ options: mockOptions,
440
+ isStreaming: isStreaming ?? false,
441
+ });
442
+ }
443
+ }
444
+ exports.RimoriTestEnvironment = RimoriTestEnvironment;
445
+ // Todo: How to test if the event was received by the parent?
446
+ // TODO: The matcher option of RimoriTestEnvironment v1 might be useful to use
@@ -0,0 +1,3 @@
1
+ import { UserInfo } from '@rimori/client/src/controller/SettingsController';
2
+ export declare const DEFAULT_USER_INFO: UserInfo;
3
+ export type DefaultUserInfo = typeof DEFAULT_USER_INFO;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_USER_INFO = void 0;
4
+ exports.DEFAULT_USER_INFO = {
5
+ mother_tongue: {
6
+ code: 'en',
7
+ name: 'English',
8
+ native: 'English',
9
+ capitalized: 'English',
10
+ uppercase: 'ENGLISH',
11
+ },
12
+ target_language: {
13
+ code: 'sv',
14
+ name: 'Swedish',
15
+ native: 'Svenska',
16
+ capitalized: 'Swedish',
17
+ uppercase: 'SWEDISH',
18
+ },
19
+ skill_level_reading: 'Pre-A1',
20
+ skill_level_writing: 'Pre-A1',
21
+ skill_level_grammar: 'Pre-A1',
22
+ skill_level_speaking: 'Pre-A1',
23
+ skill_level_listening: 'Pre-A1',
24
+ skill_level_understanding: 'Pre-A1',
25
+ goal_longterm: 'Learn Swedish',
26
+ goal_weekly: 'Practice daily',
27
+ study_buddy: {
28
+ id: 'buddy-1',
29
+ name: 'Test Buddy',
30
+ description: 'Test study buddy',
31
+ avatarUrl: '',
32
+ voiceId: 'alloy',
33
+ aiPersonality: 'friendly',
34
+ },
35
+ story_genre: 'fiction',
36
+ study_duration: 30,
37
+ motivation_type: 'career',
38
+ onboarding_completed: true,
39
+ context_menu_on_select: true,
40
+ user_name: 'Test User',
41
+ target_country: 'SE',
42
+ target_city: 'Stockholm',
43
+ };
@@ -0,0 +1 @@
1
+ export * from './core/RimoriTestEnvironment';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./core/RimoriTestEnvironment"), exports);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const test_1 = require("@playwright/test");
4
+ const RimoriTestEnvironment_1 = require("../core/RimoriTestEnvironment");
5
+ const pluginId = 'pl7720512027';
6
+ const pluginUrl = 'http://localhost:3009';
7
+ test_1.test.describe('Translator Plugin', () => {
8
+ let env;
9
+ test_1.test.beforeEach(async ({ page }) => {
10
+ env = new RimoriTestEnvironment_1.RimoriTestEnvironment({ page, pluginId });
11
+ env.ai.mockGetObject({
12
+ gramatically_corrected_input_text: 'tree',
13
+ detected_language: 'English',
14
+ text_type: 'noun',
15
+ word_unexisting_likelihood: 0,
16
+ translation_mother_tongue: 'tree',
17
+ translation_swedish: 'träd',
18
+ translation_noun_singular: 'tree',
19
+ plural: 'träd',
20
+ en_ett_word: 'ett',
21
+ alternative_meaning_mother_tongue: '',
22
+ }, {
23
+ matcher: (request) => {
24
+ const body = request.postDataJSON();
25
+ return body?.instructions?.includes('Look up the word or phrase') ?? false;
26
+ },
27
+ });
28
+ env.ai.mockGetObject({
29
+ example_sentence: {
30
+ target_language: 'Jag ser ett **träd** i skogen.',
31
+ english: 'I see a **tree** in the forest.',
32
+ mother_tongue: 'Ich sehe einen **Baum** im Wald.',
33
+ },
34
+ explanation: 'A tall perennial plant with a single woody stem, branches, and leaves.',
35
+ }, {
36
+ delay: 1000,
37
+ matcher: (request) => {
38
+ const body = request.postDataJSON();
39
+ return body?.instructions?.includes('Provide example sentence and explanation') ?? false;
40
+ },
41
+ });
42
+ page.on('console', (msg) => {
43
+ console.log(`[browser:${msg.type()}]`, msg.text());
44
+ });
45
+ await env.setup();
46
+ await page.goto(`${pluginUrl}/#/sidebar/translate`);
47
+ });
48
+ // test.afterEach(async () => {
49
+ // await env.teardown();
50
+ // });
51
+ (0, test_1.test)('translates with open page', async ({ page }) => {
52
+ //fill and submit the word to be translated
53
+ await page.getByRole('textbox', { name: 'snö, fog, Baum,....' }).fill('tree');
54
+ await page.getByRole('button', { name: 'Look up word' }).click();
55
+ //wait for basic translation to be completed
56
+ await (0, test_1.expect)(page.getByText('ett')).toBeVisible();
57
+ await (0, test_1.expect)(page.getByText('träd', { exact: true })).toBeVisible();
58
+ await (0, test_1.expect)(page.getByText('(träd)')).toBeVisible();
59
+ await (0, test_1.expect)(page.getByText('tree')).toBeVisible();
60
+ await (0, test_1.expect)(page.locator('.h-4').first()).toBeVisible();
61
+ await (0, test_1.expect)(page.locator('.h-4.bg-gray-700.rounded-md.animate-pulse.w-full')).toBeVisible();
62
+ //wait for example sentence to be completed
63
+ await (0, test_1.expect)(page.getByText('A tall perennial plant with a single woody stem, branches, and leaves.')).toBeVisible();
64
+ await (0, test_1.expect)(page.getByText('Jag ser ett träd i skogen.')).toBeVisible();
65
+ await (0, test_1.expect)(page.getByText('Ich sehe einen Baum im Wald.')).toBeVisible();
66
+ });
67
+ (0, test_1.test)('translates via side panel action event', async ({ page }) => {
68
+ // Set up the listener BEFORE navigating so it's ready when the plugin calls onSidePanelAction
69
+ await env.event.triggerOnSidePanelAction({
70
+ plugin_id: pluginId,
71
+ action_key: 'translate',
72
+ action: 'translate',
73
+ text: 'tree',
74
+ });
75
+ // Navigate to the page - the plugin will load and call onSidePanelAction, which will trigger our listener
76
+ await page.goto(`${pluginUrl}/#/sidebar/translate`);
77
+ await (0, test_1.expect)(page.getByText('ett')).toBeVisible();
78
+ await (0, test_1.expect)(page.getByText('träd', { exact: true })).toBeVisible();
79
+ await (0, test_1.expect)(page.getByText('A tall perennial plant with a single woody stem, branches, and leaves.')).toBeVisible();
80
+ await (0, test_1.expect)(page.getByText('Jag ser ett träd i skogen.')).toBeVisible();
81
+ });
82
+ (0, test_1.test)('translates and resets the translator', async ({ page }) => {
83
+ // Set up the listener BEFORE navigating so it's ready when the plugin calls onSidePanelAction
84
+ await env.event.triggerOnSidePanelAction({
85
+ plugin_id: pluginId,
86
+ action_key: 'translate',
87
+ action: 'translate',
88
+ text: 'tree',
89
+ });
90
+ // Navigate to the page - the plugin will load and call onSidePanelAction, which will trigger our listener
91
+ await page.goto(`${pluginUrl}/#/sidebar/translate`);
92
+ // wait for basic translation to be completed
93
+ await (0, test_1.expect)(page.getByText('ett')).toBeVisible();
94
+ await (0, test_1.expect)(page.getByText('träd', { exact: true })).toBeVisible();
95
+ // wait for full translation to be completed
96
+ await (0, test_1.expect)(page.getByText('Jag ser ett träd i skogen.')).toBeVisible();
97
+ // reset the translator
98
+ await page.getByRole('button', { name: 'New translation' }).click();
99
+ // wait for reset to be completed
100
+ await (0, test_1.expect)(page.getByText('Translate')).toBeVisible();
101
+ await (0, test_1.expect)(page.getByRole('textbox', { name: 'snö, fog, Baum,....' })).toBeVisible();
102
+ });
103
+ (0, test_1.test)('translates and ask question about the translation', async ({ page }) => {
104
+ // Mock streaming text response for the chat/question feature
105
+ await env.ai.mockGetSteamedText('This is a tree in Swedish: träd. It is an ett word.');
106
+ // Set up the listener BEFORE navigating so it's ready when the plugin calls onSidePanelAction
107
+ await env.event.triggerOnSidePanelAction({
108
+ plugin_id: pluginId,
109
+ action_key: 'translate',
110
+ action: 'translate',
111
+ text: 'tree',
112
+ });
113
+ // Navigate to the page - the plugin will load and call onSidePanelAction, which will trigger our listener
114
+ await page.goto(`${pluginUrl}/#/sidebar/translate`);
115
+ // wait for translation to be completed
116
+ await (0, test_1.expect)(page.getByText('ett')).toBeVisible();
117
+ await (0, test_1.expect)(page.getByText('träd', { exact: true })).toBeVisible();
118
+ await (0, test_1.expect)(page.getByText('Jag ser ett träd i skogen.')).toBeVisible();
119
+ // ask a question about the translation
120
+ await page.getByRole('textbox', { name: 'Ask questions...' }).click();
121
+ await page.getByRole('textbox', { name: 'Ask questions...' }).fill('What does that mean, explain in detail!');
122
+ await page.getByRole('textbox', { name: 'Ask questions...' }).press('Enter');
123
+ await (0, test_1.expect)(page.getByText('What does that mean, explain')).toBeVisible();
124
+ // validate that ai response is visible
125
+ await (0, test_1.expect)(page.getByText('This is a tree in Swedish: träd. It is an ett word.')).toBeVisible();
126
+ // reset the translator and check that the ai chat is cleared
127
+ await page.getByRole('button', { name: 'New translation' }).click();
128
+ await page.getByRole('textbox', { name: 'snö, fog, Baum,....' }).fill('tree');
129
+ await page.getByRole('button', { name: 'Look up word' }).click();
130
+ await (0, test_1.expect)(page.getByText('ett')).toBeVisible();
131
+ await page.getByText('Jag ser ett träd i skogen.').waitFor({ state: 'visible' });
132
+ await (0, test_1.expect)(page.getByText('This is a tree in Swedish: träd. It is an ett word.')).not.toBeVisible();
133
+ });
134
+ });