fix(ui-mode): run teardown handlers with Command + C (#24267)
Fixes https://github.com/microsoft/playwright/issues/23907
This commit is contained in:
parent
1fdd7541e0
commit
1288519915
|
|
@ -25,6 +25,7 @@ import { createPlaywright } from '../../playwright';
|
||||||
import { ProgressController } from '../../progress';
|
import { ProgressController } from '../../progress';
|
||||||
import { open, wsServer } from '../../../utilsBundle';
|
import { open, wsServer } from '../../../utilsBundle';
|
||||||
import type { Page } from '../../page';
|
import type { Page } from '../../page';
|
||||||
|
import type { BrowserType } from '../../browserType';
|
||||||
|
|
||||||
export type Transport = {
|
export type Transport = {
|
||||||
sendEvent?: (method: string, params: any) => void;
|
sendEvent?: (method: string, params: any) => void;
|
||||||
|
|
@ -40,6 +41,7 @@ export type OpenTraceViewerOptions = {
|
||||||
port?: number;
|
port?: number;
|
||||||
isServer?: boolean;
|
isServer?: boolean;
|
||||||
transport?: Transport;
|
transport?: Transport;
|
||||||
|
persistentContextOptions?: Parameters<BrowserType['launchPersistentContext']>[2];
|
||||||
};
|
};
|
||||||
|
|
||||||
async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceViewerOptions): Promise<{ server: HttpServer, url: string }> {
|
async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceViewerOptions): Promise<{ server: HttpServer, url: string }> {
|
||||||
|
|
@ -144,6 +146,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
|
||||||
ignoreDefaultArgs: ['--enable-automation'],
|
ignoreDefaultArgs: ['--enable-automation'],
|
||||||
colorScheme: 'no-override',
|
colorScheme: 'no-override',
|
||||||
useWebSocket: isUnderTest(),
|
useWebSocket: isUnderTest(),
|
||||||
|
...options?.persistentContextOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller = new ProgressController(serverSideCallMetadata(), context._browser);
|
const controller = new ProgressController(serverSideCallMetadata(), context._browser);
|
||||||
|
|
@ -171,7 +174,8 @@ export async function openTraceInBrowser(traceUrls: string[], options?: OpenTrac
|
||||||
const { url } = await startTraceViewerServer(traceUrls, options);
|
const { url } = await startTraceViewerServer(traceUrls, options);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('\nListening on ' + url);
|
console.log('\nListening on ' + url);
|
||||||
await open(url).catch(() => {});
|
if (!isUnderTest())
|
||||||
|
await open(url).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
class StdinServer implements Transport {
|
class StdinServer implements Transport {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { open } from 'playwright-core/lib/utilsBundle';
|
||||||
import ListReporter from '../reporters/list';
|
import ListReporter from '../reporters/list';
|
||||||
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
||||||
import { Multiplexer } from '../reporters/multiplexer';
|
import { Multiplexer } from '../reporters/multiplexer';
|
||||||
|
import { SigIntWatcher } from './sigIntWatcher';
|
||||||
|
|
||||||
class UIMode {
|
class UIMode {
|
||||||
private _config: FullConfigInternal;
|
private _config: FullConfigInternal;
|
||||||
|
|
@ -79,7 +80,7 @@ class UIMode {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async showUI(options: { host?: string, port?: number }) {
|
async showUI(options: { host?: string, port?: number }, cancelPromise: ManualPromise<void>) {
|
||||||
let queue = Promise.resolve();
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
this._transport = {
|
this._transport = {
|
||||||
|
|
@ -118,13 +119,15 @@ class UIMode {
|
||||||
transport: this._transport,
|
transport: this._transport,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
port: options.port,
|
port: options.port,
|
||||||
|
persistentContextOptions: {
|
||||||
|
handleSIGINT: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const exitPromise = new ManualPromise<void>();
|
|
||||||
if (options.host !== undefined || options.port !== undefined) {
|
if (options.host !== undefined || options.port !== undefined) {
|
||||||
await openTraceInBrowser([], openOptions);
|
await openTraceInBrowser([], openOptions);
|
||||||
} else {
|
} else {
|
||||||
const page = await openTraceViewerApp([], 'chromium', openOptions);
|
const page = await openTraceViewerApp([], 'chromium', openOptions);
|
||||||
page.on('close', () => exitPromise.resolve());
|
page.on('close', () => cancelPromise.resolve());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.PWTEST_DEBUG) {
|
if (!process.env.PWTEST_DEBUG) {
|
||||||
|
|
@ -137,7 +140,7 @@ class UIMode {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await exitPromise;
|
await cancelPromise;
|
||||||
|
|
||||||
if (!process.env.PWTEST_DEBUG) {
|
if (!process.env.PWTEST_DEBUG) {
|
||||||
process.stdout.write = this._originalStdoutWrite;
|
process.stdout.write = this._originalStdoutWrite;
|
||||||
|
|
@ -218,11 +221,18 @@ class UIMode {
|
||||||
|
|
||||||
export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
||||||
const uiMode = new UIMode(config);
|
const uiMode = new UIMode(config);
|
||||||
const status = await uiMode.runGlobalSetup();
|
const globalSetupStatus = await uiMode.runGlobalSetup();
|
||||||
if (status !== 'passed')
|
if (globalSetupStatus !== 'passed')
|
||||||
return status;
|
return globalSetupStatus;
|
||||||
await uiMode.showUI(options);
|
const cancelPromise = new ManualPromise<void>();
|
||||||
return await uiMode.globalCleanup?.() || 'passed';
|
const sigintWatcher = new SigIntWatcher();
|
||||||
|
void sigintWatcher.promise().then(() => cancelPromise.resolve());
|
||||||
|
try {
|
||||||
|
await uiMode.showUI(options, cancelPromise);
|
||||||
|
} finally {
|
||||||
|
sigintWatcher.disarm();
|
||||||
|
}
|
||||||
|
return await uiMode.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
type StdioPayload = {
|
type StdioPayload = {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import type { TestChildProcess } from '../config/commonFixtures';
|
||||||
import { rimraf } from '../../packages/playwright-core/lib/utilsBundle';
|
import { rimraf } from '../../packages/playwright-core/lib/utilsBundle';
|
||||||
import { cleanEnv, cliEntrypoint, test as base, writeFiles } from './playwright-test-fixtures';
|
import { cleanEnv, cliEntrypoint, test as base, writeFiles } from './playwright-test-fixtures';
|
||||||
import type { Files, RunOptions } from './playwright-test-fixtures';
|
import type { Files, RunOptions } from './playwright-test-fixtures';
|
||||||
import type { Browser, Page, TestInfo } from './stable-test-runner';
|
import type { Browser, BrowserType, Page, TestInfo } from './stable-test-runner';
|
||||||
import { createGuid } from '../../packages/playwright-core/src/utils/crypto';
|
import { createGuid } from '../../packages/playwright-core/src/utils/crypto';
|
||||||
|
|
||||||
type Latch = {
|
type Latch = {
|
||||||
|
|
@ -30,8 +30,12 @@ type Latch = {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UIModeOptions = RunOptions & {
|
||||||
|
useWeb?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
type Fixtures = {
|
type Fixtures = {
|
||||||
runUITest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<{ page: Page, testProcess: TestChildProcess }>;
|
runUITest: (files: Files, env?: NodeJS.ProcessEnv, options?: UIModeOptions) => Promise<{ page: Page, testProcess: TestChildProcess }>;
|
||||||
createLatch: () => Latch;
|
createLatch: () => Latch;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,16 +86,16 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () =
|
||||||
|
|
||||||
export const test = base
|
export const test = base
|
||||||
.extend<Fixtures>({
|
.extend<Fixtures>({
|
||||||
runUITest: async ({ childProcess, playwright, headless }, use, testInfo: TestInfo) => {
|
runUITest: async ({ childProcess, headless }, use, testInfo: TestInfo) => {
|
||||||
if (process.env.CI)
|
if (process.env.CI)
|
||||||
testInfo.slow();
|
testInfo.slow();
|
||||||
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
|
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
|
||||||
let testProcess: TestChildProcess | undefined;
|
let testProcess: TestChildProcess | undefined;
|
||||||
let browser: Browser | undefined;
|
let browser: Browser | undefined;
|
||||||
await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {
|
await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: UIModeOptions = {}) => {
|
||||||
const baseDir = await writeFiles(testInfo, files, true);
|
const baseDir = await writeFiles(testInfo, files, true);
|
||||||
testProcess = childProcess({
|
testProcess = childProcess({
|
||||||
command: ['node', cliEntrypoint, 'test', '--ui', '--workers=1', ...(options.additionalArgs || [])],
|
command: ['node', cliEntrypoint, 'test', (options.useWeb ? '--ui-host=127.0.0.1' : '--ui'), '--workers=1', ...(options.additionalArgs || [])],
|
||||||
env: {
|
env: {
|
||||||
...cleanEnv(env),
|
...cleanEnv(env),
|
||||||
PWTEST_UNDER_TEST: '1',
|
PWTEST_UNDER_TEST: '1',
|
||||||
|
|
@ -101,12 +105,25 @@ export const test = base
|
||||||
},
|
},
|
||||||
cwd: options.cwd ? path.resolve(baseDir, options.cwd) : baseDir,
|
cwd: options.cwd ? path.resolve(baseDir, options.cwd) : baseDir,
|
||||||
});
|
});
|
||||||
await testProcess.waitForOutput('DevTools listening on');
|
let page: Page;
|
||||||
const line = testProcess.output.split('\n').find(l => l.includes('DevTools listening on'));
|
// We want to have ToT playwright-core here, since we install it's browsers and otherwise
|
||||||
const wsEndpoint = line!.split(' ')[3];
|
// don't have the right browser revision (ToT revisions != stable-test-runner revisions).
|
||||||
browser = await playwright.chromium.connectOverCDP(wsEndpoint);
|
const chromium: BrowserType = require('../../packages/playwright-core').chromium;
|
||||||
const [context] = browser.contexts();
|
if (options.useWeb) {
|
||||||
const [page] = context.pages();
|
await testProcess.waitForOutput('Listening on');
|
||||||
|
const line = testProcess.output.split('\n').find(l => l.includes('Listening on'));
|
||||||
|
const uiAddress = line!.split(' ')[2];
|
||||||
|
browser = await chromium.launch();
|
||||||
|
page = await browser.newPage();
|
||||||
|
await page.goto(uiAddress);
|
||||||
|
} else {
|
||||||
|
await testProcess.waitForOutput('DevTools listening on');
|
||||||
|
const line = testProcess.output.split('\n').find(l => l.includes('DevTools listening on'));
|
||||||
|
const wsEndpoint = line!.split(' ')[3];
|
||||||
|
browser = await chromium.connectOverCDP(wsEndpoint);
|
||||||
|
const [context] = browser.contexts();
|
||||||
|
[page] = context.pages();
|
||||||
|
}
|
||||||
return { page, testProcess };
|
return { page, testProcess };
|
||||||
});
|
});
|
||||||
await browser?.close();
|
await browser?.close();
|
||||||
|
|
|
||||||
|
|
@ -193,3 +193,37 @@ test('should run part of the setup only', async ({ runUITest }) => {
|
||||||
◯ test
|
◯ test
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const useWeb of [true, false]) {
|
||||||
|
test.describe(`web-mode: ${useWeb}`, () => {
|
||||||
|
test('should run teardown with SIGINT', async ({ runUITest }) => {
|
||||||
|
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||||
|
const { page, testProcess } = await runUITest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
export default defineConfig({
|
||||||
|
globalTeardown: './globalTeardown.ts',
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'globalTeardown.ts': `
|
||||||
|
export default async () => {
|
||||||
|
console.log('\\n%%from-global-teardown0000')
|
||||||
|
await new Promise(f => setTimeout(f, 3000));
|
||||||
|
console.log('\\n%%from-global-teardown3000')
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.js': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('should work', async ({}) => {});
|
||||||
|
`
|
||||||
|
}, null, { useWeb });
|
||||||
|
await page.getByTitle('Run all').click();
|
||||||
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
|
await testProcess.kill('SIGINT');
|
||||||
|
await expect.poll(() => testProcess.outputLines()).toEqual([
|
||||||
|
'from-global-teardown0000',
|
||||||
|
'from-global-teardown3000',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue