Skip to content

Commit c0fbc82

Browse files
authored
Merge pull request #390 from ulixee/loader
fix(puppet): better handling of default loader
2 parents fe95569 + ba6bcee commit c0fbc82

File tree

9 files changed

+74
-44
lines changed

9 files changed

+74
-44
lines changed

core/lib/FrameEnvironment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ b) Use the UserProfile feature to set cookies for 1 or more domains before they'
355355
return this.waitForDom(jsPath, options);
356356
}
357357

358-
public waitForLoad(status: IPipelineStatus, options?: IWaitForOptions): Promise<void> {
358+
public async waitForLoad(status: IPipelineStatus, options?: IWaitForOptions): Promise<void> {
359+
await this.isReady;
359360
return this.navigationsObserver.waitForLoad(status, options);
360361
}
361362

core/test/domRecorder.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ function sort() {
225225
const domFrames = domChanges.filter(x => x.tagName === 'IFRAME');
226226
expect(domFrames).toHaveLength(3);
227227

228-
await tab.puppetPage.frames[3].waitForLoad();
228+
await tab.frameEnvironmentsByPuppetId.get(tab.puppetPage.frames[3].id).isReady;
229229

230230
await state.db.flush();
231231
const frames = state.db.frames.all();

interfaces/IPuppetFrame.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface IPuppetFrame extends ITypedEventEmitter<IPuppetFrameEvents> {
1414
isDefaultUrl: boolean;
1515
isAttached(): boolean;
1616
resolveNodeId(backendNodeId: number): Promise<string>;
17-
waitForLoad(eventName?: keyof ILifecycleEvents): Promise<void>;
17+
waitForLoad(eventName?: keyof ILifecycleEvents, timeoutMs?: number): Promise<void>;
1818
waitForLoader(loaderId?: string): Promise<Error | undefined>;
1919
canEvaluate(isolatedFromWebPageEnvironment: boolean): boolean;
2020
getFrameElementNodeId(): Promise<string>;

mitm-socket/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ export default class MitmSocket
181181

182182
private triggerConnectErrorIfNeeded(isExiting = false): void {
183183
if (this.connectPromise?.isResolved) return;
184-
if (!isExiting && !this.connectError) return;
184+
if (isExiting && !this.connectError) {
185+
this.connectPromise.resolve();
186+
return;
187+
}
185188
this.connectPromise?.reject(
186189
buildConnectError(
187190
this.connectError ?? `Socket process exited during connect`,

puppet-chrome/lib/BrowserContext.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ import TargetInfo = Protocol.Target.TargetInfo;
3434

3535
export class BrowserContext
3636
extends TypedEventEmitter<IPuppetContextEvents>
37-
implements IPuppetContext
38-
{
37+
implements IPuppetContext {
3938
public logger: IBoundLog;
4039

4140
public workersById = new Map<string, IPuppetWorker>();
@@ -48,6 +47,7 @@ export class BrowserContext
4847
private pageOptionsByTargetId = new Map<string, IPuppetPageOptions>();
4948
private readonly createdTargetIds = new Set<string>();
5049
private creatingTargetPromises: Promise<void>[] = [];
50+
private waitForPageAttachedById = new Map<string, Resolvable<Page>>();
5151
private readonly browser: Browser;
5252

5353
private isClosing = false;
@@ -81,8 +81,8 @@ export class BrowserContext
8181
public defaultPageInitializationFn: (page: IPuppetPage) => Promise<any> = () => Promise.resolve();
8282

8383
async newPage(options?: IPuppetPageOptions): Promise<Page> {
84-
const resolvable = new Resolvable<void>();
85-
this.creatingTargetPromises.push(resolvable.promise);
84+
const createTargetPromise = new Resolvable<void>();
85+
this.creatingTargetPromises.push(createTargetPromise.promise);
8686

8787
const { targetId } = await this.sendWithBrowserDevtoolsSession('Target.createTarget', {
8888
url: 'about:blank',
@@ -94,29 +94,24 @@ export class BrowserContext
9494

9595
await this.attachToTarget(targetId);
9696

97-
resolvable.resolve();
98-
const idx = this.creatingTargetPromises.indexOf(resolvable.promise);
97+
createTargetPromise.resolve();
98+
const idx = this.creatingTargetPromises.indexOf(createTargetPromise.promise);
9999
if (idx >= 0) this.creatingTargetPromises.splice(idx, 1);
100100

101-
let hasTimedOut = false;
102-
const timeout = setTimeout(() => {
103-
hasTimedOut = true;
104-
}, 10e3).unref();
105-
106-
// NOTE: flow here interrupts and expects session to attach and call onPageAttached below
107-
while (!this.isClosing) {
108-
const page = this.pagesById.get(targetId);
109-
if (!page) {
110-
if (hasTimedOut) throw new Error('Error creating page. Timed out waiting to attach');
111-
await new Promise(setImmediate);
112-
113-
continue;
114-
}
115-
clearTimeout(timeout);
116-
await page.isReady;
117-
if (page.isClosed) throw new Error('Page has been closed.');
118-
return page;
101+
let page = this.pagesById.get(targetId);
102+
if (!page) {
103+
const pageAttachedPromise = new Resolvable<Page>(
104+
60e3,
105+
'Error creating page. Timed out waiting to attach',
106+
);
107+
this.waitForPageAttachedById.set(targetId, pageAttachedPromise);
108+
page = await pageAttachedPromise.promise;
109+
this.waitForPageAttachedById.delete(targetId);
119110
}
111+
112+
await page.isReady;
113+
if (page.isClosed) throw new Error('Page has been closed.');
114+
return page;
120115
}
121116

122117
initializePage(page: Page): Promise<any> {
@@ -150,6 +145,7 @@ export class BrowserContext
150145

151146
const page = new Page(devtoolsSession, targetInfo.targetId, this, this.logger, opener);
152147
this.pagesById.set(page.targetId, page);
148+
this.waitForPageAttachedById.get(page.targetId)?.resolve(page);
153149
await page.isReady;
154150
this.emit('page', { page });
155151
return page;
@@ -220,6 +216,9 @@ export class BrowserContext
220216
if (this.isClosing) return;
221217
this.isClosing = true;
222218

219+
for (const waitingPage of this.waitForPageAttachedById.values()) {
220+
waitingPage.reject(new CanceledPromiseError('BrowserContext shutting down'));
221+
}
223222
if (this.browser.devtoolsSession.isConnected()) {
224223
await Promise.all([...this.pagesById.values()].map(x => x.close()));
225224
await this.sendWithBrowserDevtoolsSession('Target.disposeBrowserContext', {

puppet-chrome/lib/Frame.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default class Frame extends TypedEventEmitter<IPuppetFrameEvents> impleme
7373
private loaderIdResolvers = new Map<string, IResolvablePromise<Error | null>>();
7474
private readonly devtoolsSession: DevtoolsSession;
7575
private startedLoaderId: string;
76+
private defaultLoaderId: string;
7677
private resolveLoaderTimeout: NodeJS.Timeout;
7778

7879
private get activeLoader(): IResolvablePromise<Error | null> {
@@ -236,11 +237,15 @@ export default class Frame extends TypedEventEmitter<IPuppetFrameEvents> impleme
236237
public onLoaded(internalFrame: PageFrame): void {
237238
this.internalFrame = internalFrame;
238239
this.updateUrl();
239-
this.setLoader(internalFrame.loaderId);
240-
if (internalFrame.loaderId && this.url) {
241-
this.loaderIdResolvers.get(internalFrame.loaderId).resolve();
240+
if (!internalFrame.loaderId) return;
241+
242+
// if we this is the first loader and url is default, this is the first loader
243+
if (this.isDefaultUrl && !this.defaultLoaderId && this.loaderIdResolvers.size === 0) {
244+
this.defaultLoaderId = internalFrame.loaderId;
242245
}
243-
if (internalFrame.loaderId && internalFrame.unreachableUrl) {
246+
this.setLoader(internalFrame.loaderId);
247+
248+
if (this.url || internalFrame.unreachableUrl) {
244249
// if this is a loaded frame, just count it as loaded. it shouldn't fail
245250
this.loaderIdResolvers.get(internalFrame.loaderId).resolve();
246251
}
@@ -299,9 +304,17 @@ export default class Frame extends TypedEventEmitter<IPuppetFrameEvents> impleme
299304
}
300305
}
301306

302-
public async waitForLoader(loaderId?: string): Promise<Error | null> {
303-
const hasLoaderError = await this.loaderIdResolvers.get(loaderId ?? this.activeLoaderId)
304-
?.promise;
307+
public async waitForLoader(loaderId?: string, timeoutMs = 60e3): Promise<Error | null> {
308+
if (!loaderId) {
309+
loaderId = this.activeLoaderId;
310+
if (loaderId === this.defaultLoaderId) {
311+
// wait for an actual frame to load
312+
const frameLoader = await this.waitOn('frame-loader-created', null, timeoutMs);
313+
loaderId = frameLoader.loaderId;
314+
}
315+
}
316+
317+
const hasLoaderError = await this.loaderIdResolvers.get(loaderId)?.promise;
305318
if (hasLoaderError) return hasLoaderError;
306319

307320
if (!this.getActiveContextId(false)) {
@@ -348,7 +361,7 @@ export default class Frame extends TypedEventEmitter<IPuppetFrameEvents> impleme
348361
lifecycle[name] = new Date();
349362
}
350363

351-
if (!this.isDefaultUrl) {
364+
if (loaderId !== this.defaultLoaderId) {
352365
this.emit('frame-lifecycle', { frame: this, name, loaderId: pageLoaderId });
353366
}
354367
}
@@ -425,9 +438,15 @@ export default class Frame extends TypedEventEmitter<IPuppetFrameEvents> impleme
425438
};
426439
}
427440

428-
public async waitForLoad(event: keyof ILifecycleEvents = 'load'): Promise<void> {
441+
public async waitForLoad(
442+
event: keyof ILifecycleEvents = 'load',
443+
timeoutMs = 30e3,
444+
): Promise<void> {
445+
event ??= 'load';
446+
timeoutMs ??= 30e3;
447+
await this.waitForLoader(null, timeoutMs);
429448
if (this.lifecycleEvents[event]) return;
430-
await this.waitOn('frame-lifecycle', x => x.name === event, 30e3);
449+
await this.waitOn('frame-lifecycle', x => x.name === event, timeoutMs);
431450
}
432451

433452
private setLoader(loaderId: string): void {

puppet-chrome/lib/FramesManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default class FramesManager extends TypedEventEmitter<IPuppetFrameManager
9292
}
9393
} catch (error) {
9494
if (error instanceof CanceledPromiseError) {
95+
resolve();
9596
return;
9697
}
9798
reject(error);

puppet/test/TestPage.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface ITestPage extends IPuppetPage {
1010
type(text: string): Promise<void>;
1111
attachFrame(frameId: string, url: string): Promise<IPuppetFrame>;
1212
detachFrame(frameId: string): Promise<void>;
13-
goto(url: string, waitOnLifecycle?: string): Promise<void>;
13+
goto(url: string, waitOnLifecycle?: string, timeoutMs?: number): Promise<void>;
1414
setContent(content: string): Promise<void>;
1515
waitForPopup(): Promise<IPuppetPage>;
1616
}
@@ -81,13 +81,20 @@ export async function goto(
8181
page: IPuppetPage,
8282
url: string,
8383
waitOnLifecycle: 'load' | 'DOMContentLoaded' = 'load',
84+
timeoutMs?: number,
8485
) {
85-
const nav = page.navigate(url);
86-
const lifecycle = page.mainFrame.waitOn('frame-lifecycle', ev => ev.name === waitOnLifecycle);
87-
await Promise.all([lifecycle, nav, page.mainFrame.waitOn('frame-navigated')]);
86+
waitOnLifecycle ??= 'load';
87+
timeoutMs ??= 30e3;
88+
await Promise.all([
89+
page.navigate(url),
90+
page.mainFrame.waitOn('frame-navigated', null, timeoutMs),
91+
]);
92+
await page.mainFrame.waitForLoad(waitOnLifecycle, timeoutMs);
8893
}
8994

9095
export async function setContent(page: IPuppetPage, content: string) {
96+
// @ts-ignore
97+
page.mainFrame.defaultLoaderId = null;
9198
await page.evaluate(`((content) => {
9299
window.stop();
93100
document.open();

puppet/test/load.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('Load test', () => {
6262
page = createTestPage(await context.newPage());
6363
await page.goto(server.url('link.html'));
6464

65-
const navigate = page.mainFrame.waitOn('frame-navigated');
65+
const navigate = page.mainFrame.waitOn('frame-navigated', null, 75e3);
6666
await page.click('a');
6767
await navigate;
6868
expect(page.mainFrame.url).toBe(`${server.crossProcessBaseUrl}/empty.html`);
@@ -71,5 +71,5 @@ describe('Load test', () => {
7171
}
7272
});
7373
await Promise.all(concurrent);
74-
}, 60e3);
74+
}, 75e3);
7575
});

0 commit comments

Comments
 (0)