@qti-components/portable-custom-interaction 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+ import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';
2
+ /**
3
+ * Test-specific extension of QtiPortableCustomInteraction that adds methods
4
+ * for interacting with the iframe content in tests.
5
+ */
6
+ export class QtiPortableCustomInteractionTest extends QtiPortableCustomInteraction {
7
+ #recreatingIframe = false;
8
+ /**
9
+ * Gets the HTML content of the iframe for testing purposes
10
+ * @returns Promise that resolves with the HTML content string
11
+ */
12
+ async getIFrameContent() {
13
+ return new Promise(resolve => {
14
+ const messageId = `get-content-${Date.now()}`;
15
+ const messageHandler = (event) => {
16
+ const { data } = event;
17
+ if (data?.source === 'qti-pci-iframe' &&
18
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
19
+ data?.method === 'getContentResponse' &&
20
+ data?.messageId === messageId) {
21
+ window.removeEventListener('message', messageHandler);
22
+ resolve(data.content);
23
+ }
24
+ };
25
+ window.addEventListener('message', messageHandler);
26
+ this.sendMessageToIframe('getContent', { messageId });
27
+ // Set timeout to avoid hanging promises
28
+ setTimeout(() => {
29
+ window.removeEventListener('message', messageHandler);
30
+ resolve('');
31
+ }, 5000);
32
+ });
33
+ }
34
+ // Expose the protected context
35
+ setTestContext(newContext) {
36
+ this.context = newContext;
37
+ // If needed, force an update to push the context changes to the iframe
38
+ this.updateComplete.then(() => {
39
+ // const responseVal = this.responseVariablesToQtiVariableJSON(
40
+ // (newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.value as string | string[]) ||
41
+ // null,
42
+ // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.cardinality || 'single',
43
+ // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.baseType || 'string'
44
+ // );
45
+ this.value = newContext?.variables?.find(v => v.identifier === this.responseIdentifier).value;
46
+ // this.recreateIframe();
47
+ });
48
+ }
49
+ /**
50
+ * Recreates the iframe completely
51
+ * @returns Promise that resolves when the iframe is loaded
52
+ */
53
+ async recreateIframe() {
54
+ this.#recreatingIframe = true;
55
+ try {
56
+ // Remove existing iframe if it exists
57
+ if (this.iframe) {
58
+ this._iframeLoaded = false;
59
+ this.iframe.remove();
60
+ }
61
+ this._pendingMessages = [];
62
+ // Create a new iframe
63
+ this.createIframe();
64
+ // Wait for iframe to load
65
+ return new Promise(resolve => {
66
+ const loadHandler = () => {
67
+ this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);
68
+ resolve();
69
+ };
70
+ this.addEventListener('qti-portable-custom-interaction-loaded', loadHandler);
71
+ // Add timeout to avoid hanging if the event never fires
72
+ setTimeout(() => {
73
+ this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);
74
+ resolve();
75
+ }, 5000);
76
+ });
77
+ }
78
+ finally {
79
+ this.#recreatingIframe = false;
80
+ }
81
+ }
82
+ /**
83
+ * Override the original disconnectedCallback to handle iframe recreation
84
+ */
85
+ disconnectedCallback() {
86
+ // Only call super if we're not in the process of recreating the iframe
87
+ // This prevents removing event listeners that we still need
88
+ if (!this.#recreatingIframe) {
89
+ super.disconnectedCallback();
90
+ }
91
+ }
92
+ /**
93
+ * Simulates a mouse click at specific coordinates within the iframe
94
+ * @param x The x coordinate
95
+ * @param y The y coordinate
96
+ * @returns Promise that resolves when the click is performed
97
+ */
98
+ async iFrameMouseClick(x, y) {
99
+ return new Promise(resolve => {
100
+ const messageId = `click-${Date.now()}`;
101
+ const messageHandler = (event) => {
102
+ const { data } = event;
103
+ if (data?.source === 'qti-pci-iframe' &&
104
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
105
+ data?.method === 'clickResponse' &&
106
+ data?.messageId === messageId) {
107
+ window.removeEventListener('message', messageHandler);
108
+ resolve();
109
+ }
110
+ };
111
+ window.addEventListener('message', messageHandler);
112
+ this.sendMessageToIframe('simulateClick', { x, y, messageId });
113
+ // Set timeout to avoid hanging promises
114
+ setTimeout(() => {
115
+ window.removeEventListener('message', messageHandler);
116
+ resolve();
117
+ }, 5000);
118
+ });
119
+ }
120
+ /**
121
+ * Clicks on an element identified by a CSS selector
122
+ * @param selector The CSS selector string
123
+ * @returns Promise that resolves with boolean indicating if the element was found and clicked
124
+ */
125
+ async iFrameClickOnElement(selector) {
126
+ return new Promise(resolve => {
127
+ const messageId = `click-selector-${Date.now()}`;
128
+ const messageHandler = (event) => {
129
+ const { data } = event;
130
+ if (data?.source === 'qti-pci-iframe' &&
131
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
132
+ data?.method === 'clickSelectorResponse' &&
133
+ data?.messageId === messageId) {
134
+ window.removeEventListener('message', messageHandler);
135
+ resolve(data.success);
136
+ }
137
+ };
138
+ window.addEventListener('message', messageHandler);
139
+ this.sendMessageToIframe('clickOnSelector', { selector, messageId });
140
+ // Set timeout to avoid hanging promises
141
+ setTimeout(() => {
142
+ window.removeEventListener('message', messageHandler);
143
+ resolve(false);
144
+ }, 5000);
145
+ });
146
+ }
147
+ /**
148
+ * Clicks on an element containing the specified text
149
+ * @param text The text to search for within elements
150
+ * @returns Promise that resolves with boolean indicating if the element was found and clicked
151
+ */
152
+ async iFrameClickOnElementByText(text) {
153
+ return new Promise(resolve => {
154
+ const messageId = `click-text-${Date.now()}`;
155
+ const messageHandler = (event) => {
156
+ const { data } = event;
157
+ if (data?.source === 'qti-pci-iframe' &&
158
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
159
+ data?.method === 'clickTextResponse' &&
160
+ data?.messageId === messageId) {
161
+ window.removeEventListener('message', messageHandler);
162
+ resolve(data.success);
163
+ }
164
+ };
165
+ window.addEventListener('message', messageHandler);
166
+ this.sendMessageToIframe('clickOnElementByText', { text, messageId });
167
+ // Set timeout to avoid hanging promises
168
+ setTimeout(() => {
169
+ window.removeEventListener('message', messageHandler);
170
+ resolve(false);
171
+ }, 5000);
172
+ });
173
+ }
174
+ /**
175
+ * Sets the value of an input element identified by a CSS selector
176
+ * @param selector The CSS selector string
177
+ * @param value The value to set
178
+ * @returns Promise that resolves with boolean indicating if the element was found and value was set
179
+ */
180
+ async iFrameSetValueElement(selector, value) {
181
+ return new Promise(resolve => {
182
+ const messageId = `set-value-${Date.now()}`;
183
+ const messageHandler = (event) => {
184
+ const { data } = event;
185
+ if (data?.source === 'qti-pci-iframe' &&
186
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
187
+ data?.method === 'setValueResponse' &&
188
+ data?.messageId === messageId) {
189
+ window.removeEventListener('message', messageHandler);
190
+ resolve(data.success);
191
+ }
192
+ };
193
+ window.addEventListener('message', messageHandler);
194
+ this.sendMessageToIframe('setValueElement', { selector, value, messageId });
195
+ // Set timeout to avoid hanging promises
196
+ setTimeout(() => {
197
+ window.removeEventListener('message', messageHandler);
198
+ resolve(false);
199
+ }, 5000);
200
+ });
201
+ }
202
+ /**
203
+ * Gets the bounding client rect for an element inside the iframe
204
+ * @param selector The CSS selector string
205
+ * @returns Promise that resolves with the rect or null
206
+ */
207
+ async iFrameGetBoundingClientRect(selector) {
208
+ return new Promise(resolve => {
209
+ const messageId = `get-bounding-rect-${Date.now()}`;
210
+ const messageHandler = (event) => {
211
+ const { data } = event;
212
+ if (data?.source === 'qti-pci-iframe' &&
213
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
214
+ data?.method === 'getBoundingRectResponse' &&
215
+ data?.messageId === messageId) {
216
+ window.removeEventListener('message', messageHandler);
217
+ resolve(data.rect ?? null);
218
+ }
219
+ };
220
+ window.addEventListener('message', messageHandler);
221
+ this.sendMessageToIframe('getBoundingRect', { selector, messageId });
222
+ setTimeout(() => {
223
+ window.removeEventListener('message', messageHandler);
224
+ resolve(null);
225
+ }, 5000);
226
+ });
227
+ }
228
+ /**
229
+ * Sets the value of an input element containing the specified text
230
+ * @param text The text to search for within elements
231
+ * @param value The value to set
232
+ * @returns Promise that resolves with boolean indicating if the element was found and value was set
233
+ */
234
+ async iFrameSetValueElementByText(text, value) {
235
+ return new Promise(resolve => {
236
+ const messageId = `set-value-text-${Date.now()}`;
237
+ const messageHandler = (event) => {
238
+ const { data } = event;
239
+ if (data?.source === 'qti-pci-iframe' &&
240
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
241
+ data?.method === 'setValueByTextResponse' &&
242
+ data?.messageId === messageId) {
243
+ window.removeEventListener('message', messageHandler);
244
+ resolve(data.success);
245
+ }
246
+ };
247
+ window.addEventListener('message', messageHandler);
248
+ this.sendMessageToIframe('setValueElementByText', { text, value, messageId });
249
+ // Set timeout to avoid hanging promises
250
+ setTimeout(() => {
251
+ window.removeEventListener('message', messageHandler);
252
+ resolve(false);
253
+ }, 5000);
254
+ });
255
+ }
256
+ /**
257
+ * Performs a mousedown event on an element identified by a CSS selector
258
+ * @param selector The CSS selector string
259
+ * @returns Promise that resolves with boolean indicating if the element was found and mousedown was performed
260
+ */
261
+ async iFrameMousedownOnElement(selector) {
262
+ return new Promise(resolve => {
263
+ const messageId = `mousedown-selector-${Date.now()}`;
264
+ const messageHandler = (event) => {
265
+ const { data } = event;
266
+ if (data?.source === 'qti-pci-iframe' &&
267
+ (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
268
+ data?.method === 'mousedownSelectorResponse' &&
269
+ data?.messageId === messageId) {
270
+ window.removeEventListener('message', messageHandler);
271
+ resolve(data.success);
272
+ }
273
+ };
274
+ window.addEventListener('message', messageHandler);
275
+ this.sendMessageToIframe('mousedownOnSelector', { selector, messageId });
276
+ // Set timeout to avoid hanging promises
277
+ setTimeout(() => {
278
+ window.removeEventListener('message', messageHandler);
279
+ resolve(false);
280
+ }, 5000);
281
+ });
282
+ }
283
+ /**
284
+ * Override the original generateIframeContent method to include
285
+ * the test message handlers in the iframe HTML
286
+ */
287
+ generateIframeContent() {
288
+ // Get the original iframe HTML from the parent class
289
+ const originalContent = super.generateIframeContent();
290
+ // Insert our test message handlers before the closing </script> tag
291
+ const testHandlers = `
292
+ // Test helper functions for QtiPortableCustomInteractionTest
293
+ // Base iframe implementation already provides PCIManager.getContent
294
+
295
+ // Base iframe implementation already provides PCIManager.simulateClick
296
+
297
+ // Base iframe implementation already provides PCIManager.clickOnSelector
298
+
299
+ // Base iframe implementation already provides PCIManager.clickOnElementByText
300
+
301
+ // Base iframe implementation already provides PCIManager.setValueElement
302
+
303
+ function deepQuerySelector(root, selector) {
304
+ if (!root) return null;
305
+ try {
306
+ const direct = root.querySelector ? root.querySelector(selector) : null;
307
+ if (direct) return direct;
308
+ } catch (e) {
309
+ // ignore invalid selector for this root
310
+ }
311
+ if (!root.querySelectorAll) return null;
312
+ const nodes = root.querySelectorAll('*');
313
+ for (const node of nodes) {
314
+ if (node && node.shadowRoot) {
315
+ const found = deepQuerySelector(node.shadowRoot, selector);
316
+ if (found) return found;
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+
322
+ function deepFindElementByExactText(root, text) {
323
+ if (!root || !text) return null;
324
+ if (root.querySelectorAll) {
325
+ const nodes = root.querySelectorAll('*');
326
+ for (const node of nodes) {
327
+ if ((node.textContent || '').trim() === text) return node;
328
+ if (node.shadowRoot) {
329
+ const found = deepFindElementByExactText(node.shadowRoot, text);
330
+ if (found) return found;
331
+ }
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+
337
+ function escapeSelectorId(id) {
338
+ try {
339
+ if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(id);
340
+ } catch (e) {
341
+ // ignore
342
+ }
343
+ return String(id).replace(/([ #;?%&,.+*~\\':"!^$[\\]()=>|\\/])/g, '\\\\$1');
344
+ }
345
+
346
+ PCIManager.setValueElementByText = function(params) {
347
+ const messageId = params && params.messageId;
348
+ try {
349
+ const text = params && params.text;
350
+ const value = params && params.value;
351
+
352
+ const textEl = text ? deepFindElementByExactText(document, text) : null;
353
+ let target = null;
354
+
355
+ if (textEl) {
356
+ const tag = (textEl.tagName || '').toUpperCase();
357
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
358
+ target = textEl;
359
+ } else if (tag === 'LABEL' && textEl.htmlFor) {
360
+ const id = escapeSelectorId(textEl.htmlFor);
361
+ target = deepQuerySelector(document, '#' + id);
362
+ }
363
+
364
+ if (!target) {
365
+ let parent = textEl.parentElement;
366
+ while (parent) {
367
+ const candidate = parent.querySelector ? parent.querySelector('input, textarea, select') : null;
368
+ if (candidate) {
369
+ target = candidate;
370
+ break;
371
+ }
372
+ parent = parent.parentElement;
373
+ }
374
+ }
375
+ }
376
+
377
+ const success = !!target && setValueOnElement(target, value);
378
+ window.parent.postMessage(
379
+ {
380
+ source: 'qti-pci-iframe',
381
+ responseIdentifier: PCIManager.responseIdentifier,
382
+ method: 'setValueByTextResponse',
383
+ messageId: messageId,
384
+ success: success
385
+ },
386
+ '*'
387
+ );
388
+ } catch (error) {
389
+ console.error('Error setting value by text:', error);
390
+ window.parent.postMessage(
391
+ {
392
+ source: 'qti-pci-iframe',
393
+ responseIdentifier: PCIManager.responseIdentifier,
394
+ method: 'setValueByTextResponse',
395
+ messageId: messageId,
396
+ success: false,
397
+ error: error.toString()
398
+ },
399
+ '*'
400
+ );
401
+ }
402
+ };
403
+
404
+ PCIManager.mousedownOnSelector = function(params) {
405
+ try {
406
+ const element = params && params.selector ? deepQuerySelector(document, params.selector) : null;
407
+
408
+ let success = false;
409
+
410
+ if (element) {
411
+ const rect = element.getBoundingClientRect();
412
+ const mousedownEvent = new MouseEvent('mousedown', {
413
+ bubbles: true,
414
+ cancelable: true,
415
+ view: window,
416
+ clientX: rect.left + rect.width / 2,
417
+ clientY: rect.top + rect.height / 2
418
+ });
419
+ element.dispatchEvent(mousedownEvent);
420
+ success = true;
421
+ }
422
+
423
+ window.parent.postMessage({
424
+ source: 'qti-pci-iframe',
425
+ responseIdentifier: PCIManager.responseIdentifier,
426
+ method: 'mousedownSelectorResponse',
427
+ messageId: params.messageId,
428
+ success
429
+ }, '*');
430
+ } catch (error) {
431
+ console.error('Error performing mousedown on selector:', error);
432
+ window.parent.postMessage({
433
+ source: 'qti-pci-iframe',
434
+ responseIdentifier: PCIManager.responseIdentifier,
435
+ method: 'mousedownSelectorResponse',
436
+ messageId: params.messageId,
437
+ success: false,
438
+ error: error.toString()
439
+ }, '*');
440
+ }
441
+ };
442
+
443
+ // Helper function for setting values on elements
444
+ function setValueOnElement(element, value) {
445
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
446
+ element.value = value;
447
+ element.dispatchEvent(new Event('input', { bubbles: true }));
448
+ element.dispatchEvent(new Event('change', { bubbles: true }));
449
+ return true;
450
+ } else if (element.tagName === 'SELECT') {
451
+ element.value = value;
452
+ element.dispatchEvent(new Event('change', { bubbles: true }));
453
+ return true;
454
+ } else if (element.isContentEditable) {
455
+ element.textContent = value;
456
+ element.dispatchEvent(new Event('input', { bubbles: true }));
457
+ return true;
458
+ }
459
+ return false;
460
+ }
461
+
462
+ // Add test-related message handlers (do not override global handlers)
463
+ let expectedParentOriginTest = null;
464
+ window.addEventListener(
465
+ 'message',
466
+ function(event) {
467
+ const { data } = event;
468
+
469
+ if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') return;
470
+ if (expectedParentOriginTest === null) expectedParentOriginTest = event.origin;
471
+ else if (event.origin !== expectedParentOriginTest) return;
472
+ if (data.responseIdentifier && data.responseIdentifier !== PCIManager.responseIdentifier) return;
473
+
474
+ switch (data.method) {
475
+ case 'setValueElementByText':
476
+ PCIManager.setValueElementByText(data.params);
477
+ return;
478
+ case 'mousedownOnSelector':
479
+ PCIManager.mousedownOnSelector(data.params);
480
+ return;
481
+ }
482
+ },
483
+ true
484
+ );`;
485
+ // Find the position to insert our test handlers
486
+ const insertPosition = originalContent.lastIndexOf('</script>');
487
+ // Insert our handlers
488
+ const modifiedContent = originalContent.substring(0, insertPosition) + testHandlers + originalContent.substring(insertPosition);
489
+ return modifiedContent;
490
+ }
491
+ }
492
+ //# sourceMappingURL=qti-portable-custom-test-interaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qti-portable-custom-test-interaction.js","sourceRoot":"","sources":["../src/qti-portable-custom-test-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';\n\nimport type { ItemContext } from '@qti-components/base';\n\n/**\n * Test-specific extension of QtiPortableCustomInteraction that adds methods\n * for interacting with the iframe content in tests.\n */\nexport class QtiPortableCustomInteractionTest extends QtiPortableCustomInteraction {\n #recreatingIframe = false;\n\n /**\n * Gets the HTML content of the iframe for testing purposes\n * @returns Promise that resolves with the HTML content string\n */\n async getIFrameContent(): Promise<string> {\n return new Promise(resolve => {\n const messageId = `get-content-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'getContentResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.content);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('getContent', { messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve('');\n }, 5000);\n });\n }\n\n // Expose the protected context\n public setTestContext(newContext: ItemContext): void {\n this.context = newContext;\n\n // If needed, force an update to push the context changes to the iframe\n this.updateComplete.then(() => {\n // const responseVal = this.responseVariablesToQtiVariableJSON(\n // (newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.value as string | string[]) ||\n // null,\n // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.cardinality || 'single',\n // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.baseType || 'string'\n // );\n this.value = newContext?.variables?.find(v => v.identifier === this.responseIdentifier).value as string;\n // this.recreateIframe();\n });\n }\n\n /**\n * Recreates the iframe completely\n * @returns Promise that resolves when the iframe is loaded\n */\n public async recreateIframe(): Promise<void> {\n this.#recreatingIframe = true;\n\n try {\n // Remove existing iframe if it exists\n if (this.iframe) {\n this._iframeLoaded = false;\n this.iframe.remove();\n }\n\n this._pendingMessages = [];\n\n // Create a new iframe\n this.createIframe();\n\n // Wait for iframe to load\n return new Promise<void>(resolve => {\n const loadHandler = () => {\n this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n resolve();\n };\n\n this.addEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n\n // Add timeout to avoid hanging if the event never fires\n setTimeout(() => {\n this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n resolve();\n }, 5000);\n });\n } finally {\n this.#recreatingIframe = false;\n }\n }\n\n /**\n * Override the original disconnectedCallback to handle iframe recreation\n */\n override disconnectedCallback(): void {\n // Only call super if we're not in the process of recreating the iframe\n // This prevents removing event listeners that we still need\n if (!this.#recreatingIframe) {\n super.disconnectedCallback();\n }\n }\n\n /**\n * Simulates a mouse click at specific coordinates within the iframe\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Promise that resolves when the click is performed\n */\n async iFrameMouseClick(x: number, y: number): Promise<void> {\n return new Promise(resolve => {\n const messageId = `click-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve();\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('simulateClick', { x, y, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve();\n }, 5000);\n });\n }\n\n /**\n * Clicks on an element identified by a CSS selector\n * @param selector The CSS selector string\n * @returns Promise that resolves with boolean indicating if the element was found and clicked\n */\n async iFrameClickOnElement(selector: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `click-selector-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickSelectorResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('clickOnSelector', { selector, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Clicks on an element containing the specified text\n * @param text The text to search for within elements\n * @returns Promise that resolves with boolean indicating if the element was found and clicked\n */\n async iFrameClickOnElementByText(text: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `click-text-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickTextResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('clickOnElementByText', { text, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Sets the value of an input element identified by a CSS selector\n * @param selector The CSS selector string\n * @param value The value to set\n * @returns Promise that resolves with boolean indicating if the element was found and value was set\n */\n async iFrameSetValueElement(selector: string, value: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `set-value-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'setValueResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('setValueElement', { selector, value, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Gets the bounding client rect for an element inside the iframe\n * @param selector The CSS selector string\n * @returns Promise that resolves with the rect or null\n */\n async iFrameGetBoundingClientRect(\n selector: string\n ): Promise<{ left: number; top: number; width: number; height: number } | null> {\n return new Promise(resolve => {\n const messageId = `get-bounding-rect-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'getBoundingRectResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.rect ?? null);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('getBoundingRect', { selector, messageId });\n\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(null);\n }, 5000);\n });\n }\n\n /**\n * Sets the value of an input element containing the specified text\n * @param text The text to search for within elements\n * @param value The value to set\n * @returns Promise that resolves with boolean indicating if the element was found and value was set\n */\n async iFrameSetValueElementByText(text: string, value: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `set-value-text-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'setValueByTextResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('setValueElementByText', { text, value, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Performs a mousedown event on an element identified by a CSS selector\n * @param selector The CSS selector string\n * @returns Promise that resolves with boolean indicating if the element was found and mousedown was performed\n */\n async iFrameMousedownOnElement(selector: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `mousedown-selector-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'mousedownSelectorResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('mousedownOnSelector', { selector, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Override the original generateIframeContent method to include\n * the test message handlers in the iframe HTML\n */\n override generateIframeContent(): string {\n // Get the original iframe HTML from the parent class\n const originalContent = super.generateIframeContent();\n\n // Insert our test message handlers before the closing </script> tag\n const testHandlers = `\n // Test helper functions for QtiPortableCustomInteractionTest\n // Base iframe implementation already provides PCIManager.getContent\n \n // Base iframe implementation already provides PCIManager.simulateClick\n \n // Base iframe implementation already provides PCIManager.clickOnSelector\n \n // Base iframe implementation already provides PCIManager.clickOnElementByText\n \n // Base iframe implementation already provides PCIManager.setValueElement\n \n function deepQuerySelector(root, selector) {\n if (!root) return null;\n try {\n const direct = root.querySelector ? root.querySelector(selector) : null;\n if (direct) return direct;\n } catch (e) {\n // ignore invalid selector for this root\n }\n if (!root.querySelectorAll) return null;\n const nodes = root.querySelectorAll('*');\n for (const node of nodes) {\n if (node && node.shadowRoot) {\n const found = deepQuerySelector(node.shadowRoot, selector);\n if (found) return found;\n }\n }\n return null;\n }\n\n function deepFindElementByExactText(root, text) {\n if (!root || !text) return null;\n if (root.querySelectorAll) {\n const nodes = root.querySelectorAll('*');\n for (const node of nodes) {\n if ((node.textContent || '').trim() === text) return node;\n if (node.shadowRoot) {\n const found = deepFindElementByExactText(node.shadowRoot, text);\n if (found) return found;\n }\n }\n }\n return null;\n }\n\n function escapeSelectorId(id) {\n try {\n if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(id);\n } catch (e) {\n // ignore\n }\n return String(id).replace(/([ #;?%&,.+*~\\\\':\"!^$[\\\\]()=>|\\\\/])/g, '\\\\\\\\$1');\n }\n\n PCIManager.setValueElementByText = function(params) {\n const messageId = params && params.messageId;\n try {\n const text = params && params.text;\n const value = params && params.value;\n\n const textEl = text ? deepFindElementByExactText(document, text) : null;\n let target = null;\n\n if (textEl) {\n const tag = (textEl.tagName || '').toUpperCase();\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {\n target = textEl;\n } else if (tag === 'LABEL' && textEl.htmlFor) {\n const id = escapeSelectorId(textEl.htmlFor);\n target = deepQuerySelector(document, '#' + id);\n }\n\n if (!target) {\n let parent = textEl.parentElement;\n while (parent) {\n const candidate = parent.querySelector ? parent.querySelector('input, textarea, select') : null;\n if (candidate) {\n target = candidate;\n break;\n }\n parent = parent.parentElement;\n }\n }\n }\n\n const success = !!target && setValueOnElement(target, value);\n window.parent.postMessage(\n {\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'setValueByTextResponse',\n messageId: messageId,\n success: success\n },\n '*'\n );\n } catch (error) {\n console.error('Error setting value by text:', error);\n window.parent.postMessage(\n {\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'setValueByTextResponse',\n messageId: messageId,\n success: false,\n error: error.toString()\n },\n '*'\n );\n }\n };\n \n PCIManager.mousedownOnSelector = function(params) {\n try {\n const element = params && params.selector ? deepQuerySelector(document, params.selector) : null;\n \n let success = false;\n \n if (element) {\n const rect = element.getBoundingClientRect();\n const mousedownEvent = new MouseEvent('mousedown', {\n bubbles: true,\n cancelable: true,\n view: window,\n clientX: rect.left + rect.width / 2,\n clientY: rect.top + rect.height / 2\n });\n element.dispatchEvent(mousedownEvent);\n success = true;\n }\n \n window.parent.postMessage({\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'mousedownSelectorResponse',\n messageId: params.messageId,\n success\n }, '*');\n } catch (error) {\n console.error('Error performing mousedown on selector:', error);\n window.parent.postMessage({\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'mousedownSelectorResponse',\n messageId: params.messageId,\n success: false,\n error: error.toString()\n }, '*');\n }\n };\n \n // Helper function for setting values on elements\n function setValueOnElement(element, value) {\n if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {\n element.value = value;\n element.dispatchEvent(new Event('input', { bubbles: true }));\n element.dispatchEvent(new Event('change', { bubbles: true }));\n return true;\n } else if (element.tagName === 'SELECT') {\n element.value = value;\n element.dispatchEvent(new Event('change', { bubbles: true }));\n return true;\n } else if (element.isContentEditable) {\n element.textContent = value;\n element.dispatchEvent(new Event('input', { bubbles: true }));\n return true;\n }\n return false;\n }\n \n // Add test-related message handlers (do not override global handlers)\n let expectedParentOriginTest = null;\n window.addEventListener(\n 'message',\n function(event) {\n const { data } = event;\n\n if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') return;\n if (expectedParentOriginTest === null) expectedParentOriginTest = event.origin;\n else if (event.origin !== expectedParentOriginTest) return;\n if (data.responseIdentifier && data.responseIdentifier !== PCIManager.responseIdentifier) return;\n\n switch (data.method) {\n case 'setValueElementByText':\n PCIManager.setValueElementByText(data.params);\n return;\n case 'mousedownOnSelector':\n PCIManager.mousedownOnSelector(data.params);\n return;\n }\n },\n true\n );`;\n\n // Find the position to insert our test handlers\n const insertPosition = originalContent.lastIndexOf('</script>');\n\n // Insert our handlers\n const modifiedContent =\n originalContent.substring(0, insertPosition) + testHandlers + originalContent.substring(insertPosition);\n\n return modifiedContent;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-portable-custom-interaction-test': QtiPortableCustomInteractionTest;\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';
2
+ import { QtiPortableCustomInteractionTest } from './qti-portable-custom-test-interaction';
3
+ customElements.define('qti-portable-custom-interaction', QtiPortableCustomInteraction);
4
+ customElements.define('qti-portable-custom-interaction-test', QtiPortableCustomInteractionTest);
5
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAE1F,cAAc,CAAC,MAAM,CAAC,iCAAiC,EAAE,4BAA4B,CAAC,CAAC;AACvF,cAAc,CAAC,MAAM,CAAC,sCAAsC,EAAE,gCAAgC,CAAC,CAAC","sourcesContent":["import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';\nimport { QtiPortableCustomInteractionTest } from './qti-portable-custom-test-interaction';\n\ncustomElements.define('qti-portable-custom-interaction', QtiPortableCustomInteraction);\ncustomElements.define('qti-portable-custom-interaction-test', QtiPortableCustomInteractionTest);\n"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@qti-components/portable-custom-interaction",
3
+ "description": "QTI portable-custom-interaction interaction",
4
+ "version": "1.0.1",
5
+ "author": "",
6
+ "dependencies": {
7
+ "@qti-components/utilities": "^1.3.1",
8
+ "@qti-components/interactions-core": "^1.0.1",
9
+ "@qti-components/base": "^1.4.1"
10
+ },
11
+ "devDependencies": {
12
+ "@lit/context": "^1.1.6",
13
+ "lit": "^3.3.1"
14
+ },
15
+ "keywords": [
16
+ "interactions",
17
+ "qti",
18
+ "portable-custom-interaction"
19
+ ],
20
+ "license": "GPL-3.0-only",
21
+ "main": "dist/index.js",
22
+ "peerDependencies": {
23
+ "@lit/context": "^1.1.6",
24
+ "lit": "^3.3.1"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "sideEffects": true,
30
+ "type": "module",
31
+ "types": "dist/index.d.ts",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./register": {
41
+ "types": "./dist/register.d.ts",
42
+ "import": "./dist/register.js"
43
+ },
44
+ "./*": "./dist/*",
45
+ "./styles": {
46
+ "types": "./dist/qti-portable-custom-interaction.styles.d.ts",
47
+ "import": "./dist/qti-portable-custom-interaction.styles.js"
48
+ }
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/Citolab/qti-components.git",
53
+ "directory": "packages/interactions/portable-custom-interaction"
54
+ },
55
+ "scripts": {
56
+ "attw": "attw --profile esm-only --pack",
57
+ "build": "tsc --paths null",
58
+ "clean": "rm -rf dist",
59
+ "release": "semantic-release --no-ci",
60
+ "release:semantic": "semantic-release --no-ci",
61
+ "release:semantic:dry-run": "semantic-release --dry-run --no-ci"
62
+ }
63
+ }