Merge branch 'main' into revert-32855-http-proxy-har-timing-connect
This commit is contained in:
commit
cbd70ee300
|
|
@ -102,7 +102,7 @@ await page.RouteAsync("**/*", async route =>
|
||||||
|
|
||||||
**Details**
|
**Details**
|
||||||
|
|
||||||
Note that any overrides such as [`option: url`] or [`option: headers`] only apply to the request being routed. If this request results in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header through redirects, use the combination of [`method: Route.fetch`] and [`method: Route.fulfill`] instead.
|
The [`option: headers`] option applies to both the routed request and any redirects it initiates. However, [`option: url`], [`option: method`], and [`option: postData`] only apply to the original request and are not carried over to redirected requests.
|
||||||
|
|
||||||
[`method: Route.continue`] will immediately send the request to the network, other matching handlers won't be invoked. Use [`method: Route.fallback`] If you want next matching handler in the chain to be invoked.
|
[`method: Route.continue`] will immediately send the request to the network, other matching handlers won't be invoked. Use [`method: Route.fallback`] If you want next matching handler in the chain to be invoked.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
|
|
||||||
generateTestHeader(options: LanguageGeneratorOptions): string {
|
generateTestHeader(options: LanguageGeneratorOptions): string {
|
||||||
const formatter = new JavaScriptFormatter();
|
const formatter = new JavaScriptFormatter();
|
||||||
const useText = formatContextOptions(options.contextOptions, options.deviceName);
|
const useText = formatContextOptions(options.contextOptions, options.deviceName, this._isTest);
|
||||||
formatter.add(`
|
formatter.add(`
|
||||||
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
||||||
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||||
|
|
@ -157,7 +157,7 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
||||||
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`);
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,8 +199,12 @@ function formatObjectOrVoid(value: any, indent = ' '): string {
|
||||||
return result === '{}' ? '' : result;
|
return result === '{}' ? '' : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, isTest: boolean): string {
|
||||||
const device = deviceName && deviceDescriptors[deviceName];
|
const device = deviceName && deviceDescriptors[deviceName];
|
||||||
|
if (isTest) {
|
||||||
|
// No recordHAR fixture in test.
|
||||||
|
options = { ...options, recordHar: undefined };
|
||||||
|
}
|
||||||
if (!device)
|
if (!device)
|
||||||
return formatObjectOrVoid(options);
|
return formatObjectOrVoid(options);
|
||||||
// Filter out all the properties from the device descriptor.
|
// Filter out all the properties from the device descriptor.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import zlib from 'zlib';
|
||||||
import type { HTTPCredentials } from '../../types/types';
|
import type { HTTPCredentials } from '../../types/types';
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { getUserAgent } from '../utils/userAgent';
|
import { getUserAgent } from '../utils/userAgent';
|
||||||
import { assert, constructURLBasedOnBaseURL, createGuid, monotonicTime } from '../utils';
|
import { assert, constructURLBasedOnBaseURL, createGuid, eventsHelper, monotonicTime, type RegisteredListener } from '../utils';
|
||||||
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
||||||
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
||||||
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
||||||
|
|
@ -312,8 +312,11 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
|
|
||||||
let securityDetails: har.SecurityDetails | undefined;
|
let securityDetails: har.SecurityDetails | undefined;
|
||||||
|
|
||||||
|
const listeners: RegisteredListener[] = [];
|
||||||
|
|
||||||
const request = requestConstructor(url, requestOptions as any, async response => {
|
const request = requestConstructor(url, requestOptions as any, async response => {
|
||||||
const responseAt = monotonicTime();
|
const responseAt = monotonicTime();
|
||||||
|
|
||||||
const notifyRequestFinished = (body?: Buffer) => {
|
const notifyRequestFinished = (body?: Buffer) => {
|
||||||
const endAt = monotonicTime();
|
const endAt = monotonicTime();
|
||||||
// spec: http://www.softwareishard.com/blog/har-12-spec/#timings
|
// spec: http://www.softwareishard.com/blog/har-12-spec/#timings
|
||||||
|
|
@ -478,12 +481,13 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
});
|
});
|
||||||
request.on('error', reject);
|
request.on('error', reject);
|
||||||
|
|
||||||
const disposeListener = () => {
|
listeners.push(
|
||||||
|
eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => {
|
||||||
reject(new Error('Request context disposed.'));
|
reject(new Error('Request context disposed.'));
|
||||||
request.destroy();
|
request.destroy();
|
||||||
};
|
})
|
||||||
this.on(APIRequestContext.Events.Dispose, disposeListener);
|
);
|
||||||
request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener));
|
request.on('close', () => eventsHelper.removeEventListeners(listeners));
|
||||||
|
|
||||||
request.on('socket', socket => {
|
request.on('socket', socket => {
|
||||||
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
|
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
|
||||||
|
|
@ -492,9 +496,10 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;
|
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;
|
||||||
|
|
||||||
// non-happy-eyeballs sockets
|
// non-happy-eyeballs sockets
|
||||||
socket.on('lookup', () => { dnsLookupAt = monotonicTime(); });
|
listeners.push(
|
||||||
socket.on('connect', () => { tcpConnectionAt = monotonicTime(); });
|
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
|
||||||
socket.on('secureConnect', () => {
|
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }),
|
||||||
|
eventsHelper.addEventListener(socket, 'secureConnect', () => {
|
||||||
tlsHandshakeAt = monotonicTime();
|
tlsHandshakeAt = monotonicTime();
|
||||||
|
|
||||||
if (socket instanceof TLSSocket) {
|
if (socket instanceof TLSSocket) {
|
||||||
|
|
@ -507,7 +512,8 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
issuer: peerCertificate.issuer.CN
|
issuer: peerCertificate.issuer.CN
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
serverIPAddress = socket.remoteAddress;
|
serverIPAddress = socket.remoteAddress;
|
||||||
serverPort = socket.remotePort;
|
serverPort = socket.remotePort;
|
||||||
|
|
|
||||||
|
|
@ -86,15 +86,20 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte
|
||||||
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
||||||
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
||||||
}
|
}
|
||||||
if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian') {
|
if (distroInfo?.id === 'debian' || distroInfo?.id === 'raspbian' || distroInfo?.id === 'devuan') {
|
||||||
const isOfficiallySupportedPlatform = distroInfo?.id === 'debian';
|
const isOfficiallySupportedPlatform = distroInfo?.id === 'debian';
|
||||||
if (distroInfo?.version === '11')
|
let debianVersion = distroInfo?.version;
|
||||||
|
if (distroInfo.id === 'devuan') {
|
||||||
|
// Devuan is debian-based but it's always 7 versions behind
|
||||||
|
debianVersion = String(parseInt(distroInfo.version, 10) + 7);
|
||||||
|
}
|
||||||
|
if (debianVersion === '11')
|
||||||
return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
return { hostPlatform: ('debian11' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
||||||
if (distroInfo?.version === '12')
|
if (debianVersion === '12')
|
||||||
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
||||||
// use most recent supported release for 'debian testing' and 'unstable'.
|
// use most recent supported release for 'debian testing' and 'unstable'.
|
||||||
// they never include a numeric version entry in /etc/os-release.
|
// they never include a numeric version entry in /etc/os-release.
|
||||||
if (distroInfo?.version === '')
|
if (debianVersion === '')
|
||||||
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
return { hostPlatform: ('debian12' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
|
||||||
}
|
}
|
||||||
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
|
||||||
|
|
|
||||||
12
packages/playwright-core/types/types.d.ts
vendored
12
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -20614,12 +20614,12 @@ export interface Route {
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
* Note that any overrides such as [`url`](https://playwright.dev/docs/api/class-route#route-continue-option-url) or
|
* The [`headers`](https://playwright.dev/docs/api/class-route#route-continue-option-headers) option applies to both
|
||||||
* [`headers`](https://playwright.dev/docs/api/class-route#route-continue-option-headers) only apply to the request
|
* the routed request and any redirects it initiates. However,
|
||||||
* being routed. If this request results in a redirect, overrides will not be applied to the new redirected request.
|
* [`url`](https://playwright.dev/docs/api/class-route#route-continue-option-url),
|
||||||
* If you want to propagate a header through redirects, use the combination of
|
* [`method`](https://playwright.dev/docs/api/class-route#route-continue-option-method), and
|
||||||
* [route.fetch([options])](https://playwright.dev/docs/api/class-route#route-fetch) and
|
* [`postData`](https://playwright.dev/docs/api/class-route#route-continue-option-post-data) only apply to the
|
||||||
* [route.fulfill([options])](https://playwright.dev/docs/api/class-route#route-fulfill) instead.
|
* original request and are not carried over to redirected requests.
|
||||||
*
|
*
|
||||||
* [route.continue([options])](https://playwright.dev/docs/api/class-route#route-continue) will immediately send the
|
* [route.continue([options])](https://playwright.dev/docs/api/class-route#route-continue) will immediately send the
|
||||||
* request to the network, other matching handlers won't be invoked. Use
|
* request to the network, other matching handlers won't be invoked. Use
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,8 @@ export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestS
|
||||||
else
|
else
|
||||||
location = `${relativeTestPath(config, test)}:${step?.location?.line ?? test.location.line}:${step?.location?.column ?? test.location.column}`;
|
location = `${relativeTestPath(config, test)}:${step?.location?.line ?? test.location.line}:${step?.location?.column ?? test.location.column}`;
|
||||||
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
||||||
return `${projectTitle}${location} › ${titles.join(' › ')}${stepSuffix(step)}`;
|
const tags = test.tags.length > 0 ? ` ${test.tags.join(' ')}` : '';
|
||||||
|
return `${projectTitle}${location} › ${titles.join(' › ')}${stepSuffix(step)}${tags}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTestHeader(config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string {
|
function formatTestHeader(config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string {
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,14 @@ export const TestListView: React.FC<{
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
||||||
requestedCollapseAllCount: number,
|
requestedCollapseAllCount: number,
|
||||||
|
requestedExpandAllCount: number,
|
||||||
setFilterText: (text: string) => void,
|
setFilterText: (text: string) => void,
|
||||||
onRevealSource: () => void,
|
onRevealSource: () => void,
|
||||||
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText, onRevealSource }) => {
|
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, requestedExpandAllCount, setFilterText, onRevealSource }) => {
|
||||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||||
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
||||||
|
const [expandAllCount, setExpandAllCount] = React.useState(requestedExpandAllCount);
|
||||||
|
|
||||||
// Look for a first failure within the run batch to select it.
|
// Look for a first failure within the run batch to select it.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -67,6 +69,16 @@ export const TestListView: React.FC<{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expandAllCount !== requestedExpandAllCount) {
|
||||||
|
treeState.expandedItems.clear();
|
||||||
|
for (const item of testTree.flatTreeItems())
|
||||||
|
treeState.expandedItems.set(item.id, true);
|
||||||
|
setExpandAllCount(requestedExpandAllCount);
|
||||||
|
setSelectedTreeItemId(undefined);
|
||||||
|
setTreeState({ ...treeState });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!runningState || runningState.itemSelectedByUser)
|
if (!runningState || runningState.itemSelectedByUser)
|
||||||
return;
|
return;
|
||||||
let selectedTreeItem: TreeItem | undefined;
|
let selectedTreeItem: TreeItem | undefined;
|
||||||
|
|
@ -85,7 +97,7 @@ export const TestListView: React.FC<{
|
||||||
|
|
||||||
if (selectedTreeItem)
|
if (selectedTreeItem)
|
||||||
setSelectedTreeItemId(selectedTreeItem.id);
|
setSelectedTreeItemId(selectedTreeItem.id);
|
||||||
}, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, treeState, setTreeState]);
|
}, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, expandAllCount, setExpandAllCount, requestedExpandAllCount, treeState, setTreeState]);
|
||||||
|
|
||||||
// Compute selected item.
|
// Compute selected item.
|
||||||
const { selectedTreeItem } = React.useMemo(() => {
|
const { selectedTreeItem } = React.useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const commandQueue = React.useRef(Promise.resolve());
|
const commandQueue = React.useRef(Promise.resolve());
|
||||||
const runTestBacklog = React.useRef<Set<string>>(new Set());
|
const runTestBacklog = React.useRef<Set<string>>(new Set());
|
||||||
const [collapseAllCount, setCollapseAllCount] = React.useState(0);
|
const [collapseAllCount, setCollapseAllCount] = React.useState(0);
|
||||||
|
const [expandAllCount, setExpandAllCount] = React.useState(0);
|
||||||
const [isDisconnected, setIsDisconnected] = React.useState(false);
|
const [isDisconnected, setIsDisconnected] = React.useState(false);
|
||||||
const [hasBrowsers, setHasBrowsers] = React.useState(true);
|
const [hasBrowsers, setHasBrowsers] = React.useState(true);
|
||||||
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
||||||
|
|
@ -473,6 +474,9 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<ToolbarButton icon='collapse-all' title='Collapse all' onClick={() => {
|
<ToolbarButton icon='collapse-all' title='Collapse all' onClick={() => {
|
||||||
setCollapseAllCount(collapseAllCount + 1);
|
setCollapseAllCount(collapseAllCount + 1);
|
||||||
}} />
|
}} />
|
||||||
|
<ToolbarButton icon='expand-all' title='Expand all' onClick={() => {
|
||||||
|
setExpandAllCount(expandAllCount + 1);
|
||||||
|
}} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<TestListView
|
<TestListView
|
||||||
filterText={filterText}
|
filterText={filterText}
|
||||||
|
|
@ -487,6 +491,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
setWatchedTreeIds={setWatchedTreeIds}
|
setWatchedTreeIds={setWatchedTreeIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
requestedCollapseAllCount={collapseAllCount}
|
requestedCollapseAllCount={collapseAllCount}
|
||||||
|
requestedExpandAllCount={expandAllCount}
|
||||||
setFilterText={setFilterText}
|
setFilterText={setFilterText}
|
||||||
onRevealSource={onRevealSource}
|
onRevealSource={onRevealSource}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -85,13 +85,11 @@ test('test', async ({ page }) => {`;
|
||||||
await cli.waitFor(expectedResult);
|
await cli.waitFor(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with --save-har', async ({ runCLI }, testInfo) => {
|
test('should not generate recordHAR with --save-har', async ({ runCLI }, testInfo) => {
|
||||||
const harFileName = testInfo.outputPath('har.har');
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
const expectedResult = `
|
const expectedResult = `test.use({
|
||||||
recordHar: {
|
serviceWorkers: 'block'
|
||||||
mode: 'minimal',
|
});`;
|
||||||
path: '${harFileName.replace(/\\/g, '\\\\')}'
|
|
||||||
}`;
|
|
||||||
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`], {
|
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`], {
|
||||||
autoExitWhen: expectedResult,
|
autoExitWhen: expectedResult,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
import { test as it, expect } from './pageTest';
|
import { test as it, expect } from './pageTest';
|
||||||
import type { Route } from 'playwright-core';
|
import type { Route } from 'playwright-core';
|
||||||
|
import type * as http from 'http';
|
||||||
|
|
||||||
it('should work', async ({ page, server }) => {
|
it('should work', async ({ page, server }) => {
|
||||||
await page.route('**/*', route => route.continue());
|
await page.route('**/*', route => route.continue());
|
||||||
|
|
@ -473,6 +474,196 @@ it('continue should delete headers on redirects', {
|
||||||
expect(serverRequest.headers.foo).toBeFalsy();
|
expect(serverRequest.headers.foo).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('propagate headers same origin redirect', {
|
||||||
|
annotation: [
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' },
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' },
|
||||||
|
]
|
||||||
|
}, async ({ page, server }) => {
|
||||||
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
|
let resolve;
|
||||||
|
const serverRequestPromise = new Promise<http.IncomingMessage>(f => resolve = f);
|
||||||
|
server.setRoute('/something', (request, response) => {
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
response.writeHead(204, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization,custom',
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(request);
|
||||||
|
response.writeHead(200, { });
|
||||||
|
response.end('done');
|
||||||
|
});
|
||||||
|
await server.setRedirect('/redirect', '/something');
|
||||||
|
const text = await page.evaluate(async url => {
|
||||||
|
const data = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
authorization: 'credentials',
|
||||||
|
custom: 'foo'
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return data.text();
|
||||||
|
}, server.PREFIX + '/redirect');
|
||||||
|
expect(text).toBe('done');
|
||||||
|
const serverRequest = await serverRequestPromise;
|
||||||
|
expect.soft(serverRequest.headers['authorization']).toBe('credentials');
|
||||||
|
expect.soft(serverRequest.headers['custom']).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagate headers cross origin', {
|
||||||
|
annotation: [
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' },
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' },
|
||||||
|
]
|
||||||
|
}, async ({ page, server }) => {
|
||||||
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
|
let resolve;
|
||||||
|
const serverRequestPromise = new Promise<http.IncomingMessage>(f => resolve = f);
|
||||||
|
server.setRoute('/something', (request, response) => {
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
response.writeHead(204, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization,custom',
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(request);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
});
|
||||||
|
response.end('done');
|
||||||
|
});
|
||||||
|
const text = await page.evaluate(async url => {
|
||||||
|
const data = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
authorization: 'credentials',
|
||||||
|
custom: 'foo'
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return data.text();
|
||||||
|
}, server.CROSS_PROCESS_PREFIX + '/something');
|
||||||
|
expect(text).toBe('done');
|
||||||
|
const serverRequest = await serverRequestPromise;
|
||||||
|
expect.soft(serverRequest.headers['authorization']).toBe('credentials');
|
||||||
|
expect.soft(serverRequest.headers['custom']).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagate headers cross origin redirect', {
|
||||||
|
annotation: [
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' },
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' },
|
||||||
|
]
|
||||||
|
}, async ({ page, server }) => {
|
||||||
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
|
let resolve;
|
||||||
|
const serverRequestPromise = new Promise<http.IncomingMessage>(f => resolve = f);
|
||||||
|
server.setRoute('/something', (request, response) => {
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
response.writeHead(204, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization,custom',
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(request);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
});
|
||||||
|
response.end('done');
|
||||||
|
});
|
||||||
|
server.setRoute('/redirect', (request, response) => {
|
||||||
|
response.writeHead(301, { location: `${server.CROSS_PROCESS_PREFIX}/something` });
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
const text = await page.evaluate(async url => {
|
||||||
|
const data = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
authorization: 'credentials',
|
||||||
|
custom: 'foo'
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return data.text();
|
||||||
|
}, server.PREFIX + '/redirect');
|
||||||
|
expect(text).toBe('done');
|
||||||
|
const serverRequest = await serverRequestPromise;
|
||||||
|
// Authorization header not propagated to cross-origin redirect.
|
||||||
|
expect.soft(serverRequest.headers['authorization']).toBeFalsy();
|
||||||
|
expect.soft(serverRequest.headers['custom']).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagate headers cross origin redirect after interception', {
|
||||||
|
annotation: [
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' },
|
||||||
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32045' },
|
||||||
|
]
|
||||||
|
}, async ({ page, server, browserName }) => {
|
||||||
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
|
let resolve;
|
||||||
|
const serverRequestPromise = new Promise<http.IncomingMessage>(f => resolve = f);
|
||||||
|
server.setRoute('/something', (request, response) => {
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
response.writeHead(204, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization,custom',
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(request);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': server.PREFIX,
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
});
|
||||||
|
response.end('done');
|
||||||
|
});
|
||||||
|
server.setRoute('/redirect', (request, response) => {
|
||||||
|
response.writeHead(301, { location: `${server.CROSS_PROCESS_PREFIX}/something` });
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
await page.route('**/redirect', async route => {
|
||||||
|
await route.continue({
|
||||||
|
headers: {
|
||||||
|
...route.request().headers(),
|
||||||
|
authorization: 'credentials',
|
||||||
|
custom: 'foo'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const text = await page.evaluate(async url => {
|
||||||
|
const data = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
authorization: 'none',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return data.text();
|
||||||
|
}, server.PREFIX + '/redirect');
|
||||||
|
expect(text).toBe('done');
|
||||||
|
const serverRequest = await serverRequestPromise;
|
||||||
|
if (browserName === 'webkit')
|
||||||
|
expect.soft(serverRequest.headers['authorization']).toBeFalsy();
|
||||||
|
else
|
||||||
|
expect.soft(serverRequest.headers['authorization']).toBe('credentials');
|
||||||
|
expect.soft(serverRequest.headers['custom']).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
it('should intercept css variable with background url', async ({ page, server }) => {
|
it('should intercept css variable with background url', async ({ page, server }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -418,5 +418,19 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.output).toMatch(/\d+ passed \(\d+(\.\d)?(ms|s)\)/);
|
expect(result.output).toMatch(/\d+ passed \(\d+(\.\d)?(ms|s)\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should output tags', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
test('passes', { tag: ['@foo', '@bar'] }, async ({}) => {
|
||||||
|
expect(0).toBe(0);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
const text = result.output;
|
||||||
|
|
||||||
|
expect(text).toContain('passes @foo @bar');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +297,32 @@ test('should collapse all', async ({ runUITest }) => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should expand all', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32825' }
|
||||||
|
}, async ({ runUITest }) => {
|
||||||
|
const { page } = await runUITest(basicTestTree);
|
||||||
|
|
||||||
|
await page.getByTestId('test-tree').getByText('suite').click();
|
||||||
|
await page.getByTitle('Collapse all').click();
|
||||||
|
await expect.poll(dumpTestTree(page)).toContain(`
|
||||||
|
► ◯ a.test.ts
|
||||||
|
► ◯ b.test.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
await page.getByTitle('Expand all').click();
|
||||||
|
await expect.poll(dumpTestTree(page)).toContain(`
|
||||||
|
▼ ◯ a.test.ts
|
||||||
|
◯ passes
|
||||||
|
◯ fails
|
||||||
|
▼ ◯ suite
|
||||||
|
◯ inner passes
|
||||||
|
◯ inner fails
|
||||||
|
▼ ◯ b.test.ts
|
||||||
|
◯ passes
|
||||||
|
◯ fails
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
test('should resolve title conflicts', async ({ runUITest }) => {
|
test('should resolve title conflicts', async ({ runUITest }) => {
|
||||||
const { page } = await runUITest({
|
const { page } = await runUITest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue