Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/test/domRecorder.test.ts
1029 views
1
import Core, { Session } from '@secret-agent/core';
2
import { Helpers } from '@secret-agent/testing';
3
import { LocationStatus } from '@secret-agent/interfaces/Location';
4
import { InteractionCommand } from '@secret-agent/interfaces/IInteractions';
5
import { ITestKoaServer } from '@secret-agent/testing/helpers';
6
import { DomActionType } from '@secret-agent/interfaces/IDomChangeEvent';
7
import HumanEmulator from '@secret-agent/plugin-utils/lib/HumanEmulator';
8
import ConnectionToClient from '../server/ConnectionToClient';
9
import { MouseEventType } from '../models/MouseEventsTable';
10
11
let koaServer: ITestKoaServer;
12
let connectionToClient: ConnectionToClient;
13
beforeAll(async () => {
14
Core.use(
15
class BasicHumanEmulator extends HumanEmulator {
16
static id = 'basic';
17
},
18
);
19
connectionToClient = Core.addConnection();
20
Helpers.onClose(() => connectionToClient.disconnect(), true);
21
koaServer = await Helpers.runKoaServer();
22
});
23
afterAll(Helpers.afterAll);
24
afterEach(Helpers.afterEach);
25
26
describe('basic Page Recorder tests', () => {
27
it('detects added nodes', async () => {
28
koaServer.get('/test1', ctx => {
29
ctx.body = `<body>
30
<a href="#" onclick="addMe()">I am a test</a>
31
<script>
32
function addMe() {
33
const elem = document.createElement('A');
34
elem.setAttribute('id', 'link2');
35
elem.setAttribute('href', '/test2');
36
document.body.append(elem);
37
return false;
38
}
39
</script>
40
</body>`;
41
});
42
const meta = await connectionToClient.createSession({
43
humanEmulatorId: 'basic',
44
});
45
const tab = Session.getTab(meta);
46
await tab.goto(`${koaServer.baseUrl}/test1`);
47
await tab.waitForLoad('DomContentLoaded');
48
49
const changesAfterLoad = await tab.getMainFrameDomChanges();
50
51
const commandId = tab.lastCommandId;
52
expect(changesAfterLoad).toHaveLength(12);
53
expect(changesAfterLoad[0].textContent).toBe(`${koaServer.baseUrl}/test1`);
54
55
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
56
57
await tab.waitForElement(['document', ['querySelector', 'a#link2']]);
58
59
const changes = await tab.getMainFrameDomChanges(commandId);
60
expect(changes).toHaveLength(2);
61
expect(changes[0].action).toBe(DomActionType.added);
62
expect(changes[0].tagName).toBe('A');
63
});
64
65
it('detects removed nodes', async () => {
66
koaServer.get('/test2', ctx => {
67
ctx.body = `<body>
68
<a href="#" onclick="removeMe()">I am a test</a>
69
<script>
70
function removeMe() {
71
const elem = document.querySelector('a');
72
elem.parentNode.removeChild(elem);
73
return false;
74
}
75
</script>
76
</body>`;
77
});
78
const meta = await connectionToClient.createSession();
79
const tab = Session.getTab(meta);
80
Helpers.needsClosing.push(tab.session);
81
await tab.goto(`${koaServer.baseUrl}/test2`);
82
await tab.waitForLoad('DomContentLoaded');
83
84
const changesAfterLoad = await tab.getMainFrameDomChanges();
85
expect(changesAfterLoad).toHaveLength(12);
86
expect(changesAfterLoad[0].textContent).toBe(`${koaServer.baseUrl}/test2`);
87
const loadCommand = tab.lastCommandId;
88
89
await tab.waitForElement(['document', ['querySelector', 'a']]);
90
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
91
92
await tab.waitForMillis(100);
93
94
const changes = await tab.getMainFrameDomChanges(loadCommand);
95
expect(changes).toHaveLength(2);
96
expect(changes[0].action).toBe(DomActionType.removed);
97
98
await tab.session.close();
99
});
100
101
it('detects reordered nodes', async () => {
102
koaServer.get('/test3', ctx => {
103
ctx.body = `<body>
104
<ul>
105
<li id="id-1">1</li>
106
<li id="id-2">2</li>
107
<li id="id-3">3</li>
108
<li id="id-4">4</li>
109
<li id="id-5">5</li>
110
</ul>
111
<a href="#" onclick="sort()">I am a test</a>
112
<script>
113
function sort() {
114
const elem1 = document.querySelector('li#id-1');
115
const elem2 = document.querySelector('li#id-2');
116
const elem3 = document.querySelector('li#id-3');
117
const parent = document.querySelector('ul');
118
parent.append(elem2, elem1, elem3);
119
parent.append(elem1);
120
}
121
</script>
122
</body>`;
123
});
124
const meta = await connectionToClient.createSession();
125
const tab = Session.getTab(meta);
126
Helpers.needsClosing.push(tab.session);
127
await tab.goto(`${koaServer.baseUrl}/test3`);
128
await tab.waitForLoad('DomContentLoaded');
129
130
const changesAfterLoad = await tab.getMainFrameDomChanges();
131
expect(changesAfterLoad).toHaveLength(30);
132
expect(changesAfterLoad[0].textContent).toBe(`${koaServer.baseUrl}/test3`);
133
const loadCommand = tab.lastCommandId;
134
135
await tab.waitForElement(['document', ['querySelector', 'a']]);
136
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
137
138
await tab.waitForMillis(100);
139
140
const changes = await tab.getMainFrameDomChanges(loadCommand);
141
// 1 remove and 1 add for each
142
expect(changes).toHaveLength(9);
143
expect(changes.filter(x => x.action === DomActionType.removed)).toHaveLength(4);
144
145
const elem3 = changesAfterLoad.find(x => x.attributes?.id === 'id-3');
146
const elem1 = changesAfterLoad.find(x => x.attributes?.id === 'id-1');
147
148
const [lastChange, locationChange] = changes.slice(-2);
149
expect(locationChange.action).toBe(DomActionType.location);
150
expect(lastChange.action).toBe(DomActionType.added);
151
expect(lastChange.nodeId).toBe(elem1.nodeId);
152
expect(lastChange.previousSiblingId).toBe(elem3.nodeId);
153
154
await tab.session.close();
155
});
156
157
it('detects attribute changes', async () => {
158
koaServer.get('/test4', ctx => {
159
ctx.body = `<body>
160
<div id="divvy">This is my div</div>
161
<a href="#" onclick="sort()">I drive</a>
162
<script>
163
function sort() {
164
document.querySelector('#divvy').setAttribute('new-attr', "1");
165
}
166
</script>
167
</body>`;
168
});
169
const meta = await connectionToClient.createSession();
170
const tab = Session.getTab(meta);
171
Helpers.needsClosing.push(tab.session);
172
await tab.goto(`${koaServer.baseUrl}/test4`);
173
await tab.waitForLoad('DomContentLoaded');
174
175
const changesAfterLoad = await tab.getMainFrameDomChanges();
176
expect(changesAfterLoad).toHaveLength(15);
177
expect(changesAfterLoad[0].textContent).toBe(`${koaServer.baseUrl}/test4`);
178
const loadCommand = tab.lastCommandId;
179
180
await tab.waitForElement(['document', ['querySelector', 'a']]);
181
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
182
183
await tab.waitForMillis(100);
184
185
const changes = await tab.getMainFrameDomChanges(loadCommand);
186
expect(changes).toHaveLength(2);
187
expect(changes[0].action).toBe(DomActionType.attribute);
188
expect(changes[0].attributes['new-attr']).toBe('1');
189
190
await tab.session.close();
191
});
192
193
it('records frame dom ids', async () => {
194
koaServer.get('/iframe', ctx => {
195
ctx.body = `<body>
196
<div id="divvy">This is my div</div>
197
<iframe name="test1" srcdoc="<body>Hello world</body"></iframe>
198
<iframe name="srcTest" src="${koaServer.baseUrl}/iframe2"></iframe>
199
</body>`;
200
});
201
koaServer.get('/iframe2', ctx => {
202
ctx.body = `<body>
203
<h1>Not much to see</h1>
204
<iframe name="subframe" src="${koaServer.baseUrl}/iframe3"></iframe>
205
</body>`;
206
});
207
koaServer.get('/iframe3', ctx => {
208
ctx.body = `<body><h1>This is deep</h1></body>`;
209
});
210
211
const meta = await connectionToClient.createSession();
212
const tab = Session.getTab(meta);
213
Helpers.needsClosing.push(tab.session);
214
await tab.goto(`${koaServer.baseUrl}/iframe`);
215
await tab.waitForLoad(LocationStatus.AllContentLoaded);
216
const state = tab.sessionState;
217
218
expect(tab.puppetPage.frames).toHaveLength(4);
219
await tab.puppetPage.frames[1].waitForLifecycleEvent('load');
220
await tab.puppetPage.frames[2].waitForLifecycleEvent('load');
221
// await tab.puppetPage.frames[3].waitOn('frame-lifecycle', f => f.name === 'load');
222
223
await state.db.flush();
224
const domChanges = state.db.domChanges.all();
225
const domFrames = domChanges.filter(x => x.tagName === 'IFRAME');
226
expect(domFrames).toHaveLength(3);
227
228
await tab.frameEnvironmentsByPuppetId.get(tab.puppetPage.frames[3].id).isReady;
229
230
await state.db.flush();
231
const frames = state.db.frames.all();
232
expect(frames).toHaveLength(4);
233
const test1 = frames.find(x => x.name === 'test1');
234
expect(test1).toBeTruthy();
235
expect(test1.domNodeId).toBe(domFrames[0].nodeId);
236
237
const srcTest = frames.find(x => x.name === 'srcTest');
238
expect(srcTest).toBeTruthy();
239
expect(srcTest.domNodeId).toBe(domFrames[1].nodeId);
240
expect(srcTest.parentId).toBe(frames[0].id);
241
242
const subframe = frames.find(x => x.name === 'subframe');
243
expect(subframe).toBeTruthy();
244
expect(subframe.domNodeId).toBe(domFrames[2].nodeId);
245
expect(subframe.parentId).toBe(srcTest.id);
246
247
await tab.session.close();
248
});
249
250
it('supports recording CSSOM', async () => {
251
koaServer.get('/cssom', ctx => {
252
ctx.body = `<body>
253
<style id="style1" type="text/css"></style>
254
<a onclick="clickIt()">Click this link</a>
255
256
<script>
257
function clickIt() {
258
const style = document.querySelector('#style1');
259
style.sheet.insertRule('body { color:red }');
260
}
261
</script>
262
</body>`;
263
});
264
265
const meta = await connectionToClient.createSession();
266
const tab = Session.getTab(meta);
267
Helpers.needsClosing.push(tab.session);
268
await tab.goto(`${koaServer.baseUrl}/cssom`);
269
await tab.waitForLoad('DomContentLoaded');
270
271
await tab.waitForElement(['document', ['querySelector', 'a']]);
272
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
273
274
await tab.waitForMillis(100);
275
276
const changes = await tab.getMainFrameDomChanges();
277
278
const propChange = changes.find(
279
x => x.action === DomActionType.property && !!x.properties['sheet.cssRules'],
280
);
281
expect(propChange).toBeTruthy();
282
283
expect(propChange.properties['sheet.cssRules']).toStrictEqual(['body { color: red; }']);
284
await tab.session.close();
285
});
286
287
it('supports recording closed shadow dom roots', async () => {
288
koaServer.get('/test5', ctx => {
289
ctx.body = `<body>
290
<script>
291
customElements.define('image-list', class extends HTMLElement {
292
closedShadow;
293
constructor() {
294
super();
295
// test with a closed one since that's harder to intercept
296
this.closedShadow = this.attachShadow({mode: 'closed'});
297
}
298
299
addImage() {
300
const img = document.createElement('img');
301
const isClosed = !!this.shadowRoot && this.closedShadow.mode === 'closed';
302
img.alt = ' new image ' + (isClosed ? 'is closed ' : '');
303
this.closedShadow.appendChild(img)
304
}
305
})
306
</script>
307
<image-list id="LI"></image-list>
308
<a onclick="LI.addImage()">Add image</a>
309
310
<script>
311
</script>
312
</body>`;
313
});
314
const meta = await connectionToClient.createSession();
315
const tab = Session.getTab(meta);
316
Helpers.needsClosing.push(tab.session);
317
await tab.goto(`${koaServer.baseUrl}/test5`);
318
await tab.waitForLoad('DomContentLoaded');
319
320
await tab.waitForElement(['document', ['querySelector', 'a']]);
321
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
322
323
await tab.waitForMillis(100);
324
325
const changes = await tab.getMainFrameDomChanges();
326
expect(changes.find(x => x.action === DomActionType.added && x.nodeType === 40)).toBeTruthy();
327
328
const shadowRoot = changes.find(x => x.action === DomActionType.added && x.nodeType === 40);
329
expect(changes.find(x => x.nodeId === shadowRoot.parentNodeId).tagName).toBe('IMAGE-LIST');
330
const shadowImg = changes.find(x => x.parentNodeId === shadowRoot.nodeId);
331
332
expect(shadowImg.tagName).toBe('IMG');
333
expect(shadowImg.attributes.alt).toBe(' new image is closed ');
334
await tab.session.close();
335
});
336
});
337
338
describe('basic Mouse Event tests', () => {
339
it('detects mouse move and click', async () => {
340
koaServer.get('/mouse1', ctx => {
341
ctx.body = `<body>
342
<div id="divvy" style="height: 2200px">This is my div</div>
343
<ul>
344
<li>1</li>
345
<li>1</li>
346
<li>1</li>
347
<li>1</li>
348
<li>1</li>
349
<li>1</li>
350
<li>1</li>
351
<li>1</li>
352
<li>1</li>
353
</ul>
354
<span>Other one</span>
355
<button>I drive</button>
356
357
</body>`;
358
});
359
const meta = await connectionToClient.createSession({
360
humanEmulatorId: 'basic',
361
viewport: {
362
height: 800,
363
width: 1000,
364
positionY: 0,
365
positionX: 0,
366
screenHeight: 800,
367
screenWidth: 1000,
368
},
369
});
370
const tab = Session.getTab(meta);
371
Helpers.needsClosing.push(tab.session);
372
const state = tab.sessionState;
373
374
await tab.goto(`${koaServer.baseUrl}/mouse1`);
375
await tab.waitForLoad(LocationStatus.PaintingStable);
376
377
await tab.interact([
378
{ command: 'click', mousePosition: ['document', ['querySelector', 'BUTTON']] },
379
]);
380
381
await tab.getMainFrameDomChanges();
382
const mouseMoves = state.db.mouseEvents.all();
383
384
expect(mouseMoves.filter(x => x.event === MouseEventType.MOVE)).toHaveLength(1);
385
expect(mouseMoves.filter(x => x.event === MouseEventType.OVER).length).toBeGreaterThanOrEqual(
386
1,
387
);
388
expect(mouseMoves.filter(x => x.event === MouseEventType.UP)).toHaveLength(1);
389
390
const mouseDownEvents = mouseMoves.filter(x => x.event === MouseEventType.DOWN);
391
expect(mouseDownEvents).toHaveLength(1);
392
393
const domChanges = await tab.getMainFrameDomChanges();
394
const linkNode = domChanges.find(x => x.tagName === 'BUTTON');
395
396
expect(mouseDownEvents[0].targetNodeId).toBe(linkNode.nodeId);
397
398
const scrollRecords = state.db.scrollEvents.all();
399
expect(scrollRecords.length).toBeGreaterThanOrEqual(1);
400
401
await tab.session.close();
402
});
403
});
404
405
describe('basic Form element tests', () => {
406
it('detects typing in an input', async () => {
407
koaServer.get('/input', ctx => {
408
ctx.body = `<body>
409
<input type="text" value="" name="box">
410
411
<button onclick="clickedButton()">Button</button>
412
<script>
413
function clickedButton() {
414
document.querySelector('input').value = 'test';
415
}
416
</script>
417
</body>`;
418
});
419
const meta = await connectionToClient.createSession();
420
const tab = Session.getTab(meta);
421
await tab.goto(`${koaServer.baseUrl}/input`);
422
await tab.waitForLoad('PaintingStable');
423
await new Promise(resolve => setTimeout(resolve, 250));
424
425
const state = tab.sessionState;
426
427
await tab.interact([
428
{
429
command: InteractionCommand.click,
430
mousePosition: ['document', ['querySelector', 'input']],
431
},
432
{
433
command: InteractionCommand.type,
434
keyboardCommands: [{ string: 'Hello world!' }],
435
},
436
]);
437
438
const textValue = await tab.execJsPath(['document', ['querySelector', 'input'], 'value']);
439
expect(textValue.value).toBe('Hello world!');
440
441
await tab.interact([
442
{
443
command: InteractionCommand.click,
444
mousePosition: ['document', ['querySelector', 'button']],
445
},
446
]);
447
const textValue2 = await tab.execJsPath(['document', ['querySelector', 'input'], 'value']);
448
expect(textValue2.value).toBe('test');
449
450
await state.db.flush();
451
452
const changesAfterType = await tab.getMainFrameDomChanges();
453
454
// should have a change for each keypress + one for test
455
expect(changesAfterType.filter(x => x.action === DomActionType.property)).toHaveLength(
456
'Hello world!'.length + 1,
457
);
458
459
const focusRecords = state.db.focusEvents.all();
460
expect(focusRecords).toHaveLength(3);
461
const inputNode = changesAfterType.find(x => x.tagName === 'INPUT');
462
expect(focusRecords[0].targetNodeId).toBe(inputNode.nodeId);
463
464
await tab.session.close();
465
});
466
467
it('detects typing in a textarea', async () => {
468
koaServer.get('/textarea', ctx => {
469
ctx.body = `<body>
470
<textarea name="any"></textarea>
471
472
<button onclick="clickedButton()">Button</button>
473
<script>
474
function clickedButton() {
475
document.querySelector('textarea').value = 'test';
476
}
477
</script>
478
</body>`;
479
});
480
const meta = await connectionToClient.createSession();
481
const tab = Session.getTab(meta);
482
Helpers.needsClosing.push(tab.session);
483
await tab.goto(`${koaServer.baseUrl}/textarea`);
484
await tab.waitForLoad('PaintingStable');
485
await new Promise(resolve => setTimeout(resolve, 250));
486
487
const state = tab.sessionState;
488
489
await tab.interact([
490
{
491
command: InteractionCommand.click,
492
mousePosition: ['document', ['querySelector', 'textarea']],
493
},
494
{
495
command: InteractionCommand.type,
496
keyboardCommands: [{ string: 'Hello world!' }],
497
},
498
]);
499
500
const textValue = await tab.execJsPath(['document', ['querySelector', 'textarea'], 'value']);
501
expect(textValue.value).toBe('Hello world!');
502
503
await tab.interact([
504
{
505
command: InteractionCommand.click,
506
mousePosition: ['document', ['querySelector', 'button']],
507
},
508
]);
509
const textValue2 = await tab.execJsPath(['document', ['querySelector', 'textarea'], 'value']);
510
expect(textValue2.value).toBe('test');
511
512
await state.db.flush();
513
514
const changesAfterType = await tab.getMainFrameDomChanges();
515
516
// should have a change for each keypress + one for test
517
expect(changesAfterType.filter(x => x.action === DomActionType.property)).toHaveLength(
518
'Hello world!'.length + 1,
519
);
520
521
const focusRecords = state.db.focusEvents.all();
522
expect(focusRecords).toHaveLength(3);
523
const inputNode = changesAfterType.find(x => x.tagName === 'TEXTAREA');
524
expect(focusRecords[0].targetNodeId).toBe(inputNode.nodeId);
525
526
await tab.session.close();
527
});
528
529
it('detects checking a checkbox', async () => {
530
koaServer.get('/cbox', ctx => {
531
ctx.body = `<body>
532
533
<input type="checkbox" id="cbox1" name="field">Box 1</input>
534
<input type="checkbox" id="cbox2" name="field">Box 2</input>
535
536
<button onclick="clickedButton()">Button</button>
537
<script>
538
function clickedButton() {
539
document.querySelector('#cbox2').checked = true;
540
}
541
</script>
542
</body>`;
543
});
544
const meta = await connectionToClient.createSession();
545
const tab = Session.getTab(meta);
546
Helpers.needsClosing.push(tab.session);
547
await tab.goto(`${koaServer.baseUrl}/cbox`);
548
await tab.waitForLoad('PaintingStable');
549
await new Promise(resolve => setTimeout(resolve, 250));
550
551
const state = tab.sessionState;
552
553
await tab.interact([
554
{
555
command: InteractionCommand.click,
556
mousePosition: ['document', ['querySelector', '#cbox1']],
557
},
558
]);
559
560
await tab.interact([
561
{
562
command: InteractionCommand.click,
563
mousePosition: ['document', ['querySelector', 'button']],
564
},
565
]);
566
const values = await tab.execJsPath(['document', ['querySelectorAll', ':checked']]);
567
expect(Object.keys(values.value)).toHaveLength(2);
568
569
await state.db.flush();
570
571
const changesAfterType = await tab.getMainFrameDomChanges();
572
573
// should have a change for each keypress + one for test
574
expect(changesAfterType.filter(x => x.action === DomActionType.property)).toHaveLength(2);
575
576
const focusRecords = state.db.focusEvents.all();
577
expect(focusRecords).toHaveLength(3);
578
const inputNode = changesAfterType.find(x => x.tagName === 'INPUT');
579
expect(focusRecords[0].targetNodeId).toBe(inputNode.nodeId);
580
581
await tab.session.close();
582
});
583
584
it('detects changing a radio', async () => {
585
koaServer.get('/radio', ctx => {
586
ctx.body = `<body>
587
588
<input type="radio" checked="true" id="radio1" name="radiogroup">Radio 1</input>
589
<input type="radio" id="radio2" name="radiogroup">Radio 2</input>
590
591
<button onclick="clickedButton()">Button</button>
592
<script>
593
function clickedButton() {
594
document.querySelector('#radio1').checked = true;
595
}
596
</script>
597
</body>`;
598
});
599
const meta = await connectionToClient.createSession();
600
const tab = Session.getTab(meta);
601
Helpers.needsClosing.push(tab.session);
602
await tab.goto(`${koaServer.baseUrl}/radio`);
603
await tab.waitForLoad('PaintingStable');
604
await new Promise(resolve => setTimeout(resolve, 250));
605
606
const state = tab.sessionState;
607
608
await tab.interact([
609
{
610
command: InteractionCommand.click,
611
mousePosition: ['document', ['querySelector', '#radio2']],
612
},
613
]);
614
615
await tab.interact([
616
{
617
command: InteractionCommand.click,
618
mousePosition: ['document', ['querySelector', 'button']],
619
},
620
]);
621
const values = await tab.execJsPath(['document', ['querySelectorAll', ':checked']]);
622
expect(Object.keys(values.value)).toHaveLength(1);
623
624
await state.db.flush();
625
626
const changesAfterType = await tab.getMainFrameDomChanges();
627
628
// 2 changes per radio change
629
expect(changesAfterType.filter(x => x.action === DomActionType.property)).toHaveLength(4);
630
expect(
631
changesAfterType.filter(
632
x => x.action === DomActionType.property && x.properties.checked === true,
633
),
634
).toHaveLength(2);
635
636
const focusRecords = state.db.focusEvents.all();
637
expect(focusRecords).toHaveLength(3);
638
const inputNode = changesAfterType.find(x => x.attributes?.id === 'radio2');
639
expect(focusRecords[0].targetNodeId).toBe(inputNode.nodeId);
640
await tab.session.close();
641
});
642
643
it('detects changing a select', async () => {
644
koaServer.get('/select', ctx => {
645
ctx.body = `<body>
646
<select name="test">
647
<option id="opt1" value="1" selected>Option 1</option>
648
<option id="opt2" value="2">Option 2</option>
649
</select>
650
651
<button onclick="clickedButton()">Button</button>
652
<script>
653
function clickedButton() {
654
document.querySelector('#opt1').selected = true;
655
}
656
</script>
657
</body>`;
658
});
659
const meta = await connectionToClient.createSession();
660
const tab = Session.getTab(meta);
661
Helpers.needsClosing.push(tab.session);
662
await tab.goto(`${koaServer.baseUrl}/select`);
663
await tab.waitForLoad('PaintingStable');
664
await new Promise(resolve => setTimeout(resolve, 250));
665
666
const state = tab.sessionState;
667
668
await tab.interact([
669
{
670
command: InteractionCommand.click,
671
mousePosition: ['document', ['querySelector', 'select']],
672
},
673
{
674
command: InteractionCommand.click,
675
mousePosition: ['document', ['querySelector', '#opt2']],
676
},
677
]);
678
679
await tab.interact([
680
{
681
command: InteractionCommand.click,
682
mousePosition: ['document', ['querySelector', 'button']],
683
},
684
]);
685
const values = await tab.execJsPath(['document', ['querySelectorAll', ':checked']]);
686
expect(Object.keys(values.value)).toHaveLength(1);
687
688
await state.db.flush();
689
690
const changesAfterType = await tab.getMainFrameDomChanges();
691
692
const focusRecords = state.db.focusEvents.all();
693
694
expect(focusRecords).toHaveLength(3);
695
const select = changesAfterType.find(x => x.tagName === 'SELECT');
696
const opt2 = changesAfterType.find(x => x.attributes?.id === 'opt2');
697
expect(focusRecords[0].targetNodeId).toBe(select.nodeId);
698
699
// 2 sets of: 1 change for each option, 1 for select
700
expect(changesAfterType.filter(x => x.action === DomActionType.property)).toHaveLength(6);
701
expect(
702
changesAfterType.filter(
703
x => x.action === DomActionType.property && x.nodeId === select.nodeId,
704
),
705
).toHaveLength(2);
706
expect(
707
changesAfterType.filter(x => x.action === DomActionType.property && x.nodeId === opt2.nodeId),
708
).toHaveLength(2);
709
await tab.session.close();
710
});
711
});
712
713
describe('Multi-tab recording', () => {
714
it('should record dom separately for two pages', async () => {
715
koaServer.get('/page1', ctx => {
716
ctx.body = `<body>
717
<a href="/page2" target="_blank" onclick="addMe()">I am on page1</a>
718
<script>
719
function addMe() {
720
const elem = document.createElement('A');
721
elem.setAttribute('id', 'link2');
722
elem.setAttribute('href', '/test2');
723
document.body.append(elem);
724
return true;
725
}
726
</script>
727
</body>
728
`;
729
});
730
koaServer.get('/page2', ctx => {
731
ctx.body = `<body>
732
<a href="#" onclick="addMe2()">I am on page2</a>
733
<script>
734
function addMe2() {
735
const elem = document.createElement('div');
736
elem.setAttribute('id', 'divPage2');
737
document.body.append(elem);
738
return true;
739
}
740
</script>
741
</body>
742
`;
743
});
744
const meta = await connectionToClient.createSession();
745
const tab = Session.getTab(meta);
746
Helpers.needsClosing.push(tab.session);
747
await tab.goto(`${koaServer.baseUrl}/page1`);
748
await tab.waitForLoad('DomContentLoaded');
749
750
await tab.interact([{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] }]);
751
752
const tab2 = await tab.waitForNewTab();
753
await tab2.waitForLoad('PaintingStable');
754
await tab2.interact([
755
{ command: 'click', mousePosition: ['document', ['querySelector', 'a']] },
756
]);
757
758
const tab1Changes = await tab.getMainFrameDomChanges();
759
const tab2Changes = await tab2.getMainFrameDomChanges();
760
761
const frame1DomRecords = tab1Changes.map(x => ({
762
action: x.action,
763
textContent: x.textContent,
764
tagName: x.tagName,
765
attributes: x.attributes,
766
}));
767
768
const frame2DomRecords = tab2Changes.map(x => ({
769
action: x.action,
770
textContent: x.textContent,
771
tagName: x.tagName,
772
attributes: x.attributes,
773
}));
774
775
expect(frame1DomRecords[0]).toStrictEqual({
776
action: DomActionType.newDocument,
777
tagName: undefined,
778
attributes: undefined,
779
textContent: `${koaServer.baseUrl}/page1`,
780
});
781
expect(frame1DomRecords.filter(x => x.tagName)).toHaveLength(6);
782
expect(frame1DomRecords[frame1DomRecords.length - 1]).toStrictEqual({
783
action: DomActionType.added,
784
tagName: 'A',
785
attributes: { id: 'link2', href: '/test2' },
786
textContent: undefined,
787
});
788
789
expect(frame2DomRecords[0]).toStrictEqual({
790
action: DomActionType.newDocument,
791
tagName: undefined,
792
attributes: undefined,
793
textContent: `${koaServer.baseUrl}/page2`,
794
});
795
expect(frame2DomRecords.filter(x => x.tagName)).toHaveLength(6);
796
expect(frame2DomRecords[frame2DomRecords.length - 2]).toStrictEqual({
797
action: DomActionType.added,
798
tagName: 'DIV',
799
attributes: { id: 'divPage2' },
800
textContent: undefined,
801
});
802
await tab.session.close();
803
});
804
});
805
806