Browse Source

chore: bring in folio source (#6923)

- Source now lives at `src/test`.
- Former folio tests live at `tests/playwright-test`.
- We use `src/test/internal.ts` that exposes base test without
  Playwright fixtures for most tests (to avoid modifications for now).
- Test types live in `types/testFoo.d.ts`.
- Stable test runner is installed to `tests/config/test-runner` during `npm install`.
- All deps including test-only are now listed in `package.json`.
  Non-test deps must also be listed in `build_package.js` to get included.
pull/6928/head
Dmitry Gozman 1 week ago
committed by GitHub
parent
commit
f745bf1fbc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      .github/workflows/tests_primary.yml
  2. 11
      install-from-github.js
  3. 946
      package-lock.json
  4. 49
      package.json
  5. 26
      packages/build_package.js
  6. 2
      packages/playwright-test/index.js
  7. 19
      src/cli/cli.ts
  8. 32
      src/test/cli.ts
  9. 367
      src/test/dispatcher.ts
  10. 40
      src/test/expect.ts
  11. 361
      src/test/fixtures.ts
  12. 34
      src/test/globals.ts
  13. 179
      src/test/golden.ts
  14. 8
      src/test/index.ts
  15. 25
      src/test/internal.ts
  16. 67
      src/test/ipc.ts
  17. 397
      src/test/loader.ts
  18. 109
      src/test/project.ts
  19. 77
      src/test/reporter.ts
  20. 245
      src/test/reporters/base.ts
  21. 57
      src/test/reporters/dot.ts
  22. 30
      src/test/reporters/empty.ts
  23. 160
      src/test/reporters/json.ts
  24. 199
      src/test/reporters/junit.ts
  25. 74
      src/test/reporters/line.ts
  26. 111
      src/test/reporters/list.ts
  27. 65
      src/test/reporters/multiplexer.ts
  28. 319
      src/test/runner.ts
  29. 223
      src/test/test.ts
  30. 161
      src/test/testType.ts
  31. 99
      src/test/transform.ts
  32. 17
      src/test/types.ts
  33. 195
      src/test/util.ts
  34. 119
      src/test/worker.ts
  35. 453
      src/test/workerRunner.ts
  36. 2222
      src/third_party/diff_match_patch.js
  37. 6
      tests/android/androidTest.ts
  38. 4
      tests/config/android.config.ts
  39. 11
      tests/config/baseTest.ts
  40. 6
      tests/config/browserTest.ts
  41. 6
      tests/config/default.config.ts
  42. 4
      tests/config/electron.config.ts
  43. 1
      tests/config/test-runner/.gitignore
  44. 3
      tests/config/test-runner/README.md
  45. 17
      tests/config/test-runner/index.d.ts
  46. 17
      tests/config/test-runner/index.js
  47. 3358
      tests/config/test-runner/package-lock.json
  48. 6
      tests/config/test-runner/package.json
  49. 2
      tests/config/utils.ts
  50. 6
      tests/electron/electronTest.ts
  51. 2
      tests/inspector/inspectorTest.ts
  52. 2
      tests/page/pageTest.ts
  53. 84
      tests/playwright-test/access-data.spec.ts
  54. BIN
      tests/playwright-test/assets/screenshot-canvas-actual.png
  55. BIN
      tests/playwright-test/assets/screenshot-canvas-expected.png
  56. 95
      tests/playwright-test/base-reporter.spec.ts
  57. 253
      tests/playwright-test/basic.spec.ts
  58. 401
      tests/playwright-test/config.spec.ts
  59. 92
      tests/playwright-test/dot-reporter.spec.ts
  60. 163
      tests/playwright-test/exit-code.spec.ts
  61. 126
      tests/playwright-test/expect.spec.ts
  62. 392
      tests/playwright-test/fixture-errors.spec.ts
  63. 598
      tests/playwright-test/fixtures.spec.ts
  64. 111
      tests/playwright-test/gitignore.spec.ts
  65. 214
      tests/playwright-test/global-setup.spec.ts
  66. 256
      tests/playwright-test/golden.spec.ts
  67. 192
      tests/playwright-test/hooks.spec.ts
  68. 81
      tests/playwright-test/json-reporter.spec.ts
  69. 193
      tests/playwright-test/junit-reporter.spec.ts
  70. 50
      tests/playwright-test/line-reporter.spec.ts
  71. 34
      tests/playwright-test/list-mode.spec.ts
  72. 43
      tests/playwright-test/list-reporter.spec.ts
  73. 98
      tests/playwright-test/match-grep.spec.ts
  74. 65
      tests/playwright-test/max-failures.spec.ts
  75. 166
      tests/playwright-test/options.spec.ts
  76. 70
      tests/playwright-test/override-timeout.spec.ts
  77. 262
      tests/playwright-test/playwright-test-fixtures.ts
  78. 22
      tests/playwright-test/playwright-test-internal.d.ts
  79. 17
      tests/playwright-test/playwright-test-internal.js
  80. 29
      tests/playwright-test/playwright-test.config.ts
  81. 54
      tests/playwright-test/repeat-each.spec.ts
  82. 186
      tests/playwright-test/retry.spec.ts
  83. 56
      tests/playwright-test/shard.spec.ts
  84. 62
      tests/playwright-test/stdio.spec.ts
  85. 178
      tests/playwright-test/test-extend.spec.ts
  86. 262
      tests/playwright-test/test-ignore.spec.ts
  87. 54
      tests/playwright-test/test-info.spec.ts
  88. 216
      tests/playwright-test/test-modifiers.spec.ts
  89. 233
      tests/playwright-test/test-output-dir.spec.ts
  90. 113
      tests/playwright-test/timeout.spec.ts
  91. 107
      tests/playwright-test/types-2.spec.ts
  92. 165
      tests/playwright-test/types.spec.ts
  93. 90
      tests/playwright-test/worker-index.spec.ts
  94. 9
      types/test.d.ts
  95. 71
      types/testExpect.d.ts
  96. 868
      types/testInternal.d.ts
  97. 3
      utils/check_deps.js

17
.github/workflows/tests_primary.yml

@ -47,3 +47,20 @@ jobs:
name: ${{ matrix.browser }}-${{ matrix.os }}-test-results
path: test-results
test_test_runner:
name: Test Runner
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- run: npm ci
env:
DEBUG: pw:install
- run: npm run build
- run: npm run ttest

11
install-from-github.js

@ -18,6 +18,17 @@
// This file is only run when someone installs via the github repo
const {execSync} = require('child_process');
const path = require('path');
console.log(`Updating test runner...`);
try {
execSync('npm ci --save=false --fund=false --audit=false', {
stdio: ['inherit', 'inherit', 'inherit'],
cwd: path.join(__dirname, 'tests', 'config', 'test-runner'),
});
} catch (e) {
process.exit(1);
}
console.log(`Rebuilding installer...`);
try {

946
package-lock.json
File diff suppressed because it is too large
View File

49
package.json

@ -9,12 +9,14 @@
"node": ">=12"
},
"scripts": {
"ctest": "folio --config=tests/config/default.config.ts --project=chromium",
"ftest": "folio --config=tests/config/default.config.ts --project=firefox",
"wtest": "folio --config=tests/config/default.config.ts --project=webkit",
"atest": "folio --config=tests/config/android.config.ts",
"etest": "folio --config=tests/config/electron.config.ts",
"test": "folio --config=tests/config/default.config.ts",
"basetest": "node ./tests/config/test-runner/node_modules/@playwright/test/lib/cli/cli.js test",
"ctest": "npm run basetest -- --config=tests/config/default.config.ts --project=chromium",
"ftest": "npm run basetest -- --config=tests/config/default.config.ts --project=firefox",
"wtest": "npm run basetest -- --config=tests/config/default.config.ts --project=webkit",
"atest": "npm run basetest -- --config=tests/config/android.config.ts",
"etest": "npm run basetest -- --config=tests/config/electron.config.ts",
"ttest": "npm run basetest -- --config=tests/playwright-test/playwright-test.config.ts",
"test": "npm run basetest -- --config=tests/config/default.config.ts",
"eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext ts . || eslint --ext ts .",
"tsc": "tsc -p .",
"tsc-installer": "tsc -p ./src/install/tsconfig.json",
@ -36,7 +38,32 @@
"bin": {
"playwright": "./lib/cli/cli.js"
},
"DEPS-NOTE": "Any non-test dependency must be added to the build_package.js script as well",
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@babel/core": "^7.14.0",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-dynamic-import": "^7.13.8",
"@babel/plugin-proposal-export-namespace-from": "^7.12.13",
"@babel/plugin-proposal-logical-assignment-operators": "^7.13.8",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-numeric-separator": "^7.12.13",
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@babel/plugin-proposal-private-methods": "^7.13.0",
"@babel/plugin-proposal-private-property-in-object": "^7.14.0",
"@babel/plugin-syntax-async-generators": "^7.8.4",
"@babel/plugin-syntax-json-strings": "^7.8.3",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.14.0",
"@babel/preset-typescript": "^7.13.0",
"colors": "^1.4.0",
"expect": "^26.4.2",
"minimatch": "^3.0.3",
"ms": "^2.1.2",
"pirates": "^4.0.1",
"pixelmatch": "^5.2.1",
"source-map-support": "^0.4.18",
"commander": "^6.1.0",
"debug": "^4.1.1",
"extract-zip": "^2.0.1",
@ -53,10 +80,14 @@
"yazl": "^2.5.1"
},
"devDependencies": {
"@types/babel__code-frame": "^7.0.2",
"@types/babel__core": "^7.1.14",
"@types/debug": "^4.1.5",
"@types/extract-zip": "^1.6.2",
"@types/mime": "^2.0.3",
"@types/minimatch": "^3.0.3",
"@types/node": "^10.17.28",
"@types/pixelmatch": "^5.2.1",
"@types/pngjs": "^3.4.2",
"@types/progress": "^2.0.3",
"@types/proper-lockfile": "^4.1.1",
@ -65,8 +96,10 @@
"@types/react-dom": "^17.0.0",
"@types/resize-observer-browser": "^0.1.4",
"@types/rimraf": "^3.0.0",
"@types/source-map-support": "^0.4.2",
"@types/webpack": "^4.41.25",
"@types/ws": "7.2.6",
"@types/xml2js": "^0.4.5",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
@ -80,7 +113,6 @@
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^6.1.0",
"folio": "=0.4.0-alpha28",
"formidable": "^1.2.2",
"html-webpack-plugin": "^4.4.1",
"ncp": "^2.0.0",
@ -90,9 +122,10 @@
"socksv5": "0.0.6",
"style-loader": "^1.2.1",
"ts-loader": "^8.0.3",
"typescript": "^4.0.2",
"typescript": "=4.2.4",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"xml2js": "^0.4.23",
"yaml": "^1.10.0"
}
}

