mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
chore(ui): make ui-side drive things (#21398)
This commit is contained in:
parent
0ebe090b8c
commit
f0cd123a45
@ -40,17 +40,13 @@ export class TaskRunner<Context> {
|
||||
this._tasks.push({ name, task });
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._interrupted = true;
|
||||
}
|
||||
|
||||
async run(context: Context, deadline: number): Promise<FullResult['status']> {
|
||||
const { status, cleanup } = await this.runDeferCleanup(context, deadline);
|
||||
async run(context: Context, deadline: number, cancelPromise?: ManualPromise<void>): Promise<FullResult['status']> {
|
||||
const { status, cleanup } = await this.runDeferCleanup(context, deadline, cancelPromise);
|
||||
const teardownStatus = await cleanup();
|
||||
return status === 'passed' ? teardownStatus : status;
|
||||
}
|
||||
|
||||
async runDeferCleanup(context: Context, deadline: number): Promise<{ status: FullResult['status'], cleanup: () => Promise<FullResult['status']> }> {
|
||||
async runDeferCleanup(context: Context, deadline: number, cancelPromise = new ManualPromise<void>()): Promise<{ status: FullResult['status'], cleanup: () => Promise<FullResult['status']> }> {
|
||||
const sigintWatcher = new SigIntWatcher();
|
||||
const timeoutWatcher = new TimeoutWatcher(deadline);
|
||||
const teardownRunner = new TaskRunner(this._reporter, this._globalTimeoutForError);
|
||||
@ -87,6 +83,7 @@ export class TaskRunner<Context> {
|
||||
|
||||
await Promise.race([
|
||||
taskLoop(),
|
||||
cancelPromise,
|
||||
sigintWatcher.promise(),
|
||||
timeoutWatcher.promise,
|
||||
]);
|
||||
@ -98,7 +95,7 @@ export class TaskRunner<Context> {
|
||||
this._interrupted = true;
|
||||
|
||||
let status: FullResult['status'] = 'passed';
|
||||
if (sigintWatcher.hadSignal()) {
|
||||
if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) {
|
||||
status = 'interrupted';
|
||||
} else if (timeoutWatcher.timedOut()) {
|
||||
this._reporter.onError?.({ message: `Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run` });
|
||||
@ -106,7 +103,7 @@ export class TaskRunner<Context> {
|
||||
} else if (this._hasErrors) {
|
||||
status = 'failed';
|
||||
}
|
||||
|
||||
cancelPromise?.resolve();
|
||||
const cleanup = () => teardownRunner.runDeferCleanup(context, deadline).then(r => r.status);
|
||||
return { status, cleanup };
|
||||
}
|
||||
|
@ -14,112 +14,144 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FullResult } from 'packages/playwright-test/reporter';
|
||||
import { showTraceViewer } from 'playwright-core/lib/server';
|
||||
import type { Page } from 'playwright-core/lib/server/page';
|
||||
import { showTraceViewer, serverSideCallMetadata } from 'playwright-core/lib/server';
|
||||
import { clearCompilationCache } from '../common/compilationCache';
|
||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
||||
import type { FullResult } from '../../reporter';
|
||||
import { clearCompilationCache, dependenciesForTestFile } from '../common/compilationCache';
|
||||
import type { FullConfigInternal } from '../common/types';
|
||||
import ListReporter from '../reporters/list';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
||||
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import type { TaskRunnerState } from './tasks';
|
||||
import { createReporter } from './reporters';
|
||||
import type { TaskRunnerState } from './tasks';
|
||||
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import { chokidar } from '../utilsBundle';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export async function runUIMode(config: FullConfigInternal): Promise<FullResult['status']> {
|
||||
// Reset the settings that don't apply to watch.
|
||||
config._internal.passWithNoTests = true;
|
||||
for (const p of config.projects)
|
||||
p.retries = 0;
|
||||
class UIMode {
|
||||
private _config: FullConfigInternal;
|
||||
private _page!: Page;
|
||||
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
||||
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
||||
private _watcher: FSWatcher | undefined;
|
||||
private _watchTestFile: string | undefined;
|
||||
|
||||
{
|
||||
// Global setup.
|
||||
const reporter = await createReporter(config, 'watch');
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
|
||||
reporter.onConfigure(config);
|
||||
const context: TaskRunnerState = {
|
||||
config,
|
||||
reporter,
|
||||
phases: [],
|
||||
};
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
|
||||
if (status !== 'passed')
|
||||
return await globalCleanup();
|
||||
constructor(config: FullConfigInternal) {
|
||||
this._config = config;
|
||||
config._internal.passWithNoTests = true;
|
||||
for (const p of config.projects)
|
||||
p.retries = 0;
|
||||
config._internal.configCLIOverrides.use = config._internal.configCLIOverrides.use || {};
|
||||
config._internal.configCLIOverrides.use.trace = 'on';
|
||||
}
|
||||
|
||||
// Show trace viewer.
|
||||
const page = await showTraceViewer([], 'chromium', { watchMode: true });
|
||||
await page.mainFrame()._waitForFunctionExpression(serverSideCallMetadata(), '!!window.dispatch', false, undefined, { timeout: 0 });
|
||||
{
|
||||
// List
|
||||
const controller = new Controller(config, page);
|
||||
const listReporter = new TeleReporterEmitter(message => controller!.send(message));
|
||||
const reporter = new Multiplexer([listReporter]);
|
||||
const taskRunner = createTaskRunnerForList(config, reporter);
|
||||
async runGlobalSetup(): Promise<FullResult['status']> {
|
||||
const reporter = await createReporter(this._config, 'watch');
|
||||
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
||||
reporter.onConfigure(this._config);
|
||||
const context: TaskRunnerState = {
|
||||
config,
|
||||
config: this._config,
|
||||
reporter,
|
||||
phases: [],
|
||||
};
|
||||
reporter.onConfigure(config);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
|
||||
if (status !== 'passed')
|
||||
return await globalCleanup();
|
||||
if (status !== 'passed') {
|
||||
await globalCleanup();
|
||||
return status;
|
||||
}
|
||||
this.globalCleanup = globalCleanup;
|
||||
return status;
|
||||
}
|
||||
|
||||
async showUI() {
|
||||
this._page = await showTraceViewer([], 'chromium', { watchMode: true });
|
||||
const exitPromise = new ManualPromise();
|
||||
this._page.on('close', () => exitPromise.resolve());
|
||||
this._page.exposeBinding('sendMessage', false, async (source, data) => {
|
||||
const { method, params }: { method: string, params: any } = data;
|
||||
if (method === 'list')
|
||||
await this._listTests();
|
||||
if (method === 'run')
|
||||
await this._runTests(params.testIds);
|
||||
if (method === 'stop')
|
||||
this._stopTests();
|
||||
if (method === 'watch')
|
||||
this._watchFile(params.fileName);
|
||||
if (method === 'exit')
|
||||
exitPromise.resolve();
|
||||
});
|
||||
await exitPromise;
|
||||
}
|
||||
|
||||
private _dispatchEvent(message: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
this._page.mainFrame().evaluateExpression(dispatchFuncSource, true, message).catch(e => console.log(e));
|
||||
}
|
||||
|
||||
private async _listTests() {
|
||||
const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e));
|
||||
const reporter = new Multiplexer([listReporter]);
|
||||
const taskRunner = createTaskRunnerForList(this._config, reporter);
|
||||
const context: TaskRunnerState = { config: this._config, reporter, phases: [] };
|
||||
reporter.onConfigure(this._config);
|
||||
await taskRunner.run(context, 0);
|
||||
}
|
||||
|
||||
await new Promise(() => {});
|
||||
// TODO: implement watch queue with the sigint watcher and global teardown.
|
||||
return 'passed';
|
||||
}
|
||||
private async _runTests(testIds: string[]) {
|
||||
await this._stopTests();
|
||||
|
||||
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
||||
this._config._internal.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
||||
|
||||
class Controller {
|
||||
private _page: Page;
|
||||
private _queue = Promise.resolve();
|
||||
private _runReporter: TeleReporterEmitter;
|
||||
const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e));
|
||||
const reporter = new Multiplexer([new ListReporter(), runReporter]);
|
||||
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
||||
const context: TaskRunnerState = { config: this._config, reporter, phases: [] };
|
||||
clearCompilationCache();
|
||||
reporter.onConfigure(this._config);
|
||||
const stop = new ManualPromise();
|
||||
const run = taskRunner.run(context, 0, stop).then(async status => {
|
||||
await reporter.onExit({ status });
|
||||
return status;
|
||||
});
|
||||
this._testRun = { run, stop };
|
||||
await run;
|
||||
this._testRun = undefined;
|
||||
}
|
||||
|
||||
constructor(config: FullConfigInternal, page: Page) {
|
||||
this._page = page;
|
||||
this._runReporter = new TeleReporterEmitter(message => this!.send(message));
|
||||
this._page.exposeBinding('binding', false, (source, data) => {
|
||||
const { method, params } = data;
|
||||
if (method === 'run') {
|
||||
const { location, testIds } = params;
|
||||
if (location)
|
||||
config._internal.cliArgs = [location];
|
||||
if (testIds) {
|
||||
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
||||
config._internal.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
||||
}
|
||||
this._queue = this._queue.then(() => runTests(config, this._runReporter));
|
||||
return this._queue;
|
||||
}
|
||||
private async _watchFile(fileName: string) {
|
||||
if (this._watchTestFile === fileName)
|
||||
return;
|
||||
if (this._watcher)
|
||||
await this._watcher.close();
|
||||
this._watchTestFile = fileName;
|
||||
if (!fileName)
|
||||
return;
|
||||
|
||||
const files = [fileName, ...dependenciesForTestFile(fileName)];
|
||||
this._watcher = chokidar.watch(files, { ignoreInitial: true }).on('all', async (event, file) => {
|
||||
if (event !== 'add' && event !== 'change')
|
||||
return;
|
||||
this._dispatchEvent({ method: 'fileChanged', params: { fileName: file } });
|
||||
});
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
const func = (message: any) => {
|
||||
(window as any).dispatch(message);
|
||||
};
|
||||
// eslint-disable-next-line no-console
|
||||
this._page.mainFrame().evaluateExpression(String(func), true, message).catch(e => console.log(e));
|
||||
private async _stopTests() {
|
||||
this._testRun?.stop?.resolve();
|
||||
await this._testRun?.run;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests(config: FullConfigInternal, teleReporter: TeleReporterEmitter) {
|
||||
const reporter = new Multiplexer([new ListReporter(), teleReporter]);
|
||||
config._internal.configCLIOverrides.use = config._internal.configCLIOverrides.use || {};
|
||||
config._internal.configCLIOverrides.use.trace = 'on';
|
||||
const dispatchFuncSource = String((message: any) => {
|
||||
(window as any).dispatch(message);
|
||||
});
|
||||
|
||||
const taskRunner = createTaskRunnerForWatch(config, reporter);
|
||||
const context: TaskRunnerState = {
|
||||
config,
|
||||
reporter,
|
||||
phases: [],
|
||||
};
|
||||
clearCompilationCache();
|
||||
reporter.onConfigure(config);
|
||||
const status = await taskRunner.run(context, 0);
|
||||
await reporter.onExit({ status });
|
||||
export async function runUIMode(config: FullConfigInternal): Promise<FullResult['status']> {
|
||||
const uiMode = new UIMode(config);
|
||||
const status = await uiMode.runGlobalSetup();
|
||||
if (status !== 'passed')
|
||||
return status;
|
||||
await uiMode.showUI();
|
||||
return await uiMode.globalCleanup?.() || 'passed';
|
||||
}
|
||||
|
@ -23,7 +23,11 @@
|
||||
}
|
||||
|
||||
.watch-mode-sidebar .toolbar-button:not([disabled]) .codicon-play {
|
||||
color: var(--vscode-testing-runAction);
|
||||
color: var(--vscode-debugIcon-restartForeground);
|
||||
}
|
||||
|
||||
.watch-mode-sidebar .toolbar-button:not([disabled]) .codicon-debug-stop {
|
||||
color: var(--vscode-debugIcon-stopForeground);
|
||||
}
|
||||
|
||||
.watch-mode-list-item {
|
||||
|
@ -25,11 +25,13 @@ import { SplitView } from '@web/components/splitView';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import './watchMode.css';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
|
||||
let rootSuite: Suite | undefined;
|
||||
|
||||
let updateList: () => void = () => {};
|
||||
let updateProgress: () => void = () => {};
|
||||
let runWatchedTests = () => {};
|
||||
|
||||
type Entry = { test?: TestCase, fileSuite: Suite };
|
||||
|
||||
@ -47,8 +49,15 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
sendMessageNoReply('list');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
sendMessageNoReply('watch', {
|
||||
fileName: selectedFileSuite?.location?.file || selectedTest?.location?.file
|
||||
});
|
||||
}, [selectedFileSuite, selectedTest]);
|
||||
|
||||
const selectedOrDefaultFileSuite = selectedFileSuite || rootSuite?.suites?.[0]?.suites?.[0];
|
||||
const tests: TestCase[] = [];
|
||||
const fileSuites: Suite[] = [];
|
||||
@ -93,8 +102,16 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
const runEntry = (entry: Entry) => {
|
||||
expandedFiles.set(entry.fileSuite, true);
|
||||
setSelectedTest(entry.test);
|
||||
runTests(collectTestIds(entry));
|
||||
};
|
||||
|
||||
runWatchedTests = () => {
|
||||
runTests(collectTestIds({ test: selectedTest, fileSuite: selectedFileSuite || selectedTest!.parent }));
|
||||
};
|
||||
|
||||
const runTests = (testIds: string[] | undefined) => {
|
||||
setIsRunningTest(true);
|
||||
runTests(entry.test ? entry.test.location.file + ':' + entry.test.location.line : entry.fileSuite.title, undefined).then(() => {
|
||||
sendMessage('run', { testIds }).then(() => {
|
||||
setIsRunningTest(false);
|
||||
});
|
||||
};
|
||||
@ -103,20 +120,18 @@ export const WatchModeView: React.FC<{}> = ({
|
||||
return <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||
<TraceView test={selectedTest} isRunningTest={isRunningTest}></TraceView>
|
||||
<div className='vbox watch-mode-sidebar'>
|
||||
<div style={{ flex: 'none', display: 'flex', padding: 4 }}>
|
||||
<Toolbar>
|
||||
<input ref={inputRef} type='search' placeholder='Filter tests' spellCheck={false} value={filterText}
|
||||
onChange={e => {
|
||||
setFilterText(e.target.value);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setIsRunningTest(true);
|
||||
runTests(undefined, [...visibleTestIds]).then(() => {
|
||||
setIsRunningTest(false);
|
||||
});
|
||||
}
|
||||
if (e.key === 'Enter')
|
||||
runTests([...visibleTestIds]);
|
||||
}}></input>
|
||||
</div>
|
||||
<ToolbarButton icon='play' title='Run' onClick={() => runTests([...visibleTestIds])} disabled={isRunningTest}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
||||
</Toolbar>
|
||||
<ListView
|
||||
items={[...entries.values()]}
|
||||
itemKey={(entry: Entry) => entry.test ? entry.test!.id : entry.fileSuite.title }
|
||||
@ -264,12 +279,28 @@ const receiver = new TeleReporterReceiver({
|
||||
|
||||
|
||||
(window as any).dispatch = (message: any) => {
|
||||
receiver.dispatch(message);
|
||||
if (message.method === 'fileChanged')
|
||||
runWatchedTests();
|
||||
else
|
||||
receiver.dispatch(message);
|
||||
};
|
||||
|
||||
async function runTests(location: string | undefined, testIds: string[] | undefined): Promise<void> {
|
||||
await (window as any).binding({
|
||||
method: 'run',
|
||||
params: { location, testIds }
|
||||
const sendMessage = async (method: string, params: any) => {
|
||||
await (window as any).sendMessage({ method, params });
|
||||
};
|
||||
|
||||
const sendMessageNoReply = (method: string, params?: any) => {
|
||||
sendMessage(method, params).catch((e: Error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const collectTestIds = (entry: Entry): string[] => {
|
||||
const testIds: string[] = [];
|
||||
if (entry.test)
|
||||
testIds.push(entry.test.id);
|
||||
else
|
||||
entry.fileSuite.allTests().forEach(test => testIds.push(test.id));
|
||||
return testIds;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user