mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
/**
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { expect, playwrightTest } from '../config/browserTest';
|
|
import type { Browser, BrowserServer, ConnectOptions, Page } from 'playwright-core';
|
|
|
|
type ExtraFixtures = {
|
|
remoteServer: BrowserServer;
|
|
connect: (wsEndpoint: string, options?: ConnectOptions) => Promise<Browser>,
|
|
twoPages: { pageA: Page, pageB: Page },
|
|
};
|
|
const test = playwrightTest.extend<ExtraFixtures>({
|
|
remoteServer: async ({ browserType }, use) => {
|
|
const server = await browserType.launchServer({ _sharedBrowser: true } as any);
|
|
await use(server);
|
|
await server.close();
|
|
},
|
|
connect: async ({ browserType }, use) => {
|
|
let browser: Browser | undefined;
|
|
await use(async (wsEndpoint, options = {}) => {
|
|
browser = await browserType.connect(wsEndpoint, options);
|
|
return browser;
|
|
});
|
|
await browser?.close();
|
|
},
|
|
twoPages: async ({ remoteServer, connect }, use) => {
|
|
const browserA = await connect(remoteServer.wsEndpoint());
|
|
const contextA = await browserA.newContext();
|
|
const pageA = await contextA.newPage();
|
|
|
|
const browserB = await connect(remoteServer.wsEndpoint());
|
|
const contextB = browserB.contexts()[0];
|
|
const pageB = contextB.pages()[0];
|
|
|
|
await use({ pageA, pageB });
|
|
},
|
|
});
|
|
|
|
test.slow(true, 'All connect tests are slow');
|
|
test.skip(({ mode }) => mode !== 'default');
|
|
|
|
async function disconnect(page: Page) {
|
|
await page.context().browser().close();
|
|
// Give disconnect some time to cleanup.
|
|
await new Promise(f => setTimeout(f, 1000));
|
|
}
|
|
|
|
test('should connect two clients', async ({ connect, remoteServer, server }) => {
|
|
const browserA = await connect(remoteServer.wsEndpoint());
|
|
expect(browserA.contexts().length).toBe(0);
|
|
const contextA1 = await browserA.newContext();
|
|
const pageA1 = await contextA1.newPage();
|
|
await pageA1.goto(server.EMPTY_PAGE);
|
|
|
|
const browserB = await connect(remoteServer.wsEndpoint());
|
|
expect(browserB.contexts().length).toBe(1);
|
|
const contextB1 = browserB.contexts()[0];
|
|
expect(contextB1.pages().length).toBe(1);
|
|
const pageB1 = contextB1.pages()[0];
|
|
await expect(pageB1).toHaveURL(server.EMPTY_PAGE);
|
|
|
|
const contextB2 = await browserB.newContext({ baseURL: server.PREFIX });
|
|
expect(browserB.contexts()).toEqual([contextB1, contextB2]);
|
|
await expect.poll(() => browserA.contexts().length).toBe(2);
|
|
const contextA2 = browserA.contexts()[1];
|
|
expect(browserA.contexts()).toEqual([contextA1, contextA2]);
|
|
|
|
const pageEventPromise = new Promise<Page>(f => contextB2.on('page', f));
|
|
const pageA2 = await contextA2.newPage();
|
|
const pageB2 = await pageEventPromise;
|
|
await pageA2.goto('/frames/frame.html');
|
|
await expect(pageB2).toHaveURL('/frames/frame.html');
|
|
|
|
// Both contexts and pages should be still operational after any client disconnects.
|
|
await disconnect(pageA1);
|
|
|
|
await expect(pageB1).toHaveURL(server.EMPTY_PAGE);
|
|
await expect(pageB2).toHaveURL(server.PREFIX + '/frames/frame.html');
|
|
});
|
|
|
|
test('should have separate default timeouts', async ({ twoPages }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
pageA.setDefaultTimeout(500);
|
|
pageB.setDefaultTimeout(600);
|
|
|
|
const [errorA, errorB] = await Promise.all([
|
|
pageA.click('div').catch(e => e),
|
|
pageB.click('div').catch(e => e),
|
|
]);
|
|
expect(errorA.message).toContain('Timeout 500ms exceeded');
|
|
expect(errorB.message).toContain('Timeout 600ms exceeded');
|
|
});
|
|
|
|
test('should receive viewport size changes', async ({ twoPages }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
await pageA.setViewportSize({ width: 567, height: 456 });
|
|
expect(pageA.viewportSize()).toEqual({ width: 567, height: 456 });
|
|
await expect.poll(() => pageB.viewportSize()).toEqual({ width: 567, height: 456 });
|
|
|
|
await pageB.setViewportSize({ width: 456, height: 567 });
|
|
expect(pageB.viewportSize()).toEqual({ width: 456, height: 567 });
|
|
await expect.poll(() => pageA.viewportSize()).toEqual({ width: 456, height: 567 });
|
|
});
|
|
|
|
test('should not allow parallel js coverage and cleanup upon disconnect', async ({ twoPages, browserName }) => {
|
|
test.skip(browserName !== 'chromium');
|
|
|
|
const { pageA, pageB } = twoPages;
|
|
await pageA.coverage.startJSCoverage();
|
|
const error = await pageB.coverage.startJSCoverage().catch(e => e);
|
|
expect(error.message).toContain('JSCoverage is already enabled');
|
|
|
|
// Should cleanup coverage on disconnect and allow another client to start it.
|
|
await disconnect(pageA);
|
|
await pageB.coverage.startJSCoverage();
|
|
});
|
|
|
|
test('should not allow parallel css coverage', async ({ twoPages, browserName }) => {
|
|
test.skip(browserName !== 'chromium');
|
|
|
|
const { pageA, pageB } = twoPages;
|
|
await pageA.coverage.startCSSCoverage();
|
|
const error = await pageB.coverage.startCSSCoverage().catch(e => e);
|
|
expect(error.message).toContain('CSSCoverage is already enabled');
|
|
|
|
// Should cleanup coverage on disconnect and allow another client to start it.
|
|
await disconnect(pageA);
|
|
await pageB.coverage.startCSSCoverage();
|
|
});
|
|
|
|
test('should unpause clock', async ({ twoPages }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
await pageA.clock.install({ time: 1000 });
|
|
await pageA.clock.pauseAt(2000);
|
|
const promise = pageB.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
await disconnect(pageA);
|
|
await promise;
|
|
});
|
|
|
|
test('last emulateMedia wins', async ({ twoPages }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
await pageA.emulateMedia({ media: 'print' });
|
|
expect(await pageB.evaluate(() => window.matchMedia('screen').matches)).toBe(false);
|
|
expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(true);
|
|
await pageB.emulateMedia({ media: 'screen' });
|
|
expect(await pageB.evaluate(() => window.matchMedia('screen').matches)).toBe(true);
|
|
expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(false);
|
|
});
|
|
|
|
test('should chain routes', async ({ twoPages, server }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-foo</div>'));
|
|
server.setRoute('/bar', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-bar</div>'));
|
|
server.setRoute('/qux', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-qux</div>'));
|
|
|
|
let stall = false;
|
|
let stallCallback;
|
|
const stallPromise = new Promise(f => stallCallback = f);
|
|
await pageA.route('**/foo', async route => {
|
|
if (stall)
|
|
stallCallback();
|
|
else
|
|
await route.fallback();
|
|
});
|
|
await pageA.route('**/bar', async route => {
|
|
await route.fulfill({ body: '<div>intercepted-bar</div>', contentType: 'text/html' });
|
|
});
|
|
|
|
await pageB.route('**/foo', async route => {
|
|
await route.fulfill({ body: '<div>intercepted2-foo</div>', contentType: 'text/html' });
|
|
});
|
|
await pageB.route('**/bar', async route => {
|
|
await route.fulfill({ body: '<div>intercepted2-bar</div>', contentType: 'text/html' });
|
|
});
|
|
await pageB.route('**/qux', async route => {
|
|
await route.fulfill({ body: '<div>intercepted2-qux</div>', contentType: 'text/html' });
|
|
});
|
|
|
|
await pageA.goto(server.PREFIX + '/foo');
|
|
await expect(pageB.locator('div')).toHaveText('intercepted2-foo');
|
|
|
|
await pageA.goto(server.PREFIX + '/bar');
|
|
await expect(pageB.locator('div')).toHaveText('intercepted-bar');
|
|
|
|
await pageA.goto(server.PREFIX + '/qux');
|
|
await expect(pageB.locator('div')).toHaveText('intercepted2-qux');
|
|
|
|
stall = true;
|
|
const gotoPromise = pageB.goto(server.PREFIX + '/foo');
|
|
await stallPromise;
|
|
await pageA.context().browser().close();
|
|
|
|
await gotoPromise;
|
|
await expect(pageB.locator('div')).toHaveText('intercepted2-foo');
|
|
|
|
await pageB.goto(server.PREFIX + '/bar');
|
|
await expect(pageB.locator('div')).toHaveText('intercepted2-bar');
|
|
});
|
|
|
|
test.fixme('should chain routes with changed url', async ({ twoPages, server }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-foo</div>'));
|
|
server.setRoute('/baz', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-baz</div>'));
|
|
|
|
await pageA.route('**/foo', async route => {
|
|
await route.fallback({ url: server.PREFIX + '/baz' });
|
|
});
|
|
await pageB.route('**/baz', async route => {
|
|
await route.fulfill({ body: '<div>intercepted2-baz</div>', contentType: 'text/html' });
|
|
});
|
|
|
|
await pageA.goto(server.PREFIX + '/foo');
|
|
await expect(pageB.locator('div')).toHaveText('intercepted2-baz');
|
|
});
|
|
|
|
test('should remove exposed bindings upon disconnect', async ({ twoPages }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
await pageA.exposeBinding('pageBindingA', () => 'pageBindingAResult');
|
|
await pageA.evaluate(() => {
|
|
(window as any).pageBindingACopy = (window as any).pageBindingA;
|
|
});
|
|
expect(await pageB.evaluate(() => (window as any).pageBindingA())).toBe('pageBindingAResult');
|
|
expect(await pageB.evaluate(() => !!(window as any).pageBindingACopy)).toBe(true);
|
|
|
|
await pageA.context().exposeBinding('contextBindingA', () => 'contextBindingAResult');
|
|
expect(await pageB.evaluate(() => (window as any).contextBindingA())).toBe('contextBindingAResult');
|
|
|
|
await pageB.exposeBinding('pageBindingB', () => 'pageBindingBResult');
|
|
expect(await pageA.evaluate(() => (window as any).pageBindingB())).toBe('pageBindingBResult');
|
|
await pageB.context().exposeBinding('contextBindingB', () => 'contextBindingBResult');
|
|
expect(await pageA.evaluate(() => (window as any).contextBindingB())).toBe('contextBindingBResult');
|
|
|
|
await disconnect(pageA);
|
|
|
|
expect(await pageB.evaluate(() => (window as any).pageBindingA)).toBe(undefined);
|
|
expect(await pageB.evaluate(() => (window as any).contextBindingA)).toBe(undefined);
|
|
const error = await pageB.evaluate(() => (window as any).pageBindingACopy()).catch(e => e);
|
|
expect(error.message).toContain('binding "pageBindingA" has been removed');
|
|
|
|
expect(await pageB.evaluate(() => (window as any).pageBindingB())).toBe('pageBindingBResult');
|
|
});
|
|
|
|
test('should remove init scripts upon disconnect', async ({ twoPages, server }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
await pageA.addInitScript(() => (window as any).pageValueA = 'pageValueA');
|
|
await pageA.context().addInitScript(() => (window as any).contextValueA = 'contextValueA');
|
|
await pageB.goto(server.EMPTY_PAGE);
|
|
expect(await pageB.evaluate(() => (window as any).pageValueA)).toBe('pageValueA');
|
|
expect(await pageB.evaluate(() => (window as any).contextValueA)).toBe('contextValueA');
|
|
|
|
await pageB.addInitScript(() => (window as any).pageValueB = 'pageValueB');
|
|
await pageB.context().addInitScript(() => (window as any).contextValueB = 'contextValueB');
|
|
await pageA.goto(server.EMPTY_PAGE);
|
|
expect(await pageA.evaluate(() => (window as any).pageValueB)).toBe('pageValueB');
|
|
expect(await pageA.evaluate(() => (window as any).contextValueB)).toBe('contextValueB');
|
|
|
|
await disconnect(pageB);
|
|
|
|
await pageA.goto(server.EMPTY_PAGE);
|
|
expect(await pageA.evaluate(() => (window as any).pageValueB)).toBe(undefined);
|
|
expect(await pageA.evaluate(() => (window as any).contextValueB)).toBe(undefined);
|
|
expect(await pageA.evaluate(() => (window as any).pageValueA)).toBe('pageValueA');
|
|
expect(await pageA.evaluate(() => (window as any).contextValueA)).toBe('contextValueA');
|
|
});
|
|
|
|
test('should remove locator handlers upon disconnect', async ({ twoPages, server }) => {
|
|
const { pageA, pageB } = twoPages;
|
|
|
|
await pageA.goto(server.PREFIX + '/input/handle-locator.html');
|
|
|
|
let count = 0;
|
|
await pageA.addLocatorHandler(pageA.getByText('This interstitial covers the button'), async () => {
|
|
++count;
|
|
await pageA.locator('#close').click();
|
|
});
|
|
|
|
await pageA.locator('#aside').hover();
|
|
await pageA.evaluate(() => {
|
|
(window as any).clicked = 0;
|
|
(window as any).setupAnnoyingInterstitial('mouseover', 1);
|
|
});
|
|
await pageA.locator('#target').click();
|
|
expect(count).toBe(1);
|
|
expect(await pageB.evaluate('window.clicked')).toBe(1);
|
|
await expect(pageB.locator('#interstitial')).not.toBeVisible();
|
|
|
|
await pageA.locator('#aside').hover();
|
|
await pageA.evaluate(() => {
|
|
(window as any).clicked = 0;
|
|
(window as any).setupAnnoyingInterstitial('mouseover', 1);
|
|
});
|
|
|
|
await disconnect(pageA);
|
|
|
|
const error = await pageB.locator('#target').click({ timeout: 3000 }).catch(e => e);
|
|
expect(error.message).toContain('Timeout 3000ms exceeded');
|
|
expect(error.message).toContain('intercepts pointer events');
|
|
expect(error.message).not.toContain('locator handler');
|
|
});
|