diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 2acbc5caae..c178b48aca 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -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(); let url = params.url; @@ -131,6 +138,6 @@ function harFindResponse(har: HARFile, params: { url: string, method: string }): continue; } - return entry.response; + return entry; } } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index e459407994..71c1baa73d 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -283,8 +283,12 @@ export class Route extends ChannelOwner 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); } diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index df849f7ada..fd754ae932 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -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 = { diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index d430ace66c..33295daacb 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -2457,6 +2457,7 @@ Route: abort: parameters: errorCode: string? + redirectAbortedNavigationToUrl: string? continue: parameters: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 8a24dd1b2b..7e93725379 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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), diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 791203a23b..4bd9f44827 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -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 { 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 }; } diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 771903dfca..4390dfa91f 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -57,7 +57,9 @@ export class FrameDispatcher extends Dispatcher 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) }; diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 1a8ba62de0..046ccff8f3 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -135,7 +135,7 @@ export class RouteDispatcher extends Dispatcher im } async abort(params: channels.RouteAbortParams): Promise { - await this._object.abort(params.errorCode || 'failed'); + await this._object.abort(params.errorCode || 'failed', params.redirectAbortedNavigationToUrl); } } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 3714b555eb..dc37709e55 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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 = (injectedScript: js.JSHandle) => Promise>>; export type DomTaskBody = (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; private _detachedCallback = () => {}; private _raceAgainstEvaluationStallingEventsPromises = new Set>(); + _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(action: () => Promise): Promise { + async raceNavigationAction(progress: Progress, options: types.GotoOptions, action: () => Promise): Promise { 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 { 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 { - 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 { + 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 { 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 { 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; diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index ed562df798..61c0259e99 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -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); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 118e9aced0..67c5b47953 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -348,7 +348,7 @@ export class Page extends SdkObject { async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise { 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 { 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 { 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; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 346a962f36..08302028ff 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -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) : ''; diff --git a/tests/assets/har-redirect.har b/tests/assets/har-redirect.har new file mode 100644 index 0000000000..c8e865f7d9 --- /dev/null +++ b/tests/assets/har-redirect.har @@ -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": "; 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": "

hello

" + }, + "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 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts index 8dce6eaa74..2110f29b49 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -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/'); +});