26
packages/build_package.js

@ -61,10 +61,27 @@ const PACKAGES = {
'playwright-chromium': {
description: 'A high-level API to automate Chromium',
browsers: ['chromium', 'ffmpeg'],
files: [...PLAYWRIGHT_CORE_FILES],
files: PLAYWRIGHT_CORE_FILES,
},
};
const DEPENDENCIES = [
'commander',
'debug',
'extract-zip',
'https-proxy-agent',
'jpeg-js',
'mime',
'pngjs',
'progress',
'proper-lockfile',
'proxy-from-env',
'rimraf',
'stack-utils',
'ws',
'yazl',
];
// 1. Parse CLI arguments
const args = process.argv.slice(2);
if (args.some(arg => arg === '--help')) {
@ -121,9 +138,10 @@ if (!args.some(arg => arg === '--no-cleanup')) {
// 4. Generate package.json
const pwInternalJSON = require(path.join(ROOT_PATH, 'package.json'));
const dependencies = { ...pwInternalJSON.dependencies };
if (packageName === 'playwright-test')
dependencies.folio = pwInternalJSON.devDependencies.folio;
const depNames = packageName === 'playwright-test' ? Object.keys(pwInternalJSON.dependencies) : DEPENDENCIES;
const dependencies = {};
for (const dep of depNames)
dependencies[dep] = pwInternalJSON.dependencies[dep];
await writeToPackage('package.json', JSON.stringify({
name: package.name || packageName,
version: pwInternalJSON.version,

2
packages/playwright-test/index.js

@ -16,5 +16,5 @@
module.exports = {
...require('./lib/inprocess'),
...require('./lib/cli/fixtures')
...require('./lib/test/index')
};

19
src/cli/cli.ts

@ -35,16 +35,16 @@ import { BrowserContextOptions, LaunchOptions } from '../client/types';
import { spawn } from 'child_process';
import { installDeps } from '../install/installDeps';
import { allBrowserNames, BrowserName } from '../utils/registry';
import { addTestCommand } from './testRunner';
import * as utils from '../utils/utils';
const SCRIPTS_DIRECTORY = path.join(__dirname, '..', '..', 'bin');
type BrowserChannel = 'chrome-beta'|'chrome';
const allBrowserChannels: Set<BrowserChannel> = new Set(['chrome-beta', 'chrome']);
const packageJSON = require('../../package.json');
program
.version('Version ' + require('../../package.json').version)
.version('Version ' + packageJSON.version)
.name(process.env.PW_CLI_NAME || 'npx playwright');
commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', [])
@ -226,8 +226,19 @@ program
console.log(' $ show-trace trace/directory');
});
if (!process.env.PW_CLI_TARGET_LANG)
addTestCommand(program);
if (!process.env.PW_CLI_TARGET_LANG) {
if (packageJSON.name === '@playwright/test' || process.env.PWTEST_CLI_ALLOW_TEST_COMMAND) {
require('../test/cli').addTestCommand(program);
} else {
const command = program.command('test');
command.description('Run tests with Playwright Test. Available in @playwright/test package.');
command.action(async (args, opts) => {
console.error('Please install @playwright/test package to use Playwright Test.');
console.error(' npm install -D @playwright/test');
process.exit(1);
});
}
}
if (process.argv[2] === 'run-driver')
runDriver();

32
src/cli/testRunner.ts → src/test/cli.ts

@ -19,9 +19,8 @@
import * as commander from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import type { Config } from 'folio';
type RunnerType = typeof import('folio/out/runner').Runner;
import type { Config } from './types';
import { Runner } from './runner';
const defaultTimeout = 30000;
const defaultReporter = process.env.CI ? 'dot' : 'list';
@ -37,14 +36,6 @@ const defaultConfig: Config = {
};
export function addTestCommand(program: commander.CommanderStatic) {
let Runner: RunnerType;
try {
Runner = require('folio/out/runner').Runner as RunnerType;
} catch (e) {
addStubTestCommand(program);
return;
}
const command = program.command('test [test-filter...]');
command.description('Run tests with Playwright Test');
command.option('--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`);
@ -68,7 +59,7 @@ export function addTestCommand(program: commander.CommanderStatic) {
command.option('-x', `Stop after the first failure`);
command.action(async (args, opts) => {
try {
await runTests(Runner, args, opts);
await runTests(args, opts);
} catch (e) {
console.error(e.toString());
process.exit(1);
@ -86,7 +77,7 @@ export function addTestCommand(program: commander.CommanderStatic) {
});
}
async function runTests(Runner: RunnerType, args: string[], opts: { [key: string]: any }) {
async function runTests(args: string[], opts: { [key: string]: any }) {
const browserOpt = opts.browser ? opts.browser.toLowerCase() : 'chromium';
if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt))
throw new Error(`Unsupported browser "${opts.browser}", must be one of "all", "chromium", "firefox" or "webkit"`);
@ -135,11 +126,6 @@ async function runTests(Runner: RunnerType, args: string[], opts: { [key: string
throw new Error(`Configuration file not found. Run "npx playwright test --help" for more information.`);
}
process.env.FOLIO_JUNIT_OUTPUT_NAME = process.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME;
process.env.FOLIO_JUNIT_SUITE_ID = process.env.PLAYWRIGHT_JUNIT_SUITE_ID;
process.env.FOLIO_JUNIT_SUITE_NAME = process.env.PLAYWRIGHT_JUNIT_SUITE_NAME;
process.env.FOLIO_JSON_OUTPUT_NAME = process.env.PLAYWRIGHT_JSON_OUTPUT_NAME;
const result = await runner.run(!!opts.list, args.map(forceRegExp), opts.project || undefined);
if (result === 'sigint')
process.exit(130);
@ -172,13 +158,3 @@ function overridesFromOptions(options: { [key: string]: any }): Config {
workers: options.workers ? parseInt(options.workers, 10) : undefined,
};
}
function addStubTestCommand(program: commander.CommanderStatic) {
const command = program.command('test');
command.description('Run tests with Playwright Test. Available in @playwright/test package.');
command.action(async (args, opts) => {
console.error('Please install @playwright/test package to use Playwright Test.');
console.error(' npm install -D @playwright/test');
process.exit(1);
});
}

367
src/test/dispatcher.ts

@ -0,0 +1,367 @@
/**
* 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.
*/
import child_process from 'child_process';
import path from 'path';
import { EventEmitter } from 'events';
import { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams } from './ipc';
import type { TestResult, Reporter, TestStatus } from './reporter';
import { Suite, Test } from './test';
import { Loader } from './loader';
type DispatcherEntry = {
runPayload: RunPayload;
hash: string;
repeatEachIndex: number;
projectIndex: number;
};
export class Dispatcher {
private _workers = new Set<Worker>();
private _freeWorkers: Worker[] = [];
private _workerClaimers: (() => void)[] = [];
private _testById = new Map<string, { test: Test, result: TestResult }>();
private _queue: DispatcherEntry[] = [];
private _stopCallback = () => {};
readonly _loader: Loader;
private _suite: Suite;
private _reporter: Reporter;
private _hasWorkerErrors = false;
private _isStopped = false;
private _failureCount = 0;
constructor(loader: Loader, suite: Suite, reporter: Reporter) {
this._loader = loader;
this._reporter = reporter;
this._suite = suite;
for (const suite of this._suite.suites) {
for (const spec of suite._allSpecs()) {
for (const test of spec.tests)
this._testById.set(test._id, { test, result: test._appendTestResult() });
}
}
this._queue = this._filesSortedByWorkerHash();
// Shard tests.
const shard = this._loader.fullConfig().shard;
if (shard) {
let total = this._suite.totalTestCount();
const shardSize = Math.ceil(total / shard.total);
const from = shardSize * shard.current;
const to = shardSize * (shard.current + 1);
let current = 0;
total = 0;
const filteredQueue: DispatcherEntry[] = [];
for (const entry of this._queue) {
if (current >= from && current < to) {
filteredQueue.push(entry);
total += entry.runPayload.entries.length;
}
current += entry.runPayload.entries.length;
}
this._queue = filteredQueue;
}
}
_filesSortedByWorkerHash(): DispatcherEntry[] {
const entriesByWorkerHashAndFile = new Map<string, Map<string, DispatcherEntry>>();
for (const fileSuite of this._suite.suites) {
const file = fileSuite.file;
for (const spec of fileSuite._allSpecs()) {
for (const test of spec.tests) {
let entriesByFile = entriesByWorkerHashAndFile.get(test._workerHash);
if (!entriesByFile) {
entriesByFile = new Map();
entriesByWorkerHashAndFile.set(test._workerHash, entriesByFile);
}
let entry = entriesByFile.get(file);
if (!entry) {
entry = {
runPayload: {
entries: [],
file,
},
repeatEachIndex: test._repeatEachIndex,
projectIndex: test._projectIndex,
hash: test._workerHash,
};
entriesByFile.set(file, entry);
}
entry.runPayload.entries.push({
retry: this._testById.get(test._id)!.result.retry,
testId: test._id,
});
}
}
}
const result: DispatcherEntry[] = [];
for (const entriesByFile of entriesByWorkerHashAndFile.values()) {
for (const entry of entriesByFile.values())
result.push(entry);
}
result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1));
return result;
}
async run() {
// Loop in case job schedules more jobs
while (this._queue.length && !this._isStopped)
await this._dispatchQueue();
}
async _dispatchQueue() {
const jobs = [];
while (this._queue.length) {
if (this._isStopped)
break;
const entry = this._queue.shift()!;
const requiredHash = entry.hash;
let worker = await this._obtainWorker(entry);
while (!this._isStopped && worker.hash && worker.hash !== requiredHash) {
worker.stop();
worker = await this._obtainWorker(entry);
}
if (this._isStopped)
break;
jobs.push(this._runJob(worker, entry));
}
await Promise.all(jobs);
}
async _runJob(worker: Worker, entry: DispatcherEntry) {
worker.run(entry.runPayload);
let doneCallback = () => {};
const result = new Promise<void>(f => doneCallback = f);
worker.once('done', (params: DonePayload) => {
// We won't file remaining if:
// - there are no remaining
// - we are here not because something failed
// - no unrecoverable worker error
if (!params.remaining.length && !params.failedTestId && !params.fatalError) {
this._freeWorkers.push(worker);
this._notifyWorkerClaimer();
doneCallback();
return;
}
// When worker encounters error, we will stop it and create a new one.
worker.stop();
let remaining = params.remaining;
const failedTestIds = new Set<string>();
// In case of fatal error, report all remaining tests as failing with this error.
if (params.fatalError) {
for (const { testId } of remaining) {
const { test, result } = this._testById.get(testId)!;
this._reporter.onTestBegin?.(test);
result.error = params.fatalError;
this._reportTestEnd(test, result, 'failed');
failedTestIds.add(testId);
}
// Since we pretent that all remaining tests failed, there is nothing else to run,
// except for possible retries.
remaining = [];
}
if (params.failedTestId)
failedTestIds.add(params.failedTestId);
// Only retry expected failures, not passes and only if the test failed.
for (const testId of failedTestIds) {
const pair = this._testById.get(testId)!;
if (pair.test.expectedStatus === 'passed' && pair.test.results.length < pair.test.retries + 1) {
pair.result = pair.test._appendTestResult();
remaining.unshift({
retry: pair.result.retry,
testId: pair.test._id,
});
}
}
if (remaining.length)
this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } });
// This job is over, we just scheduled another one.
doneCallback();
});
return result;
}
async _obtainWorker(entry: DispatcherEntry) {
const claimWorker = (): Promise<Worker> | null => {
// Use available worker.
if (this._freeWorkers.length)
return Promise.resolve(this._freeWorkers.pop()!);
// Create a new worker.
if (this._workers.size < this._loader.fullConfig().workers)
return this._createWorker(entry);
return null;
};
// Note: it is important to claim the worker synchronously,
// so that we won't miss a _notifyWorkerClaimer call while awaiting.
let worker = claimWorker();
if (!worker) {
// Wait for available or stopped worker.
await new Promise<void>(f => this._workerClaimers.push(f));
worker = claimWorker();
}
return worker!;
}
async _notifyWorkerClaimer() {
if (this._isStopped || !this._workerClaimers.length)
return;
const callback = this._workerClaimers.shift()!;
callback();
}
_createWorker(entry: DispatcherEntry) {
const worker = new Worker(this);
worker.on('testBegin', (params: TestBeginPayload) => {
const { test, result: testRun } = this._testById.get(params.testId)!;
testRun.workerIndex = params.workerIndex;
this._reporter.onTestBegin(test);
});
worker.on('testEnd', (params: TestEndPayload) => {
const { test, result } = this._testById.get(params.testId)!;
result.duration = params.duration;
result.error = params.error;
test.expectedStatus = params.expectedStatus;
test.annotations = params.annotations;
test.timeout = params.timeout;
if (params.expectedStatus === 'skipped' && params.status === 'skipped')
test.skipped = true;
this._reportTestEnd(test, result, params.status);
});
worker.on('stdOut', (params: TestOutputPayload) => {
const chunk = chunkFromParams(params);
const pair = params.testId ? this._testById.get(params.testId) : undefined;
if (pair)
pair.result.stdout.push(chunk);
this._reporter.onStdOut(chunk, pair ? pair.test : undefined);
});
worker.on('stdErr', (params: TestOutputPayload) => {
const chunk = chunkFromParams(params);
const pair = params.testId ? this._testById.get(params.testId) : undefined;
if (pair)
pair.result.stderr.push(chunk);
this._reporter.onStdErr(chunk, pair ? pair.test : undefined);
});
worker.on('teardownError', ({error}) => {
this._hasWorkerErrors = true;
this._reporter.onError(error);
});
worker.on('exit', () => {
this._workers.delete(worker);
this._notifyWorkerClaimer();
if (this._stopCallback && !this._workers.size)
this._stopCallback();
});
this._workers.add(worker);
return worker.init(entry).then(() => worker);
}
async stop() {
this._isStopped = true;
if (this._workers.size) {
const result = new Promise<void>(f => this._stopCallback = f);
for (const worker of this._workers)
worker.stop();
await result;
}
}
private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) {
if (this._isStopped)
return;
result.status = status;
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount;
const maxFailures = this._loader.fullConfig().maxFailures;
if (!maxFailures || this._failureCount <= maxFailures)
this._reporter.onTestEnd(test, result);
if (maxFailures && this._failureCount === maxFailures)
this._isStopped = true;
}
hasWorkerErrors(): boolean {
return this._hasWorkerErrors;
}
}
let lastWorkerIndex = 0;
class Worker extends EventEmitter {
process: child_process.ChildProcess;
runner: Dispatcher;
hash = '';
index: number;
constructor(runner: Dispatcher) {
super();
this.runner = runner;
this.index = lastWorkerIndex++;
this.process = child_process.fork(path.join(__dirname, 'worker.js'), {
detached: false,
env: {
FORCE_COLOR: process.stdout.isTTY ? '1' : '0',
DEBUG_COLORS: process.stdout.isTTY ? '1' : '0',
TEST_WORKER_INDEX: String(this.index),
...process.env
},
// Can't pipe since piping slows down termination for some reason.
stdio: ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc']
});
this.process.on('exit', () => this.emit('exit'));
this.process.on('error', e => {}); // do not yell at a send to dead process.
this.process.on('message', (message: any) => {
const { method, params } = message;
this.emit(method, params);
});
}
async init(entry: DispatcherEntry) {
this.hash = entry.hash;
const params: WorkerInitParams = {
workerIndex: this.index,
repeatEachIndex: entry.repeatEachIndex,
projectIndex: entry.projectIndex,
loader: this.runner._loader.serialize(),
};
this.process.send({ method: 'init', params });
await new Promise(f => this.process.once('message', f)); // Ready ack
}
run(runPayload: RunPayload) {
this.process.send({ method: 'run', params: runPayload });
}
stop() {
this.process.send({ method: 'stop' });
}
}
function chunkFromParams(params: TestOutputPayload): string | Buffer {
if (typeof params.text === 'string')
return params.text;
return Buffer.from(params.buffer!, 'base64');
}

40
src/test/expect.ts

@ -0,0 +1,40 @@
/**
* 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.
*/
import type { Expect } from './types';
import expectLibrary from 'expect';
import { currentTestInfo } from './globals';
import { compare } from './golden';
export const expect: Expect = expectLibrary;
function toMatchSnapshot(received: Buffer | string, nameOrOptions: string | { name: string, threshold?: number }, optOptions: { threshold?: number } = {}) {
let options: { name: string, threshold?: number };
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`toMatchSnapshot() must be called during the test`);
if (typeof nameOrOptions === 'string')
options = { name: nameOrOptions, ...optOptions };
else
options = { ...nameOrOptions };
if (!options.name)
throw new Error(`toMatchSnapshot() requires a "name" parameter`);
const { pass, message } = compare(received, options.name, testInfo.snapshotPath, testInfo.outputPath, testInfo.config.updateSnapshots, options);
return { pass, message: () => message };
}
expectLibrary.extend({ toMatchSnapshot });

361
src/test/fixtures.ts

@ -0,0 +1,361 @@
/**
* 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.
*/
import { errorWithCallLocation, formatLocation, prependErrorMessage, wrapInPromise } from './util';
import * as crypto from 'crypto';
import { FixturesWithLocation, Location } from './types';
type FixtureScope = 'test' | 'worker';
type FixtureRegistration = {
location: Location;
name: string;
scope: FixtureScope;
fn: Function | any; // Either a fixture function, or a fixture value.
auto: boolean;
deps: string[];
id: string;
super?: FixtureRegistration;
};
class Fixture {
runner: FixtureRunner;
registration: FixtureRegistration;
usages: Set<Fixture>;
value: any;
_teardownFenceCallback!: (value?: unknown) => void;
_tearDownComplete!: Promise<void>;
_setup = false;
_teardown = false;
constructor(runner: FixtureRunner, registration: FixtureRegistration) {
this.runner = runner;
this.registration = registration;
this.usages = new Set();
this.value = null;
}
async setup(info: any) {
if (typeof this.registration.fn !== 'function') {
this._setup = true;
this.value = this.registration.fn;
return;
}
const params: { [key: string]: any } = {};
for (const name of this.registration.deps) {
const registration = this.runner.pool!.resolveDependency(this.registration, name);
if (!registration)
throw errorWithCallLocation(`Unknown fixture "${name}"`);
const dep = await this.runner.setupFixtureForRegistration(registration, info);
dep.usages.add(this);
params[name] = dep.value;
}
let setupFenceFulfill = () => {};
let setupFenceReject = (e: Error) => {};
let called = false;
const setupFence = new Promise<void>((f, r) => { setupFenceFulfill = f; setupFenceReject = r; });
const teardownFence = new Promise(f => this._teardownFenceCallback = f);
this._tearDownComplete = wrapInPromise(this.registration.fn(params, async (value: any) => {
if (called)
throw errorWithCallLocation(`Cannot provide fixture value for the second time`);
called = true;
this.value = value;
setupFenceFulfill();
return await teardownFence;
}, info)).catch((e: any) => {
if (!this._setup)
setupFenceReject(e);
else
throw e;
});
await setupFence;
this._setup = true;
}
async teardown() {
if (this._teardown)
return;
this._teardown = true;
if (typeof this.registration.fn !== 'function')
return;
for (const fixture of this.usages)
await fixture.teardown();
this.usages.clear();
if (this._setup) {
this._teardownFenceCallback();
await this._tearDownComplete;
}
this.runner.instanceForId.delete(this.registration.id);
}
}
export class FixturePool {
readonly digest: string;
readonly registrations: Map<string, FixtureRegistration>;
constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool) {
this.registrations = new Map(parentPool ? parentPool.registrations : []);
for (const { fixtures, location } of fixturesList) {
try {
for (const entry of Object.entries(fixtures)) {
const name = entry[0];
let value = entry[1];
let options: { auto: boolean, scope: FixtureScope } | undefined;
if (Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1])) {
options = {
auto: !!value[1].auto,
scope: value[1].scope || 'test'
};
value = value[0];
}
const fn = value as (Function | any);
const previous = this.registrations.get(name);
if (previous && options) {
if (previous.scope !== options.scope)
throw errorWithLocations(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture.`, { location, name}, previous);
if (previous.auto !== options.auto)
throw errorWithLocations(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture.`, { location, name }, previous);
} else if (previous) {
options = { auto: previous.auto, scope: previous.scope };
} else if (!options) {
options = { auto: false, scope: 'test' };
}
const deps = fixtureParameterNames(fn, location);
const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, deps, super: previous };
registrationId(registration);
this.registrations.set(name, registration);
}
} catch (e) {
prependErrorMessage(e, `Error processing fixtures at ${formatLocation(location)}:\n`);
throw e;
}
}
this.digest = this.validate();
}
private validate() {
const markers = new Map<FixtureRegistration, 'visiting' | 'visited'>();
const stack: FixtureRegistration[] = [];
const visit = (registration: FixtureRegistration) => {
markers.set(registration, 'visiting');
stack.push(registration);
for (const name of registration.deps) {
const dep = this.resolveDependency(registration, name);
if (!dep) {
if (name === registration.name)
throw errorWithLocations(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration);
else
throw errorWithLocations(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration);
}
if (registration.scope === 'worker' && dep.scope === 'test')
throw errorWithLocations(`Worker fixture "${registration.name}" cannot depend on a test fixture "${name}".`, registration, dep);
if (!markers.has(dep)) {
visit(dep);
} else if (markers.get(dep) === 'visiting') {
const index = stack.indexOf(dep);
const regs = stack.slice(index, stack.length);
const names = regs.map(r => `"${r.name}"`);
throw errorWithLocations(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle.`, ...regs);
}
}
markers.set(registration, 'visited');
stack.pop();
};
const hash = crypto.createHash('sha1');
const names = Array.from(this.registrations.keys()).sort();
for (const name of names) {
const registration = this.registrations.get(name)!;
visit(registration);
if (registration.scope === 'worker')
hash.update(registration.id + ';');
}
return hash.digest('hex');
}
validateFunction(fn: Function, prefix: string, allowTestFixtures: boolean, location: Location) {
const visit = (registration: FixtureRegistration) => {
for (const name of registration.deps)
visit(this.resolveDependency(registration, name)!);
};
for (const name of fixtureParameterNames(fn, location)) {
const registration = this.registrations.get(name);
if (!registration)
throw errorWithLocations(`${prefix} has unknown parameter "${name}".`, { location, name: prefix, quoted: false });
if (!allowTestFixtures && registration.scope === 'test')
throw errorWithLocations(`${prefix} cannot depend on a test fixture "${name}".`, { location, name: prefix, quoted: false }, registration);
visit(registration);
}
}
resolveDependency(registration: FixtureRegistration, name: string): FixtureRegistration | undefined {
if (name === registration.name)
return registration.super;
return this.registrations.get(name);
}
}
export class FixtureRunner {
private testScopeClean = true;
pool: FixturePool | undefined;
instanceForId = new Map<string, Fixture>();
setPool(pool: FixturePool) {
if (!this.testScopeClean)
throw new Error('Did not teardown test scope');
if (this.pool && pool.digest !== this.pool.digest)
throw new Error('Digests do not match');
this.pool = pool;
}
async teardownScope(scope: string) {
for (const [, fixture] of this.instanceForId) {
if (fixture.registration.scope === scope)
await fixture.teardown();
}
if (scope === 'test')
this.testScopeClean = true;
}
async resolveParametersAndRunHookOrTest(fn: Function, scope: FixtureScope, info: any) {
// Install all automatic fixtures.
for (const registration of this.pool!.registrations.values()) {
const shouldSkip = scope === 'worker' && registration.scope === 'test';
if (registration.auto && !shouldSkip)
await this.setupFixtureForRegistration(registration, info);
}
// Install used fixtures.
const names = fixtureParameterNames(fn, { file: '<unused>', line: 1, column: 1 });
const params: { [key: string]: any } = {};
for (const name of names) {
const registration = this.pool!.registrations.get(name);
if (!registration)
throw errorWithCallLocation('Unknown fixture: ' + name);
const fixture = await this.setupFixtureForRegistration(registration, info);
params[name] = fixture.value;
}
return fn(params, info);
}
async setupFixtureForRegistration(registration: FixtureRegistration, info: any): Promise<Fixture> {
if (registration.scope === 'test')
this.testScopeClean = false;
let fixture = this.instanceForId.get(registration.id);
if (fixture)
return fixture;
fixture = new Fixture(this, registration);
this.instanceForId.set(registration.id, fixture);
await fixture.setup(info);
return fixture;
}
}
export function inheritFixtureParameterNames(from: Function, to: Function, location: Location) {
if (!(to as any)[signatureSymbol])
(to as any)[signatureSymbol] = innerFixtureParameterNames(from, location);
}
const signatureSymbol = Symbol('signature');
function fixtureParameterNames(fn: Function | any, location: Location): string[] {
if (typeof fn !== 'function')
return [];
if (!fn[signatureSymbol])
fn[signatureSymbol] = innerFixtureParameterNames(fn, location);
return fn[signatureSymbol];
}
function innerFixtureParameterNames(fn: Function, location: Location): string[] {
const text = fn.toString();
const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);
if (!match)
return [];
const trimmedParams = match[1].trim();
if (!trimmedParams)
return [];
const [firstParam] = splitByComma(trimmedParams);
if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}')
throw errorWithLocations('First argument must use the object destructuring pattern: ' + firstParam, { location });
const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => {
const colon = prop.indexOf(':');
return colon === -1 ? prop : prop.substring(0, colon).trim();
});
return props;
}
function splitByComma(s: string) {
const result: string[] = [];
const stack: string[] = [];
let start = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === '{' || s[i] === '[') {
stack.push(s[i] === '{' ? '}' : ']');
} else if (s[i] === stack[stack.length - 1]) {
stack.pop();
} else if (!stack.length && s[i] === ',') {
const token = s.substring(start, i).trim();
if (token)
result.push(token);
start = i + 1;
}
}
const lastToken = s.substring(start).trim();
if (lastToken)
result.push(lastToken);
return result;
}
// name + superId, fn -> id
const registrationIdMap = new Map<string, Map<Function | any, string>>();
let lastId = 0;
function registrationId(registration: FixtureRegistration): string {
if (registration.id)
return registration.id;
const key = registration.name + '@@@' + (registration.super ? registrationId(registration.super) : '');
let map = registrationIdMap.get(key);
if (!map) {
map = new Map();
registrationIdMap.set(key, map);
}
if (!map.has(registration.fn))
map.set(registration.fn, String(lastId++));
registration.id = map.get(registration.fn)!;
return registration.id;
}
function errorWithLocations(message: string, ...defined: { location: Location, name?: string, quoted?: boolean }[]): Error {
for (const { name, location, quoted } of defined) {
let prefix = '';
if (name && quoted === false)
prefix = name + ' ';
else if (name)
prefix = `"${name}" `;
message += `\n ${prefix}defined at ${formatLocation(location)}`;
}
const error = new Error(message);
error.stack = 'Error: ' + message + '\n';
return error;
}

34
src/test/globals.ts

@ -0,0 +1,34 @@
/**
* 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.
*/
import type { TestInfo } from './types';
import { Suite } from './test';
let currentTestInfoValue: TestInfo | null = null;
export function setCurrentTestInfo(testInfo: TestInfo | null) {
currentTestInfoValue = testInfo;
}
export function currentTestInfo(): TestInfo | null {
return currentTestInfoValue;
}
let currentFileSuite: Suite | undefined;
export function setCurrentlyLoadingFileSuite(suite: Suite | undefined) {
currentFileSuite = suite;
}
export function currentlyLoadingFileSuite() {
return currentFileSuite;
}

179
src/test/golden.ts

@ -0,0 +1,179 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 colors from 'colors/safe';
import fs from 'fs';
import path from 'path';
import jpeg from 'jpeg-js';
import pixelmatch from 'pixelmatch';
import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch';
import { UpdateSnapshots } from './types';
// Note: we require the pngjs version of pixelmatch to avoid version mismatches.
const { PNG } = require(require.resolve('pngjs', { paths: [require.resolve('pixelmatch')] }));
const extensionToMimeType: { [key: string]: string } = {
'dat': 'application/octet-string',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'png': 'image/png',
'txt': 'text/plain',
};
const GoldenComparators: { [key: string]: any } = {
'application/octet-string': compareBuffers,
'image/png': compareImages,
'image/jpeg': compareImages,
'text/plain': compareText,
};
function compareBuffers(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string): { diff?: object; errorMessage?: string; } | null {
if (!actualBuffer || !(actualBuffer instanceof Buffer))
return { errorMessage: 'Actual result should be Buffer.' };
if (Buffer.compare(actualBuffer, expectedBuffer))
return { errorMessage: 'Buffers differ' };
return null;
}
function compareImages(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string, options = {}): { diff?: object; errorMessage?: string; } | null {
if (!actualBuffer || !(actualBuffer instanceof Buffer))
return { errorMessage: 'Actual result should be Buffer.' };
const actual = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpeg.decode(actualBuffer);
const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer);
if (expected.width !== actual.width || expected.height !== actual.height) {
return {
errorMessage: `Sizes differ; expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `
};
}
const diff = new PNG({width: expected.width, height: expected.height});
const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { threshold: 0.2, ...options });
return count > 0 ? { diff: PNG.sync.write(diff) } : null;
}
function compareText(actual: Buffer | string, expectedBuffer: Buffer): { diff?: object; errorMessage?: string; diffExtension?: string; } | null {
if (typeof actual !== 'string')
return { errorMessage: 'Actual result should be string' };
const expected = expectedBuffer.toString('utf-8');
if (expected === actual)
return null;
const dmp = new diff_match_patch();
const d = dmp.diff_main(expected, actual);
dmp.diff_cleanupSemantic(d);
return {
errorMessage: diff_prettyTerminal(d)
};
}
export function compare(actual: Buffer | string, name: string, snapshotPath: (name: string) => string, outputPath: (name: string) => string, updateSnapshots: UpdateSnapshots, options?: { threshold?: number }): { pass: boolean; message?: string; } {
const snapshotFile = snapshotPath(name);
if (!fs.existsSync(snapshotFile)) {
const writingActual = updateSnapshots === 'all' || updateSnapshots === 'missing';
if (writingActual) {
fs.mkdirSync(path.dirname(snapshotFile), { recursive: true });
fs.writeFileSync(snapshotFile, actual);
}
const message = snapshotFile + ' is missing in snapshots' + (writingActual ? ', writing actual.' : '.');
if (updateSnapshots === 'all') {
console.log(message);
return { pass: true, message };
}
return { pass: false, message };
}
const expected = fs.readFileSync(snapshotFile);
const extension = path.extname(snapshotFile).substring(1);
const mimeType = extensionToMimeType[extension] || 'application/octet-string';
const comparator = GoldenComparators[mimeType];
if (!comparator) {
return {
pass: false,
message: 'Failed to find comparator with type ' + mimeType + ': ' + snapshotFile,
};
}
const result = comparator(actual, expected, mimeType, options);
if (!result)
return { pass: true };
if (updateSnapshots === 'all') {
fs.mkdirSync(path.dirname(snapshotFile), { recursive: true });
fs.writeFileSync(snapshotFile, actual);
console.log(snapshotFile + ' does not match, writing actual.');
return {
pass: true,
message: snapshotFile + ' running with --update-snapshots, writing actual.'
};
}
const outputFile = outputPath(name);
const expectedPath = addSuffix(outputFile, '-expected');
const actualPath = addSuffix(outputFile, '-actual');
const diffPath = addSuffix(outputFile, '-diff');
fs.writeFileSync(expectedPath, expected);
fs.writeFileSync(actualPath, actual);
if (result.diff)
fs.writeFileSync(diffPath, result.diff);
const output = [
colors.red(`Snapshot comparison failed:`),
];
if (result.errorMessage) {
output.push('');
output.push(indent(result.errorMessage, ' '));
}
output.push('');
output.push(`Expected: ${colors.yellow(expectedPath)}`);
output.push(`Received: ${colors.yellow(actualPath)}`);
if (result.diff)
output.push(` Diff: ${colors.yellow(diffPath)}`);
return {
pass: false,
message: output.join('\n'),
};
}
function indent(lines: string, tab: string) {
return lines.replace(/^(?=.+$)/gm, tab);
}
function addSuffix(filePath: string, suffix: string, customExtension?: string): string {
const dirname = path.dirname(filePath);
const ext = path.extname(filePath);
const name = path.basename(filePath, ext);
return path.join(dirname, name + suffix + (customExtension || ext));
}
function diff_prettyTerminal(diffs: diff_match_patch.Diff[]) {
const html = [];
for (let x = 0; x < diffs.length; x++) {
const op = diffs[x][0]; // Operation (insert, delete, equal)
const data = diffs[x][1]; // Text of change.
const text = data;
switch (op) {
case DIFF_INSERT:
html[x] = colors.green(text);
break;
case DIFF_DELETE:
html[x] = colors.strikethrough(colors.red(text));
break;
case DIFF_EQUAL:
html[x] = text;
break;
}
}
return html.join('');
}

8
src/cli/fixtures.ts → src/test/index.ts

@ -15,12 +15,12 @@
*/
import * as fs from 'fs';
import * as folio from 'folio';
import { test as base } from './internal';
import type { LaunchOptions, BrowserContextOptions, Page } from '../../types/types';
import type { PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test';
export * from 'folio';
export const test = folio.test.extend<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>({
export * from './internal';
export const test = base.extend<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>({
defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
playwright: [ require('../inprocess'), { scope: 'worker' } ],
@ -149,5 +149,3 @@ export const test = folio.test.extend<PlaywrightTestArgs & PlaywrightTestOptions
},
});
export default test;
export const __baseTest = folio.test;

25
src/test/internal.ts

@ -0,0 +1,25 @@
/**
* Copyright 2019 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 type { TestType } from './types';
import { rootTestType } from './testType';
export type { Project, Config, TestStatus, TestInfo, WorkerInfo, TestType, Fixtures, TestFixture, WorkerFixture } from './types';
export { expect } from './expect';
export const test: TestType<{}, {}> = rootTestType.test;
export default test;

67
src/test/ipc.ts

@ -0,0 +1,67 @@
/**
* 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.
*/
import type { TestError } from './reporter';
import type { Config, TestStatus } from './types';
export type SerializedLoaderData = {
defaultConfig: Config;
overrides: Config;
configFile: { file: string } | { rootDir: string };
};
export type WorkerInitParams = {
workerIndex: number;