Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/puppet/test/Frames.test.ts
1030 views
1
import BrowserEmulator from '@secret-agent/default-browser-emulator';
2
import Log from '@secret-agent/commons/Logger';
3
import { IPuppetPage } from '@secret-agent/interfaces/IPuppetPage';
4
import IPuppetContext from '@secret-agent/interfaces/IPuppetContext';
5
import Core from '@secret-agent/core';
6
import CorePlugins from '@secret-agent/core/lib/CorePlugins';
7
import { IBoundLog } from '@secret-agent/interfaces/ILog';
8
import { TestServer } from './server';
9
import Puppet from '../index';
10
import { capturePuppetContextLogs, createTestPage, ITestPage } from './TestPage';
11
import CustomBrowserEmulator from './_CustomBrowserEmulator';
12
13
const { log } = Log(module);
14
const browserEmulatorId = CustomBrowserEmulator.id;
15
16
describe('Frames', () => {
17
let server: TestServer;
18
let page: ITestPage;
19
let puppet: Puppet;
20
let context: IPuppetContext;
21
22
beforeAll(async () => {
23
Core.use(CustomBrowserEmulator);
24
const { browserEngine } = CustomBrowserEmulator.selectBrowserMeta();
25
const plugins = new CorePlugins({ browserEmulatorId }, log as IBoundLog);
26
server = await TestServer.create(0);
27
puppet = new Puppet(browserEngine);
28
await puppet.start();
29
context = await puppet.newContext(plugins, log);
30
capturePuppetContextLogs(context, `${browserEngine.fullVersion}-Frames-test`);
31
});
32
33
afterEach(async () => {
34
await page.close().catch(() => null);
35
server.reset();
36
});
37
38
beforeEach(async () => {
39
page = createTestPage(await context.newPage());
40
});
41
42
afterAll(async () => {
43
await server.stop();
44
await context.close().catch(() => null);
45
await puppet.close();
46
});
47
48
function getContexts(puppetPage: IPuppetPage) {
49
const { browserEngine } = BrowserEmulator.selectBrowserMeta();
50
if (browserEngine.name === 'chrome') {
51
const rawPage = puppetPage;
52
// @ts-ignore
53
return rawPage.framesManager.activeContextIds.size;
54
}
55
return null;
56
}
57
58
describe('basic', () => {
59
it('should have different execution contexts', async () => {
60
await page.goto(server.emptyPage);
61
await page.attachFrame('frame1', server.emptyPage);
62
expect(page.frames.length).toBe(2);
63
await page.frames[0].evaluate(`(window.FOO = 'foo')`);
64
await page.frames[1].evaluate(`(window.FOO = 'bar')`);
65
expect(await page.frames[0].evaluate('window.FOO')).toBe('foo');
66
expect(await page.frames[1].evaluate('window.FOO')).toBe('bar');
67
});
68
69
it('should have correct execution contexts', async () => {
70
await page.goto(`${server.baseUrl}/frames/one-frame.html`);
71
expect(page.frames.length).toBe(2);
72
expect(await page.frames[0].evaluate('document.body.textContent.trim()')).toBe('');
73
expect(await page.frames[1].evaluate('document.body.textContent.trim()')).toBe(
74
`Hi, I'm frame`,
75
);
76
});
77
78
it('should dispose context on navigation', async () => {
79
await page.goto(`${server.baseUrl}/frames/one-frame.html`);
80
expect(page.frames.length).toBe(2);
81
expect(getContexts(page)).toBe(4);
82
await page.goto(server.emptyPage);
83
// isolated context might or might not be loaded
84
expect(getContexts(page)).toBeLessThanOrEqual(2);
85
});
86
87
it('should dispose context on cross-origin navigation', async () => {
88
await page.goto(`${server.baseUrl}/frames/one-frame.html`);
89
expect(page.frames.length).toBe(2);
90
expect(getContexts(page)).toBe(4);
91
await page.goto(`${server.crossProcessBaseUrl}/empty.html`);
92
// isolated context might or might not be loaded
93
expect(getContexts(page)).toBeLessThanOrEqual(2);
94
});
95
96
it('should execute after cross-site navigation', async () => {
97
await page.goto(server.emptyPage);
98
const mainFrame = page.mainFrame;
99
expect(await mainFrame.evaluate('window.location.href')).toContain('localhost');
100
await page.goto(`${server.crossProcessBaseUrl}/empty.html`);
101
expect(await mainFrame.evaluate('window.location.href')).toContain('127');
102
});
103
104
it('should be isolated between frames', async () => {
105
await page.goto(server.emptyPage);
106
await page.attachFrame('frame1', server.emptyPage);
107
expect(page.frames.length).toBe(2);
108
const [frame1, frame2] = page.frames;
109
expect(frame1 !== frame2).toBeTruthy();
110
111
await Promise.all([frame1.evaluate('(window.a = 1)'), frame2.evaluate('(window.a = 2)')]);
112
const [a1, a2] = await Promise.all([
113
frame1.evaluate('window.a'),
114
frame2.evaluate('window.a'),
115
]);
116
expect(a1).toBe(1);
117
expect(a2).toBe(2);
118
});
119
120
it('should work in iframes that failed initial navigation', async () => {
121
// - Firefox does not report domcontentloaded for the iframe.
122
// - Chromium and Firefox report empty url.
123
// - Chromium does not report main/utility worlds for the iframe.
124
125
await page.setContent(
126
`<meta http-equiv="Content-Security-Policy" content="script-src 'none';">
127
<iframe src='javascript:""'></iframe>`,
128
);
129
// Note: Chromium/Firefox never report 'load' event for the iframe.
130
await page.evaluate(`(() => {
131
const iframe = document.querySelector('iframe');
132
const div = iframe.contentDocument.createElement('div');
133
iframe.contentDocument.body.appendChild(div);
134
})()`);
135
expect(page.frames[1].url).toBe(undefined);
136
// Main world should work.
137
expect(await page.frames[1].evaluate('window.location.href')).toBe('about:blank');
138
});
139
140
it('should work in iframes that interrupted initial javascript url navigation', async () => {
141
// Chromium does not report isolated world for the iframe.
142
await page.goto(server.emptyPage);
143
await page.evaluate(`(() => {
144
const iframe = document.createElement('iframe');
145
iframe.src = 'javascript:""';
146
document.body.appendChild(iframe);
147
iframe.contentDocument.open();
148
iframe.contentDocument.write('<div>hello</div>');
149
iframe.contentDocument.close();
150
})()`);
151
expect(await page.frames[1].evaluate('window.top.location.href')).toBe(server.emptyPage);
152
});
153
});
154
155
describe('hierarchy', () => {
156
it('should handle nested frames', async () => {
157
await page.goto(`${server.baseUrl}/frames/nested-frames.html`);
158
expect(page.frames).toHaveLength(5);
159
const mainFrame = page.mainFrame;
160
expect(mainFrame.url).toMatch('nested-frames.html');
161
162
const secondChildren = page.frames.filter(x => x.parentId === mainFrame.id);
163
expect(secondChildren).toHaveLength(2);
164
expect(secondChildren.map(x => x.url).sort()).toStrictEqual([
165
`${server.baseUrl}/frames/frame.html`,
166
`${server.baseUrl}/frames/two-frames.html`,
167
]);
168
169
const secondParent = secondChildren.find(x => x.url.includes('two-frames'));
170
171
const thirdTier = page.frames.filter(x => x.parentId === secondParent.id);
172
expect(thirdTier).toHaveLength(2);
173
await thirdTier[0].waitForLoader();
174
expect(thirdTier[0].url).toMatch('frame.html');
175
await thirdTier[1].waitForLoader();
176
expect(thirdTier[1].url).toMatch('frame.html');
177
});
178
179
it('should send events when frames are manipulated dynamically', async () => {
180
await page.goto(server.emptyPage);
181
182
// validate framenavigated events
183
const navigatedFrames = [];
184
page.on('frame-created', ({ frame }) => {
185
frame.on('frame-navigated', () => {
186
navigatedFrames.push({ frame });
187
});
188
});
189
await page.attachFrame('frame1', './assets/frame.html');
190
expect(page.frames.length).toBe(2);
191
expect(page.frames[1].url).toContain('/assets/frame.html');
192
193
await page.evaluate(`(async () => {
194
const frame = document.getElementById('frame1');
195
frame.src = './empty.html';
196
await new Promise(x => (frame.onload = x));
197
})()`);
198
expect(navigatedFrames.length).toBe(2);
199
expect(navigatedFrames[1].frame.url).toBe(server.emptyPage);
200
201
// validate framedetached events
202
await page.detachFrame('frame1');
203
expect(page.frames.length).toBe(1);
204
});
205
206
it('should send "frameNavigated" when navigating on anchor URLs', async () => {
207
await page.goto(server.emptyPage);
208
const frameNavigated = page.mainFrame.waitOn('frame-navigated');
209
await page.goto(`${server.emptyPage}#foo`);
210
expect(page.mainFrame.url).toBe(`${server.emptyPage}#foo`);
211
await expect(frameNavigated).resolves.toBeTruthy();
212
});
213
214
it('should persist mainFrame on cross-process navigation', async () => {
215
await page.goto(server.emptyPage);
216
const mainFrame = page.mainFrame;
217
await page.goto(`${server.crossProcessBaseUrl}/empty.html`);
218
expect(page.mainFrame === mainFrame).toBeTruthy();
219
});
220
221
it('should detach child frames on navigation', async () => {
222
let navigatedFrames = [];
223
page.mainFrame.on('frame-navigated', ev => navigatedFrames.push(ev));
224
page.on('frame-created', ({ frame }) => {
225
frame.on('frame-navigated', () => {
226
navigatedFrames.push(frame);
227
});
228
});
229
await page.goto(`${server.baseUrl}/frames/nested-frames.html`);
230
expect(page.frames.length).toBe(5);
231
for (const frame of page.frames) await frame.waitForLoader();
232
expect(navigatedFrames.length).toBe(5);
233
234
navigatedFrames = [];
235
await page.goto(server.emptyPage);
236
expect(page.frames.length).toBe(1);
237
expect(navigatedFrames.length).toBe(1);
238
});
239
240
it('should support framesets', async () => {
241
let navigatedFrames = [];
242
page.mainFrame.on('frame-navigated', ev => navigatedFrames.push(ev));
243
page.on('frame-created', ({ frame }) => {
244
frame.on('frame-navigated', () => {
245
navigatedFrames.push(frame);
246
});
247
});
248
await page.goto(`${server.baseUrl}/frames/frameset.html`);
249
expect(page.frames.length).toBe(5);
250
for (const frame of page.frames) await frame.waitForLoader();
251
expect(navigatedFrames.length).toBe(5);
252
253
navigatedFrames = [];
254
await page.goto(server.emptyPage);
255
expect(page.frames.length).toBe(1);
256
expect(navigatedFrames.length).toBe(1);
257
});
258
259
it('should report frame from-inside shadow DOM', async () => {
260
await page.goto(`${server.baseUrl}/shadow.html`);
261
await page.evaluate(`(async (url) => {
262
const frame = document.createElement('iframe');
263
frame.src = url;
264
document.body.shadowRoot.appendChild(frame);
265
await new Promise(x => (frame.onload = x));
266
})('${server.emptyPage}')`);
267
expect(page.frames.length).toBe(2);
268
expect(page.frames[1].url).toBe(server.emptyPage);
269
});
270
271
it('should report frame.name', async () => {
272
await page.attachFrame('theFrameId', server.emptyPage);
273
await page.evaluate(`((url) => {
274
const frame = document.createElement('iframe');
275
frame.name = 'theFrameName';
276
frame.src = url;
277
document.body.appendChild(frame);
278
return new Promise(x => (frame.onload = x));
279
})('${server.emptyPage}')`);
280
expect(page.frames[0].name).toBe('');
281
expect(page.frames[1].name).toBe('theFrameId');
282
expect(page.frames[2].name).toBe('theFrameName');
283
});
284
285
it('should report frame.parentId', async () => {
286
await page.attachFrame('frame1', server.emptyPage);
287
await page.attachFrame('frame2', server.emptyPage);
288
expect(page.frames[0].parentId).not.toBeTruthy();
289
expect(page.frames[1].parentId).toBe(page.mainFrame.id);
290
expect(page.frames[2].parentId).toBe(page.mainFrame.id);
291
});
292
293
it('should report different frame instance when frame re-attaches', async () => {
294
const frame1 = await page.attachFrame('frame1', server.emptyPage);
295
expect(page.frames.length).toBe(2);
296
await page.evaluate(`(() => {
297
window.frame = document.querySelector('#frame1');
298
window.frame.remove();
299
})()`);
300
// should have remove frame
301
expect(page.frames.filter(x => x.id === frame1.id)).toHaveLength(0);
302
const frame2Promise = page.waitOn('frame-created');
303
await Promise.all([frame2Promise, page.evaluate('document.body.appendChild(window.frame)')]);
304
expect((await frame2Promise).frame.id).not.toBe(frame1.id);
305
});
306
307
it('should refuse to display x-frame-options:deny iframe', async () => {
308
server.setRoute('/x-frame-options-deny.html', async (req, res) => {
309
res.setHeader('Content-Type', 'text/html');
310
res.setHeader('X-Frame-Options', 'DENY');
311
res.end(
312
`<!DOCTYPE html><html><head><title>login</title></head><body style="background-color: red;"><p>dangerous login page</p></body></html>`,
313
);
314
});
315
await page.goto(server.emptyPage);
316
await page.setContent(
317
`<iframe src="${server.crossProcessBaseUrl}/x-frame-options-deny.html"></iframe>`,
318
);
319
expect(page.frames).toHaveLength(2);
320
await page.frames[1].waitForLoader();
321
// CHROME redirects to chrome-error://chromewebdata/, not sure about other browsers
322
expect(page.frames[1].url).not.toMatch('/x-frame-options-deny.html');
323
});
324
});
325
326
describe('waiting', () => {
327
it('should await navigation when clicking anchor', async () => {
328
server.setRoute('/empty.html', async (req, res) => {
329
res.setHeader('Content-Type', 'text/html');
330
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
331
});
332
333
await page.setContent(`<a href="${server.emptyPage}">empty.html</a>`);
334
await page.mainFrame.waitForLoader();
335
336
const navigate = page.mainFrame.waitOn('frame-navigated');
337
await page.click('a');
338
await expect(navigate).resolves.toBeTruthy();
339
});
340
341
it('should await cross-process navigation when clicking anchor', async () => {
342
server.setRoute('/empty.html', async (req, res) => {
343
res.setHeader('Content-Type', 'text/html');
344
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
345
});
346
347
await page.setContent(`<a href="${server.crossProcessBaseUrl}/empty.html">empty.html</a>`);
348
349
const navigate = page.mainFrame.waitOn('frame-navigated');
350
await page.click('a');
351
await expect(navigate).resolves.toBeTruthy();
352
});
353
354
it('should await form-get on click', async () => {
355
server.setRoute('/empty.html?foo=bar', async (req, res) => {
356
res.setHeader('Content-Type', 'text/html');
357
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
358
});
359
360
await page.setContent(`
361
<form action="${server.emptyPage}" method="get">
362
<input name="foo" value="bar">
363
<input type="submit" value="Submit">
364
</form>`);
365
const navigate = page.mainFrame.waitOn('frame-navigated');
366
await page.click('input[type=submit]');
367
await expect(navigate).resolves.toBeTruthy();
368
});
369
370
it('should await form-post on click', async () => {
371
server.setRoute('/empty.html', async (req, res) => {
372
res.setHeader('Content-Type', 'text/html');
373
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
374
});
375
376
await page.setContent(`
377
<form action="${server.emptyPage}" method="post">
378
<input name="foo" value="bar">
379
<input type="submit" value="Submit">
380
</form>`);
381
382
const navigate = page.mainFrame.waitOn('frame-navigated');
383
await page.click('input[type=submit]');
384
await expect(navigate).resolves.toBeTruthy();
385
});
386
387
it('should await navigation when assigning location', async () => {
388
server.setRoute('/empty.html', async (req, res) => {
389
res.setHeader('Content-Type', 'text/html');
390
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
391
});
392
393
const navigate = page.mainFrame.waitOn('frame-navigated');
394
await page.evaluate(`window.location.href = "${server.emptyPage}"`);
395
await expect(navigate).resolves.toBeTruthy();
396
});
397
398
it('should await navigation when assigning location twice', async () => {
399
const messages = [];
400
server.setRoute('/empty.html?cancel', async (req, res) => {
401
res.end('done');
402
});
403
server.setRoute('/empty.html?override', async (req, res) => {
404
messages.push('routeoverride');
405
res.end('done');
406
});
407
408
const navigatedEvent = page.mainFrame.waitOn('frame-navigated');
409
await page.evaluate(`
410
window.location.href = "${server.emptyPage}?cancel";
411
window.location.href = "${server.emptyPage}?override";
412
`);
413
expect((await navigatedEvent).frame.url).toBe(`${server.emptyPage}?override`);
414
});
415
416
it('should await navigation when evaluating reload', async () => {
417
await page.goto(server.emptyPage);
418
server.setRoute('/empty.html', async (req, res) => {
419
res.setHeader('Content-Type', 'text/html');
420
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
421
});
422
423
const navigate = page.mainFrame.waitOn('frame-navigated');
424
await page.evaluate(`window.location.reload()`);
425
await expect(navigate).resolves.toBeTruthy();
426
});
427
428
it('should await navigating specified target', async () => {
429
server.setRoute('/empty.html', async (req, res) => {
430
res.setHeader('Content-Type', 'text/html');
431
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
432
});
433
434
await page.setContent(`
435
<a href="${server.emptyPage}" target=target>empty.html</a>
436
<iframe name=target></iframe>
437
`);
438
const frame = page.frames.find(x => x.name === 'target');
439
const nav = frame.waitOn('frame-navigated');
440
await page.click('a');
441
await nav;
442
expect(frame.url).toBe(server.emptyPage);
443
});
444
445
it('should be able to navigate directly following click', async () => {
446
server.setRoute('/login.html', async (req, res) => {
447
res.setHeader('Content-Type', 'text/html');
448
res.end(`You are logged in`);
449
});
450
451
await page.setContent(`
452
<form action="${server.baseUrl}/login.html" method="get">
453
<input type="text">
454
<input type="submit" value="Submit">
455
</form>`);
456
457
await page.click('input[type=text]');
458
await page.type('admin');
459
await page.click('input[type=submit]');
460
461
// when the process gets busy, it will schedule the empty page navigation but then get interrupted by the click
462
// ... ideally we could force it to always overlap, but in interim, just check for either condition
463
try {
464
const result = await page.navigate(server.emptyPage);
465
expect(result.loaderId).toBeTruthy();
466
} catch (error) {
467
// eslint-disable-next-line jest/no-try-expect
468
expect(String(error)).toMatch(/Navigation canceled/);
469
}
470
});
471
});
472
});
473
474