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 type { HAREntry, HARFile, HARResponse } from '../../types/types';
import type { HAREntry, HARFile } from '../../types/types';
import { debugLogger } from '../common/debugLogger';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { ZipFile } from '../utils/zipFile';
@ -51,9 +51,9 @@ export class HarRouter {
}
private async _handle(route: Route) {
let response;
let entry;
try {
response = harFindResponse(this._harFile, {
entry = harFindResponse(this._harFile, {
url: route.request().url(),
method: route.request().method()
});
@ -62,8 +62,15 @@ export class HarRouter {
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()}`);
const response = entry.response;
const sha1 = (response.content as any)._sha1;
if (this._zipFile && sha1) {
@ -106,7 +113,7 @@ export class HarRouter {
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 visited = new Set<HAREntry>();
let url = params.url;
@ -131,6 +138,6 @@ function harFindResponse(har: HARFile, params: { url: string, method: string }):
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) {
await this._abort(errorCode);
}
async _abort(errorCode?: string, redirectAbortedNavigationToUrl?: string) {
this._checkNotHandled();
await this._raceWithPageClose(this._channel.abort({ errorCode }));
await this._raceWithPageClose(this._channel.abort({ errorCode, redirectAbortedNavigationToUrl }));
this._reportHandled(true);
}

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils';
import * as dialog from '../dialog';
import * as dom from '../dom';
import type * as frames from '../frames';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
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> {
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id });
if (response.errorText)
throw new Error(`${response.errorText} at ${url}`);
throw new frames.NavigationAbortedError(response.loaderId, `${response.errorText} at ${url}`);
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 => {
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 };
if (event.newDocument)
(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> {
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,
// the navigation did not commit.
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 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 = {
frame: Frame;
info: SelectorInfo;
@ -228,8 +238,8 @@ export class FrameManager {
}
frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
frame.emit(Frame.Events.Navigation, navigationEvent);
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument, isPublic: true };
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
if (!initial) {
debugLogger.log('api', ` navigated to "${url}"`);
this._page.frameNavigatedToNewDocument(frame);
@ -243,8 +253,8 @@ export class FrameManager {
if (!frame)
return;
frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name };
frame.emit(Frame.Events.Navigation, navigationEvent);
const navigationEvent: NavigationEvent = { url, name: frame._name, isPublic: true };
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
debugLogger.log('api', ` navigated to "${url}"`);
}
@ -258,10 +268,11 @@ export class FrameManager {
url: frame._url,
name: frame._name,
newDocument: frame.pendingDocument(),
error: new Error(errorText),
error: new NavigationAbortedError(documentId, errorText),
isPublic: !frame._pendingNavigationRedirectAfterAbort
};
frame.setPendingDocument(undefined);
frame.emit(Frame.Events.Navigation, navigationEvent);
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
}
frameDetached(frameId: string) {
@ -433,7 +444,7 @@ export class FrameManager {
export class Frame extends SdkObject {
static Events = {
Navigation: 'navigation',
InternalNavigation: 'internalnavigation',
AddLifecycle: 'addlifecycle',
RemoveLifecycle: 'removelifecycle',
};
@ -456,6 +467,7 @@ export class Frame extends SdkObject {
readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {};
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
_pendingNavigationRedirectAfterAbort: { url: string, documentId: string } | undefined;
constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page, 'frame');
@ -586,15 +598,29 @@ export class Frame extends SdkObject {
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([
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._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> {
const constructedNavigationURL = constructURLBasedOnBaseURL(this._page._browserContext._options.baseURL, url);
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> {
return this.raceNavigationAction(async () => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = this._page._state.extraHTTPHeaders || [];
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
let referer = refererHeader ? refererHeader.value : undefined;
if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header');
referer = options.referer;
return this.raceNavigationAction(progress, options, async () => this._gotoAction(progress, url, options));
}
private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = this._page._state.extraHTTPHeaders || [];
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
let referer = refererHeader ? refererHeader.value : undefined;
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);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
let event: NavigationEvent;
if (navigateResult.newDocumentId) {
sameDocument.dispose();
event = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (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');
}
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;
});
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> {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.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.
if (event.error)
return true;
@ -835,28 +863,31 @@ export class Frame extends SdkObject {
async setContent(metadata: CallMetadata, html: string, options: types.NavigateOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(progress => this.raceNavigationAction(async () => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => {
this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
this._onClearLifecycle();
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
return controller.run(async progress => {
await this.raceNavigationAction(progress, options, async () => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => {
this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
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 }) => {
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));
}, this._page._timeoutSettings.navigationTimeout(options));
}
name(): string {
@ -1712,7 +1743,9 @@ class SignalBarrier {
if (frame.parentFrame())
return;
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)
this._progress.log(` navigated to "${frame._url}"`);
return true;

View file

@ -244,8 +244,10 @@ export class Route extends SdkObject {
return this._request;
}
async abort(errorCode: string = 'failed') {
async abort(errorCode: string = 'failed', redirectAbortedNavigationToUrl?: string) {
this._startHandling();
if (redirectAbortedNavigationToUrl)
this._request.frame().redirectNavigationAfterAbort(redirectAbortedNavigationToUrl, this._request._documentId!);
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> {
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(),
// so we should await it immediately.
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> {
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,
// so we should catch it immediately.
let error: Error | undefined;
@ -383,7 +383,7 @@ export class Page extends SdkObject {
async goForward(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
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,
// so we should catch it immediately.
let error: Error | undefined;

View file

@ -394,7 +394,10 @@ class ContextRecorder extends EventEmitter {
});
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.Dialog, () => this._onDialog(page));
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 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/');
});