mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
fix(har): internal redirect in renderer-initiated navigations (#15000)
fix(har): internal redirect in renderer-initiated navigations
This commit is contained in:
parent
c0ea28d558
commit
6af6fab84a
@ -56,7 +56,7 @@ export class HarRouter {
|
|||||||
|
|
||||||
if (response.action === 'redirect') {
|
if (response.action === 'redirect') {
|
||||||
debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
|
debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
|
||||||
await route._abort(undefined, response.redirectURL);
|
await route._redirectNavigationRequest(response.redirectURL!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,12 +282,14 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
async abort(errorCode?: string) {
|
async abort(errorCode?: string) {
|
||||||
await this._abort(errorCode);
|
this._checkNotHandled();
|
||||||
|
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
||||||
|
this._reportHandled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _abort(errorCode?: string, redirectAbortedNavigationToUrl?: string) {
|
async _redirectNavigationRequest(url: string) {
|
||||||
this._checkNotHandled();
|
this._checkNotHandled();
|
||||||
await this._raceWithPageClose(this._channel.abort({ errorCode, redirectAbortedNavigationToUrl }));
|
await this._raceWithPageClose(this._channel.redirectNavigationRequest({ url }));
|
||||||
this._reportHandled(true);
|
this._reportHandled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3158,17 +3158,23 @@ export interface RouteEventTarget {
|
|||||||
}
|
}
|
||||||
export interface RouteChannel extends RouteEventTarget, Channel {
|
export interface RouteChannel extends RouteEventTarget, Channel {
|
||||||
_type_Route: boolean;
|
_type_Route: boolean;
|
||||||
|
redirectNavigationRequest(params: RouteRedirectNavigationRequestParams, metadata?: Metadata): Promise<RouteRedirectNavigationRequestResult>;
|
||||||
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
|
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
|
||||||
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
|
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
|
||||||
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
|
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
|
||||||
}
|
}
|
||||||
|
export type RouteRedirectNavigationRequestParams = {
|
||||||
|
url: string,
|
||||||
|
};
|
||||||
|
export type RouteRedirectNavigationRequestOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type RouteRedirectNavigationRequestResult = void;
|
||||||
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 = {
|
||||||
|
@ -2491,10 +2491,13 @@ Route:
|
|||||||
|
|
||||||
commands:
|
commands:
|
||||||
|
|
||||||
|
redirectNavigationRequest:
|
||||||
|
parameters:
|
||||||
|
url: string
|
||||||
|
|
||||||
abort:
|
abort:
|
||||||
parameters:
|
parameters:
|
||||||
errorCode: string?
|
errorCode: string?
|
||||||
redirectAbortedNavigationToUrl: string?
|
|
||||||
|
|
||||||
continue:
|
continue:
|
||||||
parameters:
|
parameters:
|
||||||
|
@ -1181,9 +1181,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
});
|
});
|
||||||
scheme.RequestResponseParams = tOptional(tObject({}));
|
scheme.RequestResponseParams = tOptional(tObject({}));
|
||||||
scheme.RequestRawRequestHeadersParams = tOptional(tObject({}));
|
scheme.RequestRawRequestHeadersParams = tOptional(tObject({}));
|
||||||
|
scheme.RouteRedirectNavigationRequestParams = tObject({
|
||||||
|
url: tString,
|
||||||
|
});
|
||||||
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),
|
||||||
|
@ -135,7 +135,11 @@ 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', params.redirectAbortedNavigationToUrl);
|
await this._object.abort(params.errorCode || 'failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise<void> {
|
||||||
|
await this._object.redirectNavigationRequest(params.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ export class FrameManager {
|
|||||||
name: frame._name,
|
name: frame._name,
|
||||||
newDocument: frame.pendingDocument(),
|
newDocument: frame.pendingDocument(),
|
||||||
error: new NavigationAbortedError(documentId, errorText),
|
error: new NavigationAbortedError(documentId, errorText),
|
||||||
isPublic: !frame._pendingNavigationRedirectAfterAbort
|
isPublic: !(documentId && frame._redirectedNavigations.has(documentId)),
|
||||||
};
|
};
|
||||||
frame.setPendingDocument(undefined);
|
frame.setPendingDocument(undefined);
|
||||||
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
|
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
|
||||||
@ -467,7 +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;
|
readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
|
||||||
|
|
||||||
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
||||||
super(page, 'frame');
|
super(page, 'frame');
|
||||||
@ -604,12 +604,11 @@ export class Frame extends SdkObject {
|
|||||||
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().catch(e => {
|
action().catch(e => {
|
||||||
if (this._pendingNavigationRedirectAfterAbort && e instanceof NavigationAbortedError) {
|
if (e instanceof NavigationAbortedError && e.documentId) {
|
||||||
const { url, documentId } = this._pendingNavigationRedirectAfterAbort;
|
const data = this._redirectedNavigations.get(e.documentId);
|
||||||
this._pendingNavigationRedirectAfterAbort = undefined;
|
if (data) {
|
||||||
if (e.documentId === documentId) {
|
progress.log(`waiting for redirected navigation to "${data.url}"`);
|
||||||
progress.log(`redirecting navigation to "${url}"`);
|
return data.gotoPromise;
|
||||||
return this._gotoAction(progress, url, options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
@ -617,8 +616,14 @@ export class Frame extends SdkObject {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectNavigationAfterAbort(url: string, documentId: string) {
|
redirectNavigation(url: string, documentId: string, referer: string | undefined) {
|
||||||
this._pendingNavigationRedirectAfterAbort = { url, documentId };
|
const controller = new ProgressController(serverSideCallMetadata(), this);
|
||||||
|
const data = {
|
||||||
|
url,
|
||||||
|
gotoPromise: controller.run(progress => this._gotoAction(progress, url, { referer }), 0),
|
||||||
|
};
|
||||||
|
this._redirectedNavigations.set(documentId, data);
|
||||||
|
data.gotoPromise.finally(() => this._redirectedNavigations.delete(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> {
|
||||||
@ -659,7 +664,7 @@ export class Frame extends SdkObject {
|
|||||||
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
||||||
// This is just a sanity check. In practice, new navigation should
|
// This is just a sanity check. In practice, new navigation should
|
||||||
// cancel the previous one and report "request cancelled"-like error.
|
// cancel the previous one and report "request cancelled"-like error.
|
||||||
throw new Error('Navigation interrupted by another one');
|
throw new NavigationAbortedError(navigateResult.newDocumentId, 'Navigation interrupted by another one');
|
||||||
}
|
}
|
||||||
if (event.error)
|
if (event.error)
|
||||||
throw event.error;
|
throw event.error;
|
||||||
|
@ -244,13 +244,17 @@ export class Route extends SdkObject {
|
|||||||
return this._request;
|
return this._request;
|
||||||
}
|
}
|
||||||
|
|
||||||
async abort(errorCode: string = 'failed', redirectAbortedNavigationToUrl?: string) {
|
async abort(errorCode: string = 'failed') {
|
||||||
this._startHandling();
|
this._startHandling();
|
||||||
if (redirectAbortedNavigationToUrl)
|
|
||||||
this._request.frame().redirectNavigationAfterAbort(redirectAbortedNavigationToUrl, this._request._documentId!);
|
|
||||||
await this._delegate.abort(errorCode);
|
await this._delegate.abort(errorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async redirectNavigationRequest(url: string) {
|
||||||
|
this._startHandling();
|
||||||
|
assert(this._request.isNavigationRequest());
|
||||||
|
this._request.frame().redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
|
||||||
|
}
|
||||||
|
|
||||||
async fulfill(overrides: channels.RouteFulfillParams) {
|
async fulfill(overrides: channels.RouteFulfillParams) {
|
||||||
this._startHandling();
|
this._startHandling();
|
||||||
let body = overrides.body;
|
let body = overrides.body;
|
||||||
|
@ -115,6 +115,7 @@ it('should change document URL after redirected navigation', async ({ contextFac
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const [response] = await Promise.all([
|
const [response] = await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
|
page.waitForURL('https://www.theverge.com/'),
|
||||||
page.goto('https://theverge.com/')
|
page.goto('https://theverge.com/')
|
||||||
]);
|
]);
|
||||||
await expect(page).toHaveURL('https://www.theverge.com/');
|
await expect(page).toHaveURL('https://www.theverge.com/');
|
||||||
@ -122,6 +123,23 @@ it('should change document URL after redirected navigation', async ({ contextFac
|
|||||||
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
|
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should change document URL after redirected navigation on click', async ({ server, contextFactory, isAndroid, asset }) => {
|
||||||
|
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 page.setContent(`<a href="https://theverge.com/">click me</a>`);
|
||||||
|
const [response] = await Promise.all([
|
||||||
|
page.waitForNavigation(),
|
||||||
|
page.click('text=click me'),
|
||||||
|
]);
|
||||||
|
await expect(page).toHaveURL('https://www.theverge.com/');
|
||||||
|
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('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
|
||||||
it.fixme(isAndroid);
|
it.fixme(isAndroid);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user