fix(har): restart redirected navigation (#14939)

This commit is contained in:
Yury Semikhatsky 2022-06-17 21:17:30 -07:00 committed by GitHub
parent 030e7d211c
commit ed6b14f0f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 828 additions and 88 deletions

View file

@ -15,7 +15,7 @@
*/ */
import fs from 'fs'; import fs from 'fs';
import type { HAREntry, HARFile, HARResponse } from '../../types/types'; import type { HAREntry, HARFile } from '../../types/types';
import { debugLogger } from '../common/debugLogger'; import { debugLogger } from '../common/debugLogger';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import { ZipFile } from '../utils/zipFile'; import { ZipFile } from '../utils/zipFile';
@ -51,9 +51,9 @@ export class HarRouter {
} }
private async _handle(route: Route) { private async _handle(route: Route) {
let response; let entry;
try { try {
response = harFindResponse(this._harFile, { entry = harFindResponse(this._harFile, {
url: route.request().url(), url: route.request().url(),
method: route.request().method() method: route.request().method()
}); });
@ -62,8 +62,15 @@ export class HarRouter {
debugLogger.log('api', e); debugLogger.log('api', e);
} }
if (response) { if (entry) {
// If navigation is being redirected, restart it with the final url to ensure the document's url changes.
if (entry.request.url !== route.request().url() && route.request().isNavigationRequest()) {
debugLogger.log('api', `redirecting HAR navigation: ${route.request().url()} => ${entry.request.url}`);
await route._abort(undefined, entry.request.url);
return;
}
debugLogger.log('api', `serving from HAR: ${route.request().method()} ${route.request().url()}`); debugLogger.log('api', `serving from HAR: ${route.request().method()} ${route.request().url()}`);
const response = entry.response;
const sha1 = (response.content as any)._sha1; const sha1 = (response.content as any)._sha1;
if (this._zipFile && sha1) { if (this._zipFile && sha1) {
@ -106,7 +113,7 @@ export class HarRouter {
const redirectStatus = [301, 302, 303, 307, 308]; const redirectStatus = [301, 302, 303, 307, 308];
function harFindResponse(har: HARFile, params: { url: string, method: string }): HARResponse | undefined { function harFindResponse(har: HARFile, params: { url: string, method: string }): HAREntry | undefined {
const harLog = har.log; const harLog = har.log;
const visited = new Set<HAREntry>(); const visited = new Set<HAREntry>();
let url = params.url; let url = params.url;
@ -131,6 +138,6 @@ function harFindResponse(har: HARFile, params: { url: string, method: string }):
continue; continue;
} }
return entry.response; return entry;
} }
} }

View file

@ -283,8 +283,12 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
} }
async abort(errorCode?: string) { async abort(errorCode?: string) {
await this._abort(errorCode);
}
async _abort(errorCode?: string, redirectAbortedNavigationToUrl?: string) {
this._checkNotHandled(); this._checkNotHandled();
await this._raceWithPageClose(this._channel.abort({ errorCode })); await this._raceWithPageClose(this._channel.abort({ errorCode, redirectAbortedNavigationToUrl }));
this._reportHandled(true); this._reportHandled(true);
} }

View file

@ -3125,9 +3125,11 @@ export interface RouteChannel extends RouteEventTarget, Channel {
} }
export type RouteAbortParams = { export type RouteAbortParams = {
errorCode?: string, errorCode?: string,
redirectAbortedNavigationToUrl?: string,
}; };
export type RouteAbortOptions = { export type RouteAbortOptions = {
errorCode?: string, errorCode?: string,
redirectAbortedNavigationToUrl?: string,
}; };
export type RouteAbortResult = void; export type RouteAbortResult = void;
export type RouteContinueParams = { export type RouteContinueParams = {

View file

@ -2457,6 +2457,7 @@ Route:
abort: abort:
parameters: parameters:
errorCode: string? errorCode: string?
redirectAbortedNavigationToUrl: string?
continue: continue:
parameters: parameters:

View file

@ -1169,6 +1169,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.RequestRawRequestHeadersParams = tOptional(tObject({})); scheme.RequestRawRequestHeadersParams = tOptional(tObject({}));
scheme.RouteAbortParams = tObject({ scheme.RouteAbortParams = tObject({
errorCode: tOptional(tString), errorCode: tOptional(tString),
redirectAbortedNavigationToUrl: tOptional(tString),
}); });
scheme.RouteContinueParams = tObject({ scheme.RouteContinueParams = tObject({
url: tOptional(tString), url: tOptional(tString),

View file

@ -23,7 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils'; import { assert, createGuid, headersArrayToObject } from '../../utils';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import type * as frames from '../frames'; import * as frames from '../frames';
import { helper } from '../helper'; import { helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page'; import type { PageBinding, PageDelegate } from '../page';
@ -588,7 +588,7 @@ class FrameSession {
async _navigate(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> { async _navigate(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id }); const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id });
if (response.errorText) if (response.errorText)
throw new Error(`${response.errorText} at ${url}`); throw new frames.NavigationAbortedError(response.loaderId, `${response.errorText} at ${url}`);
return { newDocumentId: response.loaderId }; return { newDocumentId: response.loaderId };
} }

View file

@ -57,7 +57,9 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel> im
frame.on(Frame.Events.RemoveLifecycle, lifecycleEvent => { frame.on(Frame.Events.RemoveLifecycle, lifecycleEvent => {
this._dispatchEvent('loadstate', { remove: lifecycleEvent }); this._dispatchEvent('loadstate', { remove: lifecycleEvent });
}); });
frame.on(Frame.Events.Navigation, (event: NavigationEvent) => { frame.on(Frame.Events.InternalNavigation, (event: NavigationEvent) => {
if (!event.isPublic)
return;
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
if (event.newDocument) if (event.newDocument)
(params as any).newDocument = { request: RequestDispatcher.fromNullable(this._scope, event.newDocument.request || null) }; (params as any).newDocument = { request: RequestDispatcher.fromNullable(this._scope, event.newDocument.request || null) };

View file

@ -135,7 +135,7 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteChannel> im
} }
async abort(params: channels.RouteAbortParams): Promise<void> { async abort(params: channels.RouteAbortParams): Promise<void> {
await this._object.abort(params.errorCode || 'failed'); await this._object.abort(params.errorCode || 'failed', params.redirectAbortedNavigationToUrl);
} }
} }

View file

@ -75,11 +75,21 @@ export type NavigationEvent = {
// Error for cross-document navigations if any. When error is present, // Error for cross-document navigations if any. When error is present,
// the navigation did not commit. // the navigation did not commit.
error?: Error, error?: Error,
// Wether this event should be visible to the clients via the public APIs.
isPublic?: boolean;
}; };
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>; export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
export type DomTaskBody<T, R, E> = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[]) => R | symbol; export type DomTaskBody<T, R, E> = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[]) => R | symbol;
export class NavigationAbortedError extends Error {
readonly documentId?: string;
constructor(documentId: string | undefined, message: string) {
super(message);
this.documentId = documentId;
}
}
type SelectorInFrame = { type SelectorInFrame = {
frame: Frame; frame: Frame;
info: SelectorInfo; info: SelectorInfo;
@ -228,8 +238,8 @@ export class FrameManager {
} }
frame._onClearLifecycle(); frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument }; const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument, isPublic: true };
frame.emit(Frame.Events.Navigation, navigationEvent); frame.emit(Frame.Events.InternalNavigation, navigationEvent);
if (!initial) { if (!initial) {
debugLogger.log('api', ` navigated to "${url}"`); debugLogger.log('api', ` navigated to "${url}"`);
this._page.frameNavigatedToNewDocument(frame); this._page.frameNavigatedToNewDocument(frame);
@ -243,8 +253,8 @@ export class FrameManager {
if (!frame) if (!frame)
return; return;
frame._url = url; frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name }; const navigationEvent: NavigationEvent = { url, name: frame._name, isPublic: true };
frame.emit(Frame.Events.Navigation, navigationEvent); frame.emit(Frame.Events.InternalNavigation, navigationEvent);
debugLogger.log('api', ` navigated to "${url}"`); debugLogger.log('api', ` navigated to "${url}"`);
} }
@ -258,10 +268,11 @@ export class FrameManager {
url: frame._url, url: frame._url,
name: frame._name, name: frame._name,
newDocument: frame.pendingDocument(), newDocument: frame.pendingDocument(),
error: new Error(errorText), error: new NavigationAbortedError(documentId, errorText),
isPublic: !frame._pendingNavigationRedirectAfterAbort
}; };
frame.setPendingDocument(undefined); frame.setPendingDocument(undefined);
frame.emit(Frame.Events.Navigation, navigationEvent); frame.emit(Frame.Events.InternalNavigation, navigationEvent);
} }
frameDetached(frameId: string) { frameDetached(frameId: string) {
@ -433,7 +444,7 @@ export class FrameManager {
export class Frame extends SdkObject { export class Frame extends SdkObject {
static Events = { static Events = {
Navigation: 'navigation', InternalNavigation: 'internalnavigation',
AddLifecycle: 'addlifecycle', AddLifecycle: 'addlifecycle',
RemoveLifecycle: 'removelifecycle', RemoveLifecycle: 'removelifecycle',
}; };
@ -456,6 +467,7 @@ export class Frame extends SdkObject {
readonly _detachedPromise: Promise<void>; readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {}; private _detachedCallback = () => {};
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>(); private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
_pendingNavigationRedirectAfterAbort: { url: string, documentId: string } | undefined;
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page, 'frame'); super(page, 'frame');
@ -586,15 +598,29 @@ export class Frame extends SdkObject {
this._subtreeLifecycleEvents = events; this._subtreeLifecycleEvents = events;
} }
async raceNavigationAction<T>(action: () => Promise<T>): Promise<T> { async raceNavigationAction(progress: Progress, options: types.GotoOptions, action: () => Promise<network.Response | null>): Promise<network.Response | null> {
return Promise.race([ return Promise.race([
this._page._disconnectedPromise.then(() => { throw new Error('Navigation failed because page was closed!'); }), this._page._disconnectedPromise.then(() => { throw new Error('Navigation failed because page was closed!'); }),
this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }), this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }),
this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }), this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }),
action(), action().catch(e => {
if (this._pendingNavigationRedirectAfterAbort && e instanceof NavigationAbortedError) {
const { url, documentId } = this._pendingNavigationRedirectAfterAbort;
this._pendingNavigationRedirectAfterAbort = undefined;
if (e.documentId === documentId) {
progress.log(`redirecting navigation to "${url}"`);
return this._gotoAction(progress, url, options);
}
}
throw e;
}),
]); ]);
} }
redirectNavigationAfterAbort(url: string, documentId: string) {
this._pendingNavigationRedirectAfterAbort = { url, documentId };
}
async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> { async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
const constructedNavigationURL = constructURLBasedOnBaseURL(this._page._browserContext._options.baseURL, url); const constructedNavigationURL = constructURLBasedOnBaseURL(this._page._browserContext._options.baseURL, url);
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
@ -602,57 +628,59 @@ export class Frame extends SdkObject {
} }
private async _goto(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> { private async _goto(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
return this.raceNavigationAction(async () => { return this.raceNavigationAction(progress, options, async () => this._gotoAction(progress, url, options));
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); }
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = this._page._state.extraHTTPHeaders || []; private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer'); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
let referer = refererHeader ? refererHeader.value : undefined; progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
if (options.referer !== undefined) { const headers = this._page._state.extraHTTPHeaders || [];
if (referer !== undefined && referer !== options.referer) const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
throw new Error('"referer" is already specified as extra HTTP header'); let referer = refererHeader ? refererHeader.value : undefined;
referer = options.referer; if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header');
referer = options.referer;
}
url = helper.completeUserURL(url);
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
let event: NavigationEvent;
if (navigateResult.newDocumentId) {
sameDocument.dispose();
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => {
// We are interested either in this specific document, or any other document that
// did commit and replaced the expected document.
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
}).promise;
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
// This is just a sanity check. In practice, new navigation should
// cancel the previous one and report "request cancelled"-like error.
throw new Error('Navigation interrupted by another one');
} }
url = helper.completeUserURL(url); if (event.error)
throw event.error;
} else {
event = await sameDocument.promise;
}
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.Navigation, (e: NavigationEvent) => !e.newDocument); if (!this._subtreeLifecycleEvents.has(waitUntil))
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
let event: NavigationEvent; const request = event.newDocument ? event.newDocument.request : undefined;
if (navigateResult.newDocumentId) { const response = request ? request._finalRequest().response() : null;
sameDocument.dispose(); await this._page._doSlowMo();
event = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => { return response;
// We are interested either in this specific document, or any other document that
// did commit and replaced the expected document.
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
}).promise;
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
// This is just a sanity check. In practice, new navigation should
// cancel the previous one and report "request cancelled"-like error.
throw new Error('Navigation interrupted by another one');
}
if (event.error)
throw event.error;
} else {
event = await sameDocument.promise;
}
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = event.newDocument ? event.newDocument.request : undefined;
const response = request ? request._finalRequest().response() : null;
await this._page._doSlowMo();
return response;
});
} }
async _waitForNavigation(progress: Progress, options: types.NavigateOptions): Promise<network.Response | null> { async _waitForNavigation(progress: Progress, options: types.NavigateOptions): Promise<network.Response | null> {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`waiting for navigation until "${waitUntil}"`); progress.log(`waiting for navigation until "${waitUntil}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => { const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => {
// Any failed navigation results in a rejection. // Any failed navigation results in a rejection.
if (event.error) if (event.error)
return true; return true;
@ -835,28 +863,31 @@ export class Frame extends SdkObject {
async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise<void> { async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(progress => this.raceNavigationAction(async () => { return controller.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; await this.raceNavigationAction(progress, options, async () => {
progress.log(`setting frame content, waiting until "${waitUntil}"`); const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`; progress.log(`setting frame content, waiting until "${waitUntil}"`);
const context = await this._utilityContext(); const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const lifecyclePromise = new Promise((resolve, reject) => { const context = await this._utilityContext();
this._page._frameManager._consoleMessageTags.set(tag, () => { const lifecyclePromise = new Promise((resolve, reject) => {
// Clear lifecycle right after document.open() - see 'tag' below. this._page._frameManager._consoleMessageTags.set(tag, () => {
this._onClearLifecycle(); // Clear lifecycle right after document.open() - see 'tag' below.
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject); this._onClearLifecycle();
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
});
}); });
const contentPromise = context.evaluate(({ html, tag }) => {
window.stop();
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, { html, tag });
await Promise.all([contentPromise, lifecyclePromise]);
await this._page._doSlowMo();
return null;
}); });
const contentPromise = context.evaluate(({ html, tag }) => { }, this._page._timeoutSettings.navigationTimeout(options));
window.stop();
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, { html, tag });
await Promise.all([contentPromise, lifecyclePromise]);
await this._page._doSlowMo();
}), this._page._timeoutSettings.navigationTimeout(options));
} }
name(): string { name(): string {
@ -1712,7 +1743,9 @@ class SignalBarrier {
if (frame.parentFrame()) if (frame.parentFrame())
return; return;
this.retain(); this.retain();
const waiter = helper.waitForEvent(null, frame, Frame.Events.Navigation, (e: NavigationEvent) => { const waiter = helper.waitForEvent(null, frame, Frame.Events.InternalNavigation, (e: NavigationEvent) => {
if (!e.isPublic)
return false;
if (!e.error && this._progress) if (!e.error && this._progress)
this._progress.log(` navigated to "${frame._url}"`); this._progress.log(` navigated to "${frame._url}"`);
return true; return true;

View file

@ -244,8 +244,10 @@ export class Route extends SdkObject {
return this._request; return this._request;
} }
async abort(errorCode: string = 'failed') { async abort(errorCode: string = 'failed', redirectAbortedNavigationToUrl?: string) {
this._startHandling(); this._startHandling();
if (redirectAbortedNavigationToUrl)
this._request.frame().redirectNavigationAfterAbort(redirectAbortedNavigationToUrl, this._request._documentId!);
await this._delegate.abort(errorCode); await this._delegate.abort(errorCode);
} }

View file

@ -348,7 +348,7 @@ export class Page extends SdkObject {
async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> { async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(progress => this.mainFrame().raceNavigationAction(async () => { return controller.run(progress => this.mainFrame().raceNavigationAction(progress, options, async () => {
// Note: waitForNavigation may fail before we get response to reload(), // Note: waitForNavigation may fail before we get response to reload(),
// so we should await it immediately. // so we should await it immediately.
const [response] = await Promise.all([ const [response] = await Promise.all([
@ -362,7 +362,7 @@ export class Page extends SdkObject {
async goBack(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> { async goBack(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(progress => this.mainFrame().raceNavigationAction(async () => { return controller.run(progress => this.mainFrame().raceNavigationAction(progress, options, async () => {
// Note: waitForNavigation may fail before we get response to goBack, // Note: waitForNavigation may fail before we get response to goBack,
// so we should catch it immediately. // so we should catch it immediately.
let error: Error | undefined; let error: Error | undefined;
@ -383,7 +383,7 @@ export class Page extends SdkObject {
async goForward(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> { async goForward(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(progress => this.mainFrame().raceNavigationAction(async () => { return controller.run(progress => this.mainFrame().raceNavigationAction(progress, options, async () => {
// Note: waitForNavigation may fail before we get response to goForward, // Note: waitForNavigation may fail before we get response to goForward,
// so we should catch it immediately. // so we should catch it immediately.
let error: Error | undefined; let error: Error | undefined;

View file

@ -394,7 +394,10 @@ class ContextRecorder extends EventEmitter {
}); });
this._pageAliases.delete(page); this._pageAliases.delete(page);
}); });
frame.on(Frame.Events.Navigation, () => this._onFrameNavigated(frame, page)); frame.on(Frame.Events.InternalNavigation, event => {
if (event.isPublic)
this._onFrameNavigated(frame, page);
});
page.on(Page.Events.Download, () => this._onDownload(page)); page.on(Page.Events.Download, () => this._onDownload(page));
page.on(Page.Events.Dialog, () => this._onDialog(page)); page.on(Page.Events.Dialog, () => this._onDialog(page));
const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';

View file

@ -0,0 +1,623 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Playwright",
"version": "1.23.0-next"
},
"browser": {
"name": "chromium",
"version": "103.0.5060.42"
},
"pages": [
{
"startedDateTime": "2022-06-16T21:41:23.901Z",
"id": "page@8f314969edc000996eb5c2ab22f0e6b3",
"title": "Microsoft",
"pageTimings": {
"onContentLoad": 8363,
"onLoad": 8896
}
}
],
"entries": [
{
"_requestref": "request@7d6e0ddb1e1e25f6e5c4a7c943c0bae1",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928357.437,
"startedDateTime": "2022-06-16T21:41:23.951Z",
"time": 93.99,
"request": {
"method": "GET",
"url": "https://theverge.com/",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{
"name": ":authority",
"value": "theverge.com"
},
{
"name": ":method",
"value": "GET"
},
{
"name": ":path",
"value": "/"
},
{
"name": ":scheme",
"value": "https"
},
{
"name": "accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "accept-encoding",
"value": "gzip, deflate, br"
},
{
"name": "accept-language",
"value": "en-US,en;q=0.9"
},
{
"name": "sec-ch-ua",
"value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\""
},
{
"name": "sec-ch-ua-mobile",
"value": "?0"
},
{
"name": "sec-ch-ua-platform",
"value": "\"Linux\""
},
{
"name": "sec-fetch-dest",
"value": "document"
},
{
"name": "sec-fetch-mode",
"value": "navigate"
},
{
"name": "sec-fetch-site",
"value": "none"
},
{
"name": "sec-fetch-user",
"value": "?1"
},
{
"name": "upgrade-insecure-requests",
"value": "1"
},
{
"name": "user-agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 644,
"bodySize": 0
},
"response": {
"status": 301,
"statusText": "",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "vmidv1",
"value": "9faf31ab-1415-4b90-b367-24b670205f41",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "content-length",
"value": "0"
},
{
"name": "date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "location",
"value": "http://www.theverge.com/"
},
{
"name": "retry-after",
"value": "0"
},
{
"name": "server",
"value": "Varnish"
},
{
"name": "set-cookie",
"value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "via",
"value": "1.1 varnish"
},
{
"name": "x-cache",
"value": "HIT"
},
{
"name": "x-cache-hits",
"value": "0"
},
{
"name": "x-served-by",
"value": "cache-pao17442-PAO"
},
{
"name": "x-timer",
"value": "S1655415684.005867,VS0,VE0"
}
],
"content": {
"size": -1,
"mimeType": "x-unknown",
"compression": 0
},
"headersSize": 425,
"bodySize": 0,
"redirectURL": "http://www.theverge.com/",
"_transferSize": 425
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 0,
"connect": 34.151,
"ssl": 28.074,
"send": 0,
"wait": 27.549,
"receive": 4.216
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.65.52",
"_serverPort": 443,
"_securityDetails": {
"protocol": "TLS 1.2",
"subjectName": "*.americanninjawarriornation.com",
"issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1",
"validFrom": 1644853133,
"validTo": 1679153932
}
},
{
"_requestref": "request@5c7a316ee46a095bda80c23ddc8c740d",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928427.603,
"startedDateTime": "2022-06-16T21:41:24.022Z",
"time": 44.39499999999999,
"request": {
"method": "GET",
"url": "http://www.theverge.com/",
"httpVersion": "HTTP/1.1",
"cookies": [],
"headers": [
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.9"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Host",
"value": "www.theverge.com"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 423,
"bodySize": 0
},
"response": {
"status": 301,
"statusText": "Moved Permanently",
"httpVersion": "HTTP/1.1",
"cookies": [
{
"name": "_chorus_geoip_continent",
"value": "NA"
},
{
"name": "vmidv1",
"value": "4e0c1265-10f8-4cb1-a5de-1c3cf70b531c",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "www.theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "Accept-Ranges",
"value": "bytes"
},
{
"name": "Age",
"value": "2615"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Content-Length",
"value": "0"
},
{
"name": "Content-Type",
"value": "text/html"
},
{
"name": "Date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "Location",
"value": "https://www.theverge.com/"
},
{
"name": "Server",
"value": "nginx"
},
{
"name": "Set-Cookie",
"value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;"
},
{
"name": "Set-Cookie",
"value": "vmidv1=4e0c1265-10f8-4cb1-a5de-1c3cf70b531c;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "Vary",
"value": "X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Accept-Encoding"
},
{
"name": "Via",
"value": "1.1 varnish"
},
{
"name": "X-Cache",
"value": "HIT"
},
{
"name": "X-Cache-Hits",
"value": "2"
},
{
"name": "X-Served-By",
"value": "cache-pao17450-PAO"
},
{
"name": "X-Timer",
"value": "S1655415684.035748,VS0,VE0"
}
],
"content": {
"size": -1,
"mimeType": "text/html",
"compression": 0
},
"headersSize": 731,
"bodySize": 0,
"redirectURL": "https://www.theverge.com/",
"_transferSize": 731
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 2.742,
"connect": 10.03,
"ssl": 14.123,
"send": 0,
"wait": 15.023,
"receive": 2.477
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.189.52",
"_serverPort": 80,
"_securityDetails": {}
},
{
"_requestref": "request@17664a6093c12c97d41efbff3a502adb",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928455.901,
"startedDateTime": "2022-06-16T21:41:24.050Z",
"time": 50.29199999999999,
"request": {
"method": "GET",
"url": "https://www.theverge.com/",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "vmidv1",
"value": "9faf31ab-1415-4b90-b367-24b670205f41"
},
{
"name": "_chorus_geoip_continent",
"value": "NA"
}
],
"headers": [
{
"name": ":authority",
"value": "www.theverge.com"
},
{
"name": ":method",
"value": "GET"
},
{
"name": ":path",
"value": "/"
},
{
"name": ":scheme",
"value": "https"
},
{
"name": "accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
},
{
"name": "accept-encoding",
"value": "gzip, deflate, br"
},
{
"name": "accept-language",
"value": "en-US,en;q=0.9"
},
{
"name": "cookie",
"value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41; _chorus_geoip_continent=NA"
},
{
"name": "sec-ch-ua",
"value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\""
},
{
"name": "sec-ch-ua-mobile",
"value": "?0"
},
{
"name": "sec-ch-ua-platform",
"value": "\"Linux\""
},
{
"name": "sec-fetch-dest",
"value": "document"
},
{
"name": "sec-fetch-mode",
"value": "navigate"
},
{
"name": "sec-fetch-site",
"value": "none"
},
{
"name": "sec-fetch-user",
"value": "?1"
},
{
"name": "upgrade-insecure-requests",
"value": "1"
},
{
"name": "user-agent",
"value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36"
}
],
"queryString": [],
"headersSize": 729,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2.0",
"cookies": [
{
"name": "_chorus_geoip_continent",
"value": "NA"
},
{
"name": "vmidv1",
"value": "40d8fd14-5ac3-4757-9e9c-efb106e82d3a",
"expires": "2027-06-15T21:41:24.000Z",
"domain": "www.theverge.com",
"path": "/",
"sameSite": "Lax",
"secure": true
}
],
"headers": [
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "age",
"value": "263"
},
{
"name": "cache-control",
"value": "max-age=0, public, must-revalidate"
},
{
"name": "content-encoding",
"value": "br"
},
{
"name": "content-length",
"value": "14"
},
{
"name": "content-security-policy",
"value": "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob: ; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests"
},
{
"name": "content-type",
"value": "text/html; charset=utf-8"
},
{
"name": "date",
"value": "Thu, 16 Jun 2022 21:41:24 GMT"
},
{
"name": "etag",
"value": "W/\"d498ef668223d015000070a66a181e85\""
},
{
"name": "link",
"value": "<https://concertads-configs.vox-cdn.com/sbn/verge/config.json>; rel=preload; as=fetch; crossorigin"
},
{
"name": "referrer-policy",
"value": "strict-origin-when-cross-origin"
},
{
"name": "server",
"value": "nginx"
},
{
"name": "set-cookie",
"value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;"
},
{
"name": "set-cookie",
"value": "vmidv1=40d8fd14-5ac3-4757-9e9c-efb106e82d3a;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure"
},
{
"name": "strict-transport-security",
"value": "max-age=31556952; preload"
},
{
"name": "vary",
"value": "Accept-Encoding, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Origin, X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region"
},
{
"name": "via",
"value": "1.1 varnish"
},
{
"name": "x-cache",
"value": "HIT"
},
{
"name": "x-cache-hits",
"value": "1"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "x-download-options",
"value": "noopen"
},
{
"name": "x-frame-options",
"value": "SAMEORIGIN"
},
{
"name": "x-permitted-cross-domain-policies",
"value": "none"
},
{
"name": "x-request-id",
"value": "97363ad70e272e63641c0bb784fa06a01b848dfd"
},
{
"name": "x-runtime",
"value": "0.257911"
},
{
"name": "x-served-by",
"value": "cache-pao17436-PAO"
},
{
"name": "x-timer",
"value": "S1655415684.075077,VS0,VE1"
},
{
"name": "x-xss-protection",
"value": "1; mode=block"
}
],
"content": {
"size": 14,
"mimeType": "text/html",
"compression": 0,
"text": "<h1>hello</h1>"
},
"headersSize": 1742,
"bodySize": 48716,
"redirectURL": "",
"_transferSize": 48716
},
"cache": {
"beforeRequest": null,
"afterRequest": null
},
"timings": {
"dns": 0.016,
"connect": 24.487,
"ssl": 17.406,
"send": 0,
"wait": 8.383,
"receive": -1
},
"pageref": "page@8f314969edc000996eb5c2ab22f0e6b3",
"serverIPAddress": "151.101.189.52",
"_serverPort": 443,
"_securityDetails": {
"protocol": "TLS 1.2",
"subjectName": "*.americanninjawarriornation.com",
"issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1",
"validFrom": 1644853133,
"validTo": 1679153932
}
}
]
}
}

View file

@ -104,3 +104,65 @@ it('newPage should fulfill from har, matching the method and following redirects
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
await page.close(); await page.close();
}); });
it('should change document URL after redirected navigation', async ({ contextFactory, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path } });
const page = await context.newPage();
const [response] = await Promise.all([
page.waitForNavigation(),
page.goto('https://theverge.com/')
]);
await expect(page).toHaveURL('https://www.theverge.com/');
await expect(response.request().url()).toBe('https://www.theverge.com/');
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
const page = await context.newPage();
await page.goto('https://theverge.com/');
await page.goto(server.EMPTY_PAGE);
await expect(page).toHaveURL(server.EMPTY_PAGE);
const response = await page.goBack();
await expect(page).toHaveURL('https://www.theverge.com/');
await expect(response.request().url()).toBe('https://www.theverge.com/');
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should goForward to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await expect(page).toHaveURL(server.EMPTY_PAGE);
await page.goto('https://theverge.com/');
await expect(page).toHaveURL('https://www.theverge.com/');
await page.goBack();
await expect(page).toHaveURL(server.EMPTY_PAGE);
const response = await page.goForward();
await expect(page).toHaveURL('https://www.theverge.com/');
await expect(response.request().url()).toBe('https://www.theverge.com/');
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should reload redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
const page = await context.newPage();
await page.goto('https://theverge.com/');
await expect(page).toHaveURL('https://www.theverge.com/');
const response = await page.reload();
await expect(page).toHaveURL('https://www.theverge.com/');
await expect(response.request().url()).toBe('https://www.theverge.com/');
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});