423 lines
15 KiB
JavaScript
423 lines
15 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
Object.defineProperty(exports, "__esModule", {
|
||
|
value: true
|
||
|
});
|
||
|
exports.runWatchModeLoop = runWatchModeLoop;
|
||
|
var _readline = _interopRequireDefault(require("readline"));
|
||
|
var _path = _interopRequireDefault(require("path"));
|
||
|
var _utils = require("playwright-core/lib/utils");
|
||
|
var _utilsBundle = require("playwright-core/lib/utilsBundle");
|
||
|
var _utilsBundle2 = require("../utilsBundle");
|
||
|
var _base = require("../reporters/base");
|
||
|
var _playwrightServer = require("playwright-core/lib/remote/playwrightServer");
|
||
|
var _testServer = require("./testServer");
|
||
|
var _stream = require("stream");
|
||
|
var _testServerConnection = require("../isomorphic/testServerConnection");
|
||
|
var _teleSuiteUpdater = require("../isomorphic/teleSuiteUpdater");
|
||
|
var _configLoader = require("../common/configLoader");
|
||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||
|
/**
|
||
|
* Copyright 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.
|
||
|
*/
|
||
|
|
||
|
class InMemoryTransport extends _stream.EventEmitter {
|
||
|
constructor(send) {
|
||
|
super();
|
||
|
this._send = void 0;
|
||
|
this._send = send;
|
||
|
}
|
||
|
close() {
|
||
|
this.emit('close');
|
||
|
}
|
||
|
onclose(listener) {
|
||
|
this.on('close', listener);
|
||
|
}
|
||
|
onerror(listener) {
|
||
|
// no-op to fulfil the interface, the user of InMemoryTransport doesn't emit any errors.
|
||
|
}
|
||
|
onmessage(listener) {
|
||
|
this.on('message', listener);
|
||
|
}
|
||
|
onopen(listener) {
|
||
|
this.on('open', listener);
|
||
|
}
|
||
|
send(data) {
|
||
|
this._send(data);
|
||
|
}
|
||
|
}
|
||
|
async function runWatchModeLoop(configLocation, initialOptions) {
|
||
|
if ((0, _configLoader.restartWithExperimentalTsEsm)(undefined, true)) return 'restarted';
|
||
|
const options = {
|
||
|
...initialOptions
|
||
|
};
|
||
|
let bufferMode = false;
|
||
|
const testServerDispatcher = new _testServer.TestServerDispatcher(configLocation, {});
|
||
|
const transport = new InMemoryTransport(async data => {
|
||
|
const {
|
||
|
id,
|
||
|
method,
|
||
|
params
|
||
|
} = JSON.parse(data);
|
||
|
try {
|
||
|
const result = await testServerDispatcher.transport.dispatch(method, params);
|
||
|
transport.emit('message', JSON.stringify({
|
||
|
id,
|
||
|
result
|
||
|
}));
|
||
|
} catch (e) {
|
||
|
transport.emit('message', JSON.stringify({
|
||
|
id,
|
||
|
error: String(e)
|
||
|
}));
|
||
|
}
|
||
|
});
|
||
|
testServerDispatcher.transport.sendEvent = (method, params) => {
|
||
|
transport.emit('message', JSON.stringify({
|
||
|
method,
|
||
|
params
|
||
|
}));
|
||
|
};
|
||
|
const testServerConnection = new _testServerConnection.TestServerConnection(transport);
|
||
|
transport.emit('open');
|
||
|
const teleSuiteUpdater = new _teleSuiteUpdater.TeleSuiteUpdater({
|
||
|
pathSeparator: _path.default.sep,
|
||
|
onUpdate() {}
|
||
|
});
|
||
|
const dirtyTestFiles = new Set();
|
||
|
const dirtyTestIds = new Set();
|
||
|
let onDirtyTests = new _utils.ManualPromise();
|
||
|
let queue = Promise.resolve();
|
||
|
const changedFiles = new Set();
|
||
|
testServerConnection.onTestFilesChanged(({
|
||
|
testFiles
|
||
|
}) => {
|
||
|
testFiles.forEach(file => changedFiles.add(file));
|
||
|
queue = queue.then(async () => {
|
||
|
if (changedFiles.size === 0) return;
|
||
|
const {
|
||
|
report
|
||
|
} = await testServerConnection.listTests({
|
||
|
locations: options.files,
|
||
|
projects: options.projects,
|
||
|
grep: options.grep
|
||
|
});
|
||
|
teleSuiteUpdater.processListReport(report);
|
||
|
for (const test of teleSuiteUpdater.rootSuite.allTests()) {
|
||
|
if (changedFiles.has(test.location.file)) {
|
||
|
dirtyTestFiles.add(test.location.file);
|
||
|
dirtyTestIds.add(test.id);
|
||
|
}
|
||
|
}
|
||
|
changedFiles.clear();
|
||
|
if (dirtyTestIds.size > 0) {
|
||
|
onDirtyTests.resolve('changed');
|
||
|
onDirtyTests = new _utils.ManualPromise();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report));
|
||
|
await testServerConnection.initialize({
|
||
|
interceptStdio: false,
|
||
|
watchTestDirs: true,
|
||
|
populateDependenciesOnList: true
|
||
|
});
|
||
|
await testServerConnection.runGlobalSetup({});
|
||
|
const {
|
||
|
report
|
||
|
} = await testServerConnection.listTests({});
|
||
|
teleSuiteUpdater.processListReport(report);
|
||
|
const projectNames = teleSuiteUpdater.rootSuite.suites.map(s => s.title);
|
||
|
let lastRun = {
|
||
|
type: 'regular'
|
||
|
};
|
||
|
let result = 'passed';
|
||
|
while (true) {
|
||
|
if (bufferMode) printBufferPrompt(dirtyTestFiles, teleSuiteUpdater.config.rootDir);else printPrompt();
|
||
|
const waitForCommand = readCommand();
|
||
|
const command = await Promise.race([onDirtyTests, waitForCommand.result]);
|
||
|
if (command === 'changed') waitForCommand.cancel();
|
||
|
if (bufferMode && command === 'changed') continue;
|
||
|
const shouldRunChangedFiles = bufferMode ? command === 'run' : command === 'changed';
|
||
|
if (shouldRunChangedFiles) {
|
||
|
if (dirtyTestIds.size === 0) continue;
|
||
|
const testIds = [...dirtyTestIds];
|
||
|
dirtyTestIds.clear();
|
||
|
dirtyTestFiles.clear();
|
||
|
await runTests(options, testServerConnection, {
|
||
|
testIds,
|
||
|
title: 'files changed'
|
||
|
});
|
||
|
lastRun = {
|
||
|
type: 'changed',
|
||
|
dirtyTestIds: testIds
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'run') {
|
||
|
// All means reset filters.
|
||
|
await runTests(options, testServerConnection);
|
||
|
lastRun = {
|
||
|
type: 'regular'
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'project') {
|
||
|
const {
|
||
|
selectedProjects
|
||
|
} = await _utilsBundle2.enquirer.prompt({
|
||
|
type: 'multiselect',
|
||
|
name: 'selectedProjects',
|
||
|
message: 'Select projects',
|
||
|
choices: projectNames
|
||
|
}).catch(() => ({
|
||
|
selectedProjects: null
|
||
|
}));
|
||
|
if (!selectedProjects) continue;
|
||
|
options.projects = selectedProjects.length ? selectedProjects : undefined;
|
||
|
await runTests(options, testServerConnection);
|
||
|
lastRun = {
|
||
|
type: 'regular'
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'file') {
|
||
|
const {
|
||
|
filePattern
|
||
|
} = await _utilsBundle2.enquirer.prompt({
|
||
|
type: 'text',
|
||
|
name: 'filePattern',
|
||
|
message: 'Input filename pattern (regex)'
|
||
|
}).catch(() => ({
|
||
|
filePattern: null
|
||
|
}));
|
||
|
if (filePattern === null) continue;
|
||
|
if (filePattern.trim()) options.files = filePattern.split(' ');else options.files = undefined;
|
||
|
await runTests(options, testServerConnection);
|
||
|
lastRun = {
|
||
|
type: 'regular'
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'grep') {
|
||
|
const {
|
||
|
testPattern
|
||
|
} = await _utilsBundle2.enquirer.prompt({
|
||
|
type: 'text',
|
||
|
name: 'testPattern',
|
||
|
message: 'Input test name pattern (regex)'
|
||
|
}).catch(() => ({
|
||
|
testPattern: null
|
||
|
}));
|
||
|
if (testPattern === null) continue;
|
||
|
if (testPattern.trim()) options.grep = testPattern;else options.grep = undefined;
|
||
|
await runTests(options, testServerConnection);
|
||
|
lastRun = {
|
||
|
type: 'regular'
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'failed') {
|
||
|
const failedTestIds = teleSuiteUpdater.rootSuite.allTests().filter(t => !t.ok()).map(t => t.id);
|
||
|
await runTests({}, testServerConnection, {
|
||
|
title: 'running failed tests',
|
||
|
testIds: failedTestIds
|
||
|
});
|
||
|
lastRun = {
|
||
|
type: 'failed',
|
||
|
failedTestIds
|
||
|
};
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'repeat') {
|
||
|
if (lastRun.type === 'regular') {
|
||
|
await runTests(options, testServerConnection, {
|
||
|
title: 're-running tests'
|
||
|
});
|
||
|
continue;
|
||
|
} else if (lastRun.type === 'changed') {
|
||
|
await runTests(options, testServerConnection, {
|
||
|
title: 're-running tests',
|
||
|
testIds: lastRun.dirtyTestIds
|
||
|
});
|
||
|
} else if (lastRun.type === 'failed') {
|
||
|
await runTests({}, testServerConnection, {
|
||
|
title: 're-running tests',
|
||
|
testIds: lastRun.failedTestIds
|
||
|
});
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'toggle-show-browser') {
|
||
|
await toggleShowBrowser();
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'toggle-buffer-mode') {
|
||
|
bufferMode = !bufferMode;
|
||
|
continue;
|
||
|
}
|
||
|
if (command === 'exit') break;
|
||
|
if (command === 'interrupted') {
|
||
|
result = 'interrupted';
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
const teardown = await testServerConnection.runGlobalTeardown({});
|
||
|
return result === 'passed' ? teardown.status : result;
|
||
|
}
|
||
|
function readKeyPress(handler) {
|
||
|
const promise = new _utils.ManualPromise();
|
||
|
const rl = _readline.default.createInterface({
|
||
|
input: process.stdin,
|
||
|
escapeCodeTimeout: 50
|
||
|
});
|
||
|
_readline.default.emitKeypressEvents(process.stdin, rl);
|
||
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
||
|
const listener = _utils.eventsHelper.addEventListener(process.stdin, 'keypress', (text, key) => {
|
||
|
const result = handler(text, key);
|
||
|
if (result) promise.resolve(result);
|
||
|
});
|
||
|
const cancel = () => {
|
||
|
_utils.eventsHelper.removeEventListeners([listener]);
|
||
|
rl.close();
|
||
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
||
|
};
|
||
|
void promise.finally(cancel);
|
||
|
return {
|
||
|
result: promise,
|
||
|
cancel
|
||
|
};
|
||
|
}
|
||
|
const isInterrupt = (text, key) => text === '\x03' || text === '\x1B' || key && key.name === 'escape' || key && key.ctrl && key.name === 'c';
|
||
|
async function runTests(watchOptions, testServerConnection, options) {
|
||
|
printConfiguration(watchOptions, options === null || options === void 0 ? void 0 : options.title);
|
||
|
const waitForDone = readKeyPress((text, key) => {
|
||
|
if (isInterrupt(text, key)) {
|
||
|
testServerConnection.stopTestsNoReply({});
|
||
|
return 'done';
|
||
|
}
|
||
|
});
|
||
|
await testServerConnection.runTests({
|
||
|
grep: watchOptions.grep,
|
||
|
testIds: options === null || options === void 0 ? void 0 : options.testIds,
|
||
|
locations: watchOptions === null || watchOptions === void 0 ? void 0 : watchOptions.files,
|
||
|
projects: watchOptions.projects,
|
||
|
connectWsEndpoint,
|
||
|
reuseContext: connectWsEndpoint ? true : undefined,
|
||
|
workers: connectWsEndpoint ? 1 : undefined,
|
||
|
headed: connectWsEndpoint ? true : undefined
|
||
|
}).finally(() => waitForDone.cancel());
|
||
|
}
|
||
|
function readCommand() {
|
||
|
return readKeyPress((text, key) => {
|
||
|
if (isInterrupt(text, key)) return 'interrupted';
|
||
|
if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') {
|
||
|
process.kill(process.ppid, 'SIGTSTP');
|
||
|
process.kill(process.pid, 'SIGTSTP');
|
||
|
}
|
||
|
const name = key === null || key === void 0 ? void 0 : key.name;
|
||
|
if (name === 'q') return 'exit';
|
||
|
if (name === 'h') {
|
||
|
process.stdout.write(`${(0, _base.separator)()}
|
||
|
Run tests
|
||
|
${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('run tests')}
|
||
|
${_utilsBundle.colors.bold('f')} ${_utilsBundle.colors.dim('run failed tests')}
|
||
|
${_utilsBundle.colors.bold('r')} ${_utilsBundle.colors.dim('repeat last run')}
|
||
|
${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('quit')}
|
||
|
|
||
|
Change settings
|
||
|
${_utilsBundle.colors.bold('c')} ${_utilsBundle.colors.dim('set project')}
|
||
|
${_utilsBundle.colors.bold('p')} ${_utilsBundle.colors.dim('set file filter')}
|
||
|
${_utilsBundle.colors.bold('t')} ${_utilsBundle.colors.dim('set title filter')}
|
||
|
${_utilsBundle.colors.bold('s')} ${_utilsBundle.colors.dim('toggle show & reuse the browser')}
|
||
|
${_utilsBundle.colors.bold('b')} ${_utilsBundle.colors.dim('toggle buffer mode')}
|
||
|
`);
|
||
|
return;
|
||
|
}
|
||
|
switch (name) {
|
||
|
case 'return':
|
||
|
return 'run';
|
||
|
case 'r':
|
||
|
return 'repeat';
|
||
|
case 'c':
|
||
|
return 'project';
|
||
|
case 'p':
|
||
|
return 'file';
|
||
|
case 't':
|
||
|
return 'grep';
|
||
|
case 'f':
|
||
|
return 'failed';
|
||
|
case 's':
|
||
|
return 'toggle-show-browser';
|
||
|
case 'b':
|
||
|
return 'toggle-buffer-mode';
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
let showBrowserServer;
|
||
|
let connectWsEndpoint = undefined;
|
||
|
let seq = 1;
|
||
|
function printConfiguration(options, title) {
|
||
|
const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
|
||
|
const tokens = [];
|
||
|
tokens.push(`${packageManagerCommand} playwright test`);
|
||
|
if (options.projects) tokens.push(...options.projects.map(p => _utilsBundle.colors.blue(`--project ${p}`)));
|
||
|
if (options.grep) tokens.push(_utilsBundle.colors.red(`--grep ${options.grep}`));
|
||
|
if (options.files) tokens.push(...options.files.map(a => _utilsBundle.colors.bold(a)));
|
||
|
if (title) tokens.push(_utilsBundle.colors.dim(`(${title})`));
|
||
|
tokens.push(_utilsBundle.colors.dim(`#${seq++}`));
|
||
|
const lines = [];
|
||
|
const sep = (0, _base.separator)();
|
||
|
lines.push('\x1Bc' + sep);
|
||
|
lines.push(`${tokens.join(' ')}`);
|
||
|
lines.push(`${_utilsBundle.colors.dim('Show & reuse browser:')} ${_utilsBundle.colors.bold(showBrowserServer ? 'on' : 'off')}`);
|
||
|
process.stdout.write(lines.join('\n'));
|
||
|
}
|
||
|
function printBufferPrompt(dirtyTestFiles, rootDir) {
|
||
|
const sep = (0, _base.separator)();
|
||
|
process.stdout.write('\x1Bc');
|
||
|
process.stdout.write(`${sep}\n`);
|
||
|
if (dirtyTestFiles.size === 0) {
|
||
|
process.stdout.write(`${_utilsBundle.colors.dim('Waiting for file changes. Press')} ${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('to quit or')} ${_utilsBundle.colors.bold('h')} ${_utilsBundle.colors.dim('for more options.')}\n\n`);
|
||
|
return;
|
||
|
}
|
||
|
process.stdout.write(`${_utilsBundle.colors.dim(`${dirtyTestFiles.size} test ${dirtyTestFiles.size === 1 ? 'file' : 'files'} changed:`)}\n\n`);
|
||
|
for (const file of dirtyTestFiles) process.stdout.write(` · ${_path.default.relative(rootDir, file)}\n`);
|
||
|
process.stdout.write(`\n${_utilsBundle.colors.dim(`Press`)} ${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('to run')}, ${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('to quit or')} ${_utilsBundle.colors.bold('h')} ${_utilsBundle.colors.dim('for more options.')}\n\n`);
|
||
|
}
|
||
|
function printPrompt() {
|
||
|
const sep = (0, _base.separator)();
|
||
|
process.stdout.write(`
|
||
|
${sep}
|
||
|
${_utilsBundle.colors.dim('Waiting for file changes. Press')} ${_utilsBundle.colors.bold('enter')} ${_utilsBundle.colors.dim('to run tests')}, ${_utilsBundle.colors.bold('q')} ${_utilsBundle.colors.dim('to quit or')} ${_utilsBundle.colors.bold('h')} ${_utilsBundle.colors.dim('for more options.')}
|
||
|
`);
|
||
|
}
|
||
|
async function toggleShowBrowser() {
|
||
|
if (!showBrowserServer) {
|
||
|
showBrowserServer = new _playwrightServer.PlaywrightServer({
|
||
|
mode: 'extension',
|
||
|
path: '/' + (0, _utils.createGuid)(),
|
||
|
maxConnections: 1
|
||
|
});
|
||
|
connectWsEndpoint = await showBrowserServer.listen();
|
||
|
process.stdout.write(`${_utilsBundle.colors.dim('Show & reuse browser:')} ${_utilsBundle.colors.bold('on')}\n`);
|
||
|
} else {
|
||
|
var _showBrowserServer;
|
||
|
await ((_showBrowserServer = showBrowserServer) === null || _showBrowserServer === void 0 ? void 0 : _showBrowserServer.close());
|
||
|
showBrowserServer = undefined;
|
||
|
connectWsEndpoint = undefined;
|
||
|
process.stdout.write(`${_utilsBundle.colors.dim('Show & reuse browser:')} ${_utilsBundle.colors.bold('off')}\n`);
|
||
|
}
|
||
|
}
|