diff --git a/docs/typescript.md b/docs/typescript.md index d74e80bf2..220b9cb8b 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -9,7 +9,7 @@ npm i -D ts-node And include Testplane types in your `tsconfig.json` file: -```js +```json // tsconfig.json { // other tsconfig options @@ -23,7 +23,26 @@ And include Testplane types in your `tsconfig.json` file: } ``` -Now you will be able to write Testplane tests using typescript. +Recommended config: + +```json +{ + "compilerOptions": { + "types": [ + "testplane" + ], + "sourceMap": true, + "outDir": "../test-dist", + "target": "ESNext", + "module": "CommonJS", + "strict": true, + "lib": ["esnext", "dom"], + "esModuleInterop": true + } +} +``` + +Note: this is the strictest possible setting, which works on Typescript 5.3+. If you want faster type-checks or have older Typescript version, use `"skipLibCheck": true` in `compilerOptions`. ### testplane.ctx typings diff --git a/package-lock.json b/package-lock.json index 61afb47e6..66e8c4294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "MIT", "dependencies": { "@babel/code-frame": "7.24.2", - "@gemini-testing/commander": "2.15.3", + "@gemini-testing/commander": "2.15.4", "@jspm/core": "2.0.1", + "@types/debug": "4.1.12", + "@types/yallist": "4.0.4", "@wdio/globals": "8.39.0", "@wdio/protocols": "8.38.0", "@wdio/types": "8.39.0", @@ -33,7 +35,7 @@ "import-meta-resolve": "4.0.0", "local-pkg": "0.4.3", "lodash": "4.17.21", - "looks-same": "9.0.0", + "looks-same": "9.0.1", "micromatch": "4.0.5", "mocha": "10.2.0", "plugins-loader": "1.3.3", @@ -74,9 +76,11 @@ "@types/browserify": "12.0.40", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", - "@types/debug": "4.1.12", + "@types/clear-require": "3.2.1", + "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", "@types/lodash": "4.14.191", + "@types/micromatch": "4.0.9", "@types/mocha": "10.0.1", "@types/node": "18.19.3", "@types/proxyquire": "1.3.28", @@ -1707,9 +1711,9 @@ } }, "node_modules/@gemini-testing/commander": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/@gemini-testing/commander/-/commander-2.15.3.tgz", - "integrity": "sha512-HPV73pHeL7BozcIaJTJVXIY3kwoAscy+yVVehG0QqVhfmdx6RPqpQ9KIFwV1amlBMObXvcS3gI7LojlwiI9eXw==" + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@gemini-testing/commander/-/commander-2.15.4.tgz", + "integrity": "sha512-GIvIknEbJccKMv2KCgYOOZPy4QgR3/8csvds/WCUGEJPkghHz6VrziG7cBaB4n91PsFEpOwU+uJqXun5sEBpwg==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.7", @@ -2677,6 +2681,12 @@ "integrity": "sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg==", "dev": true }, + "node_modules/@types/braces": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.4.tgz", + "integrity": "sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==", + "dev": true + }, "node_modules/@types/browserify": { "version": "12.0.40", "resolved": "https://registry.npmjs.org/@types/browserify/-/browserify-12.0.40.tgz", @@ -2713,6 +2723,16 @@ "@types/chai": "*" } }, + "node_modules/@types/clear-require": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/clear-require/-/clear-require-3.2.1.tgz", + "integrity": "sha512-7a4vxYoD0Fx2ZeCB+3QQcFp+qGc6xZQKvQ8XKISTXcPZ+VCZg1P1Ps+biY9W1Y5xJnYPbWZ8ZH+b57h9XeWnjQ==", + "deprecated": "This is a stub types definition. clear-require provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "clear-require": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -2739,11 +2759,20 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, "dependencies": { "@types/ms": "*" } }, + "node_modules/@types/escape-string-regexp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/escape-string-regexp/-/escape-string-regexp-2.0.1.tgz", + "integrity": "sha512-QbTY9C4ZdLxqnJ+2vo8IBxMXcC3t//eVTNpnQVkTlVYtuQyZkUTNbFAwtXZ0qJ+YFpib2+raDMtYWh8hBfhlgQ==", + "deprecated": "This is a stub types definition. escape-string-regexp provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "escape-string-regexp": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2832,6 +2861,15 @@ "@types/unist": "*" } }, + "node_modules/@types/micromatch": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", + "dev": true, + "dependencies": { + "@types/braces": "*" + } + }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -2847,8 +2885,7 @@ "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { "version": "18.19.3", @@ -2944,6 +2981,11 @@ "@types/node": "*" } }, + "node_modules/@types/yallist": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/yallist/-/yallist-4.0.4.tgz", + "integrity": "sha512-Gz8gcZggNjZfXsDKBtESUJfiLwxtdUTd2c+M0F/PfBeF6pyWHTjCW5JvoBMsPOmecJ27g3aUtb7I5uRJqifZOw==" + }, "node_modules/@types/yargs": { "version": "17.0.15", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.15.tgz", @@ -11333,9 +11375,9 @@ } }, "node_modules/looks-same": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-9.0.0.tgz", - "integrity": "sha512-8UI1PxtatkOJ8xLSsd0Pl2CwUKu0L6KqmF4MYqXJXny81lEv41TrjdOX+EeKpbKVa1R1Ffn0ROP6HglZ5Ex1fg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-9.0.1.tgz", + "integrity": "sha512-V+vsT22nLIUdmvxr6jxsbafpJaZvLFnwZhV7BbmN38+v6gL+/BaHnwK9z5UURhDNSOrj3baOgbwzpjINqoZCpA==", "dependencies": { "color-diff": "^1.1.0", "fs-extra": "^8.1.0", @@ -18006,9 +18048,9 @@ } }, "@gemini-testing/commander": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/@gemini-testing/commander/-/commander-2.15.3.tgz", - "integrity": "sha512-HPV73pHeL7BozcIaJTJVXIY3kwoAscy+yVVehG0QqVhfmdx6RPqpQ9KIFwV1amlBMObXvcS3gI7LojlwiI9eXw==" + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@gemini-testing/commander/-/commander-2.15.4.tgz", + "integrity": "sha512-GIvIknEbJccKMv2KCgYOOZPy4QgR3/8csvds/WCUGEJPkghHz6VrziG7cBaB4n91PsFEpOwU+uJqXun5sEBpwg==" }, "@humanwhocodes/config-array": { "version": "0.10.7", @@ -18670,6 +18712,12 @@ "integrity": "sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg==", "dev": true }, + "@types/braces": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.4.tgz", + "integrity": "sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==", + "dev": true + }, "@types/browserify": { "version": "12.0.40", "resolved": "https://registry.npmjs.org/@types/browserify/-/browserify-12.0.40.tgz", @@ -18706,6 +18754,15 @@ "@types/chai": "*" } }, + "@types/clear-require": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/clear-require/-/clear-require-3.2.1.tgz", + "integrity": "sha512-7a4vxYoD0Fx2ZeCB+3QQcFp+qGc6xZQKvQ8XKISTXcPZ+VCZg1P1Ps+biY9W1Y5xJnYPbWZ8ZH+b57h9XeWnjQ==", + "dev": true, + "requires": { + "clear-require": "*" + } + }, "@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -18732,11 +18789,19 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, "requires": { "@types/ms": "*" } }, + "@types/escape-string-regexp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/escape-string-regexp/-/escape-string-regexp-2.0.1.tgz", + "integrity": "sha512-QbTY9C4ZdLxqnJ+2vo8IBxMXcC3t//eVTNpnQVkTlVYtuQyZkUTNbFAwtXZ0qJ+YFpib2+raDMtYWh8hBfhlgQ==", + "dev": true, + "requires": { + "escape-string-regexp": "*" + } + }, "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -18825,6 +18890,15 @@ "@types/unist": "*" } }, + "@types/micromatch": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", + "dev": true, + "requires": { + "@types/braces": "*" + } + }, "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -18840,8 +18914,7 @@ "@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "@types/node": { "version": "18.19.3", @@ -18937,6 +19010,11 @@ "@types/node": "*" } }, + "@types/yallist": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/yallist/-/yallist-4.0.4.tgz", + "integrity": "sha512-Gz8gcZggNjZfXsDKBtESUJfiLwxtdUTd2c+M0F/PfBeF6pyWHTjCW5JvoBMsPOmecJ27g3aUtb7I5uRJqifZOw==" + }, "@types/yargs": { "version": "17.0.15", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.15.tgz", @@ -25031,9 +25109,9 @@ "dev": true }, "looks-same": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-9.0.0.tgz", - "integrity": "sha512-8UI1PxtatkOJ8xLSsd0Pl2CwUKu0L6KqmF4MYqXJXny81lEv41TrjdOX+EeKpbKVa1R1Ffn0ROP6HglZ5Ex1fg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-9.0.1.tgz", + "integrity": "sha512-V+vsT22nLIUdmvxr6jxsbafpJaZvLFnwZhV7BbmN38+v6gL+/BaHnwK9z5UURhDNSOrj3baOgbwzpjINqoZCpA==", "requires": { "color-diff": "^1.1.0", "fs-extra": "^8.1.0", diff --git a/package.json b/package.json index 1b7881f17..aa829310d 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,10 @@ "license": "MIT", "dependencies": { "@babel/code-frame": "7.24.2", - "@gemini-testing/commander": "2.15.3", + "@gemini-testing/commander": "2.15.4", "@jspm/core": "2.0.1", - "@types/mocha": "10.0.1", + "@types/debug": "4.1.12", + "@types/yallist": "4.0.4", "@wdio/globals": "8.39.0", "@wdio/protocols": "8.38.0", "@wdio/types": "8.39.0", @@ -76,7 +77,7 @@ "import-meta-resolve": "4.0.0", "local-pkg": "0.4.3", "lodash": "4.17.21", - "looks-same": "9.0.0", + "looks-same": "9.0.1", "micromatch": "4.0.5", "mocha": "10.2.0", "plugins-loader": "1.3.3", @@ -113,9 +114,12 @@ "@types/browserify": "12.0.40", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", - "@types/debug": "4.1.12", + "@types/clear-require": "3.2.1", + "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", "@types/lodash": "4.14.191", + "@types/micromatch": "4.0.9", + "@types/mocha": "10.0.1", "@types/node": "18.19.3", "@types/proxyquire": "1.3.28", "@types/sharp": "0.31.1", diff --git a/src/browser-pool/basic-pool.js b/src/browser-pool/basic-pool.ts similarity index 54% rename from src/browser-pool/basic-pool.js rename to src/browser-pool/basic-pool.ts index 20bcd75ab..82a3120a0 100644 --- a/src/browser-pool/basic-pool.js +++ b/src/browser-pool/basic-pool.ts @@ -1,29 +1,35 @@ -"use strict"; - -const _ = require("lodash"); -const Browser = require("../browser/new-browser"); -const { CancelledError } = require("./cancelled-error"); -const { MasterEvents } = require("../events"); -const Pool = require("./pool"); -const debug = require("debug"); - -module.exports = class BasicPool extends Pool { - static create(config, emitter) { +import debug from "debug"; +import _ from "lodash"; + +import { NewBrowser } from "../browser/new-browser"; +import { CancelledError } from "./cancelled-error"; +import { AsyncEmitter, MasterEvents } from "../events"; +import { BrowserOpts, Pool } from "./types"; +import { Config } from "../config"; +import { Browser } from "../browser/browser"; + +export class BasicPool implements Pool { + private _config: Config; + private _emitter: AsyncEmitter; + private _activeSessions: Record; + private _cancelled: boolean; + log: debug.Debugger; + + static create(config: Config, emitter: AsyncEmitter): BasicPool { return new BasicPool(config, emitter); } - constructor(config, emitter) { - super(); - + constructor(config: Config, emitter: AsyncEmitter) { this._config = config; this._emitter = emitter; this.log = debug("testplane:pool:basic"); this._activeSessions = {}; + this._cancelled = false; } - async getBrowser(id, opts = {}) { - const browser = Browser.create(this._config, { ...opts, id }); + async getBrowser(id: string, opts: BrowserOpts = {}): Promise { + const browser = NewBrowser.create(this._config, { ...opts, id }); try { await browser.init(); @@ -48,32 +54,33 @@ module.exports = class BasicPool extends Pool { } } - async freeBrowser(browser) { + async freeBrowser(browser: NewBrowser): Promise { delete this._activeSessions[browser.sessionId]; this.log(`stop browser ${browser.fullId}`); try { await this._emit(MasterEvents.SESSION_END, browser); - } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { console.warn((err && err.stack) || err); } await browser.quit(); } - _emit(event, browser) { + private _emit(event: string, browser: Browser): Promise { return this._emitter.emitAndWait(event, browser.publicAPI, { browserId: browser.id, sessionId: browser.sessionId, }); } - cancel() { + cancel(): void { this._cancelled = true; _.forEach(this._activeSessions, browser => browser.quit()); this._activeSessions = {}; } -}; +} diff --git a/src/browser-pool/caching-pool.js b/src/browser-pool/caching-pool.ts similarity index 65% rename from src/browser-pool/caching-pool.js rename to src/browser-pool/caching-pool.ts index e57529461..767a9addd 100644 --- a/src/browser-pool/caching-pool.js +++ b/src/browser-pool/caching-pool.ts @@ -1,27 +1,28 @@ -"use strict"; +import debug from "debug"; -const Promise = require("bluebird"); -const Pool = require("./pool"); -const LimitedUseSet = require("./limited-use-set"); -const debug = require("debug"); -const { buildCompositeBrowserId } = require("./utils"); +import { LimitedUseSet } from "./limited-use-set"; +import { buildCompositeBrowserId } from "./utils"; +import { BasicPool } from "./basic-pool"; +import { Config } from "../config"; +import { Pool } from "./types"; +import { NewBrowser } from "../browser/new-browser"; -module.exports = class CachingPool extends Pool { - /** - * @constructor - * @extends BasicPool - * @param {BasicPool} underlyingPool - */ - constructor(underlyingPool, config) { - super(); +export type FreeBrowserOpts = { hasFreeSlots?: boolean; force?: boolean; compositeIdForNextRequest?: string }; + +export class CachingPool implements Pool { + private _caches: Record>; + private _config: Config; + underlyingPool: BasicPool; + log: debug.Debugger; + constructor(underlyingPool: BasicPool, config: Config) { this.log = debug("testplane:pool:caching"); this.underlyingPool = underlyingPool; this._caches = {}; this._config = config; } - _getCacheFor(id, version) { + private _getCacheFor(id: string, version?: string): LimitedUseSet { const compositeId = buildCompositeBrowserId(id, version); this.log(`request for ${compositeId}`); @@ -34,7 +35,7 @@ module.exports = class CachingPool extends Pool { return this._caches[compositeId]; } - getBrowser(id, opts = {}) { + async getBrowser(id: string, opts: { version?: string } = {}): Promise { const { version } = opts; const cache = this._getCacheFor(id, version); const browser = cache.pop(); @@ -50,19 +51,21 @@ module.exports = class CachingPool extends Pool { return browser .reset() .catch(e => { - const reject = Promise.reject.bind(null, e); - return this.underlyingPool.freeBrowser(browser).then(reject, reject); + return this.underlyingPool.freeBrowser(browser).then( + () => Promise.reject(e), + () => Promise.reject(e), + ); }) .then(() => browser); } - _initPool(browserId, version) { + private _initPool(browserId: string, version?: string): void { const compositeId = buildCompositeBrowserId(browserId, version); const freeBrowser = this.underlyingPool.freeBrowser.bind(this.underlyingPool); const { testsPerSession } = this._config.forBrowser(browserId); - this._caches[compositeId] = new LimitedUseSet({ - formatItem: item => item.fullId, + this._caches[compositeId] = new LimitedUseSet({ + formatItem: (item): string => item.fullId, // browser does not get put in a set on first usages, so if // we want to limit it usage to N times, we must set N-1 limit // for the set. @@ -79,8 +82,8 @@ module.exports = class CachingPool extends Pool { * not be cached * @returns {Promise} */ - freeBrowser(browser, options = {}) { - const shouldFreeForNextRequest = () => { + async freeBrowser(browser: NewBrowser, options: FreeBrowserOpts = {}): Promise { + const shouldFreeForNextRequest = (): boolean => { const { compositeIdForNextRequest } = options; if (!compositeIdForNextRequest) { @@ -88,7 +91,7 @@ module.exports = class CachingPool extends Pool { } const { hasFreeSlots } = options; - const hasCacheForNextRequest = this._caches[options.compositeIdForNextRequest]; + const hasCacheForNextRequest = this._caches[options.compositeIdForNextRequest!]; return !hasFreeSlots && !hasCacheForNextRequest; }; @@ -105,8 +108,8 @@ module.exports = class CachingPool extends Pool { return cache.push(browser); } - cancel() { + cancel(): void { this.log("cancel"); this.underlyingPool.cancel(); } -}; +} diff --git a/src/browser-pool/cancelled-error.ts b/src/browser-pool/cancelled-error.ts index fd7f931d9..7973022e9 100644 --- a/src/browser-pool/cancelled-error.ts +++ b/src/browser-pool/cancelled-error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class CancelledError extends Error { name = "CancelledError"; message = "Browser request was cancelled"; diff --git a/src/browser-pool/index.ts b/src/browser-pool/index.ts index 9797288e0..e9791860b 100644 --- a/src/browser-pool/index.ts +++ b/src/browser-pool/index.ts @@ -1,8 +1,8 @@ import _ from "lodash"; -import BasicPool from "./basic-pool"; -import LimitedPool from "./limited-pool"; -import PerBrowserLimitedPool from "./per-browser-limited-pool"; -import CachingPool from "./caching-pool"; +import { BasicPool } from "./basic-pool"; +import { LimitedPool } from "./limited-pool"; +import { PerBrowserLimitedPool } from "./per-browser-limited-pool"; +import { CachingPool } from "./caching-pool"; import { Config } from "../config"; import { AsyncEmitter } from "../events"; diff --git a/src/browser-pool/limited-pool.js b/src/browser-pool/limited-pool.ts similarity index 62% rename from src/browser-pool/limited-pool.js rename to src/browser-pool/limited-pool.ts index aeea31e5a..c4ef79701 100644 --- a/src/browser-pool/limited-pool.js +++ b/src/browser-pool/limited-pool.ts @@ -1,26 +1,42 @@ -"use strict"; - -const _ = require("lodash"); -const Promise = require("bluebird"); -const yallist = require("yallist"); -const Pool = require("./pool"); -const { CancelledError } = require("./cancelled-error"); -const debug = require("debug"); -const { buildCompositeBrowserId } = require("./utils"); - -module.exports = class LimitedPool extends Pool { - static create(underlyingPool, opts) { +import debug from "debug"; +import _ from "lodash"; +import yallist from "yallist"; + +import { BrowserOpts, Pool } from "./types"; +import { CancelledError } from "./cancelled-error"; +import { buildCompositeBrowserId } from "./utils"; +import { Browser } from "../browser/browser"; + +export interface LimitedPoolOpts { + limit: number; + isSpecificBrowserLimiter?: boolean; +} + +interface QueueItem { + id: string; + opts: { + force?: boolean; + version?: string; + }; + resolve: (value: unknown) => void; + reject: (value: unknown) => void; +} + +export class LimitedPool implements Pool { + private _limit: number; + private _launched: number; + private _requests: number; + private _requestQueue: yallist; + private _highPriorityRequestQueue: yallist; + private _isSpecificBrowserLimiter: boolean; + log: debug.Debugger; + underlyingPool: Pool; + + static create(underlyingPool: Pool, opts: LimitedPoolOpts): LimitedPool { return new LimitedPool(underlyingPool, opts); } - /** - * @extends BasicPool - * @param {Number} limit - * @param {BasicPool} underlyingPool - */ - constructor(underlyingPool, opts) { - super(); - + constructor(underlyingPool: Pool, opts: LimitedPoolOpts) { this.log = debug("testplane:pool:limited"); this.underlyingPool = underlyingPool; @@ -34,18 +50,20 @@ module.exports = class LimitedPool extends Pool { : true; } - getBrowser(id, opts = {}) { + async getBrowser(id: string, opts: BrowserOpts = {}): Promise { const optsToPrint = JSON.stringify(opts); this.log(`get browser ${id} with opts:${optsToPrint} (launched ${this._launched}, limit ${this._limit})`); ++this._requests; - return this._getBrowser(id, opts).catch(e => { + try { + return await this._getBrowser(id, opts); + } catch (e) { --this._requests; - return Promise.reject(e); - }); + return await Promise.reject(e); + } } - freeBrowser(browser, opts = {}) { + async freeBrowser(browser: Browser, opts: BrowserOpts = {}): Promise { --this._requests; const nextRequest = this._lookAtNextRequest(); @@ -61,10 +79,10 @@ module.exports = class LimitedPool extends Pool { return this.underlyingPool.freeBrowser(browser, optsForFree).finally(() => this._launchNextBrowser()); } - cancel() { + cancel(): void { this.log("cancel"); - const reject_ = entry => entry.reject(new CancelledError()); + const reject_ = (entry: QueueItem): void => entry.reject(new CancelledError()); this._highPriorityRequestQueue.forEach(reject_); this._requestQueue.forEach(reject_); @@ -74,7 +92,7 @@ module.exports = class LimitedPool extends Pool { this.underlyingPool.cancel(); } - _getBrowser(id, opts = {}) { + private async _getBrowser(id: string, opts: BrowserOpts = {}): Promise { if (this._launched < this._limit) { this.log("can launch one more"); this._launched++; @@ -86,15 +104,11 @@ module.exports = class LimitedPool extends Pool { const queue = opts.highPriority ? this._highPriorityRequestQueue : this._requestQueue; return new Promise((resolve, reject) => { - queue.push({ id, opts, resolve, reject }); + queue.push({ id, opts, resolve: resolve as QueueItem["resolve"], reject: reject as QueueItem["resolve"] }); }); } - /** - * @param {String} id - * @returns {Promise} - */ - _newBrowser(id, opts) { + private async _newBrowser(id: string, opts: object): Promise { this.log(`launching new browser ${id} with opts:${JSON.stringify(opts)}`); return this.underlyingPool.getBrowser(id, opts).catch(e => { @@ -103,11 +117,11 @@ module.exports = class LimitedPool extends Pool { }); } - _lookAtNextRequest() { + private _lookAtNextRequest(): QueueItem | undefined { return this._highPriorityRequestQueue.get(0) || this._requestQueue.get(0); } - _launchNextBrowser() { + private _launchNextBrowser(): void { const queued = this._highPriorityRequestQueue.shift() || this._requestQueue.shift(); if (queued) { @@ -121,4 +135,4 @@ module.exports = class LimitedPool extends Pool { this._launched--; } } -}; +} diff --git a/src/browser-pool/limited-use-set.js b/src/browser-pool/limited-use-set.ts similarity index 66% rename from src/browser-pool/limited-use-set.js rename to src/browser-pool/limited-use-set.ts index 760a89103..d397438ce 100644 --- a/src/browser-pool/limited-use-set.js +++ b/src/browser-pool/limited-use-set.ts @@ -1,21 +1,27 @@ -"use strict"; - -const _ = require("lodash"); -const debug = require("debug"); +import _ from "lodash"; +import debug from "debug"; /** * Set implementation which allows to get and put an object * there only limited amout of times. After limit is reached * attempt to put an object there causes the object to be finalized. - * - * @constructor - * @param {Number} useLimit number of times object can be popped from set - * before finalizing. - * @param {Function} finalize callback which will be called when value in - * set needs to be finalized. */ -module.exports = class LimitedUseSet { - constructor(opts) { + +export interface LimitedUseSetOpts { + useLimit: number; + finalize: (value: T) => Promise; + formatItem: (value: T) => string; +} + +export class LimitedUseSet { + private _useCounts: WeakMap; + private _useLimit: number; + private _finalize: (value: T) => Promise; + private _formatItem: (value: T) => string; + private _objects: T[]; + log: debug.Debugger; + + constructor(opts: LimitedUseSetOpts) { this._useCounts = new WeakMap(); this._useLimit = opts.useLimit; this._finalize = opts.finalize; @@ -25,7 +31,7 @@ module.exports = class LimitedUseSet { this.log = debug("testplane:pool:limited-use-set"); } - push(value) { + push(value: T): Promise { const formatedItem = this._formatItem(value); this.log(`push ${formatedItem}`); @@ -41,20 +47,20 @@ module.exports = class LimitedUseSet { return Promise.resolve(); } - _isOverLimit(value) { + private _isOverLimit(value: T): boolean { if (this._useLimit === 0) { return true; } - return this._useCounts.has(value) && this._useCounts.get(value) >= this._useLimit; + return this._useCounts.has(value) && this._useCounts.get(value)! >= this._useLimit; } - pop() { + pop(): T | null { if (this._objects.length === 0) { return null; } - const result = this._objects.pop(); + const result = this._objects.pop()!; const useCount = this._useCounts.get(result) || 0; const formatedItem = this._formatItem(result); @@ -65,4 +71,4 @@ module.exports = class LimitedUseSet { return result; } -}; +} diff --git a/src/browser-pool/per-browser-limited-pool.js b/src/browser-pool/per-browser-limited-pool.js deleted file mode 100644 index b17701dbb..000000000 --- a/src/browser-pool/per-browser-limited-pool.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const Pool = require("./pool"); -const LimitedPool = require("./limited-pool"); -const debug = require("debug"); - -module.exports = class PerBrowserLimitedPool extends Pool { - constructor(underlyingPool, config) { - super(); - - this.log = debug("testplane:pool:per-browser-limited"); - - const ids = config.getBrowserIds(); - this._browserPools = _.zipObject( - ids, - ids.map(id => - LimitedPool.create(underlyingPool, { - limit: config.forBrowser(id).sessionsPerBrowser, - }), - ), - ); - } - - getBrowser(id, opts) { - this.log(`request ${id} with opts: ${JSON.stringify(opts)}`); - - return this._browserPools[id].getBrowser(id, opts); - } - - freeBrowser(browser, opts) { - this.log(`free ${browser.fullId}`); - - return this._browserPools[browser.id].freeBrowser(browser, opts); - } - - cancel() { - this.log("cancel"); - _.forEach(this._browserPools, pool => pool.cancel()); - } -}; diff --git a/src/browser-pool/per-browser-limited-pool.ts b/src/browser-pool/per-browser-limited-pool.ts new file mode 100644 index 000000000..6ea76a5c2 --- /dev/null +++ b/src/browser-pool/per-browser-limited-pool.ts @@ -0,0 +1,43 @@ +import debug from "debug"; +import { zipObject, forEach } from "lodash"; + +import { Pool } from "./types"; +import { Config } from "../config"; +import { Browser } from "../browser/browser"; +import { LimitedPool } from "./limited-pool"; + +export class PerBrowserLimitedPool implements Pool { + log: debug.Debugger; + private _browserPools: Record; + + constructor(underlyingPool: Pool, config: Config) { + this.log = debug("testplane:pool:per-browser-limited"); + + const ids = config.getBrowserIds(); + this._browserPools = zipObject( + ids, + ids.map(id => + LimitedPool.create(underlyingPool, { + limit: config.forBrowser(id).sessionsPerBrowser, + }), + ), + ); + } + + getBrowser(id: string, opts?: object): Promise { + this.log(`request ${id} with opts: ${JSON.stringify(opts)}`); + + return this._browserPools[id].getBrowser(id, opts); + } + + freeBrowser(browser: Browser, opts?: object): Promise { + this.log(`free ${browser.fullId}`); + + return this._browserPools[browser.id].freeBrowser(browser, opts); + } + + cancel(): void { + this.log("cancel"); + forEach(this._browserPools, pool => pool.cancel()); + } +} diff --git a/src/browser-pool/pool.js b/src/browser-pool/pool.js deleted file mode 100644 index 6bc5cd366..000000000 --- a/src/browser-pool/pool.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; - -module.exports = class Pool { - /** - * @returns {Promise.} - */ - // eslint-disable-next-line @typescript-eslint/no-empty-function - getBrowser() {} - - /** - * @param {Browser} browser - * @returns {Promise} - */ - freeBrowser() { - return Promise.resolve(); - } - - // eslint-disable-next-line @typescript-eslint/no-empty-function - cancel() {} -}; diff --git a/src/browser-pool/types.ts b/src/browser-pool/types.ts new file mode 100644 index 000000000..485db8f5a --- /dev/null +++ b/src/browser-pool/types.ts @@ -0,0 +1,13 @@ +import { Browser } from "../browser/browser"; + +export interface Pool { + getBrowser(id: string, opts?: object): Promise; + freeBrowser(browser: T, opts?: object): Promise; + cancel(): void; +} + +export interface BrowserOpts { + force?: boolean; + version?: string; + highPriority?: boolean; +} diff --git a/src/browser-pool/utils.js b/src/browser-pool/utils.js deleted file mode 100644 index 1aff4a67c..000000000 --- a/src/browser-pool/utils.js +++ /dev/null @@ -1 +0,0 @@ -exports.buildCompositeBrowserId = (browserId, version) => (version ? `${browserId}.${version}` : browserId); diff --git a/src/browser-pool/utils.ts b/src/browser-pool/utils.ts new file mode 100644 index 000000000..75470a97c --- /dev/null +++ b/src/browser-pool/utils.ts @@ -0,0 +1,2 @@ +export const buildCompositeBrowserId = (browserId: string, version?: string): string => + version ? `${browserId}.${version}` : browserId; diff --git a/src/browser/browser.js b/src/browser/browser.js deleted file mode 100644 index 12af80ebf..000000000 --- a/src/browser/browser.js +++ /dev/null @@ -1,159 +0,0 @@ -"use strict"; - -const crypto = require("crypto"); -const _ = require("lodash"); -const { SAVE_HISTORY_MODE } = require("../constants/config"); -const { X_REQUEST_ID_DELIMITER } = require("../constants/browser"); -const history = require("./history"); -const stacktrace = require("./stacktrace"); -const { getBrowserCommands, getElementCommands } = require("./history/commands"); -const addRunStepCommand = require("./commands/runStep").default; - -const CUSTOM_SESSION_OPTS = [ - "outputDir", - "agent", - "headers", - "transformRequest", - "transformResponse", - "strictSSL", - // cloud service opts - "user", - "key", - "region", -]; - -module.exports = class Browser { - static create(config, opts) { - return new this(config, opts); - } - - constructor(config, opts) { - this.id = opts.id; - this.version = opts.version; - - this._config = config.forBrowser(this.id); - this._debug = config.system.debug; - this._session = null; - this._callstackHistory = null; - this._state = { - ...opts.state, - isBroken: false, - }; - this._customCommands = new Set(); - } - - setHttpTimeout(timeout) { - if (timeout === null) { - timeout = this._config.httpTimeout; - } - - this._session.extendOptions({ connectionRetryTimeout: timeout }); - } - - restoreHttpTimeout() { - this.setHttpTimeout(this._config.httpTimeout); - } - - applyState(state) { - _.extend(this._state, state); - } - - _addCommands() { - this._addExtendOptionsMethod(this._session); - } - - _addSteps() { - addRunStepCommand(this); - } - - _extendStacktrace() { - stacktrace.enhanceStacktraces(this._session); - } - - _addHistory() { - if (this._config.saveHistoryMode !== SAVE_HISTORY_MODE.NONE) { - this._callstackHistory = history.initCommandHistory(this._session); - } - } - - _addExtendOptionsMethod(session) { - session.addCommand("extendOptions", opts => { - _.extend(session.options, opts); - }); - } - - _getSessionOptsFromConfig(optNames = CUSTOM_SESSION_OPTS) { - return optNames.reduce((options, optName) => { - if (optName === "transformRequest") { - options[optName] = req => { - if (!_.isNull(this._config[optName])) { - req = this._config[optName](req); - } - - if (!req.headers["X-Request-ID"]) { - req.headers["X-Request-ID"] = `${ - this.state.testXReqId - }${X_REQUEST_ID_DELIMITER}${crypto.randomUUID()}`; - } - if (!req.headers["traceparent"] && this.state.traceparent) { - req.headers["traceparent"] = this.state.traceparent; - } - - return req; - }; - } else if (!_.isNull(this._config[optName])) { - options[optName] = this._config[optName]; - } - - return options; - }, {}); - } - - _startCollectingCustomCommands() { - const browserCommands = getBrowserCommands(); - const elementCommands = getElementCommands(); - - this._session.overwriteCommand("addCommand", (origCommand, name, wrapper, elementScope, ...rest) => { - const isKnownCommand = elementScope ? elementCommands.includes(name) : browserCommands.includes(name); - - if (!isKnownCommand) { - this._customCommands.add({ name, elementScope: Boolean(elementScope) }); - } - - return origCommand(name, wrapper, elementScope, ...rest); - }); - } - - get fullId() { - return this.version ? `${this.id}.${this.version}` : this.id; - } - - get publicAPI() { - return this._session; // exposing webdriver API as is - } - - get sessionId() { - return this.publicAPI.sessionId; - } - - get config() { - return this._config; - } - - get state() { - return this._state; - } - - get capabilities() { - return this.publicAPI.capabilities; - } - - get callstackHistory() { - return this._callstackHistory; - } - - get customCommands() { - const allCustomCommands = Array.from(this._customCommands); - return _.uniqWith(allCustomCommands, _.isEqual); - } -}; diff --git a/src/browser/browser.ts b/src/browser/browser.ts new file mode 100644 index 000000000..c2f4d6efc --- /dev/null +++ b/src/browser/browser.ts @@ -0,0 +1,193 @@ +import crypto from "crypto"; +import { RequestOptions } from "https"; + +import { RemoteCapability } from "@wdio/types/build/Capabilities"; +import _ from "lodash"; + +import { SAVE_HISTORY_MODE } from "../constants/config"; +import { X_REQUEST_ID_DELIMITER } from "../constants/browser"; +import history from "./history"; +import { enhanceStacktraces } from "./stacktrace"; +import { getBrowserCommands, getElementCommands } from "./history/commands"; +import addRunStepCommand from "./commands/runStep"; +import { Config } from "../config"; +import { AsyncEmitter } from "../events"; +import { BrowserConfig } from "../config/browser-config"; +import Callstack from "./history/callstack"; + +const CUSTOM_SESSION_OPTS = [ + "outputDir", + "agent", + "headers", + "transformRequest", + "transformResponse", + "strictSSL", + // cloud service opts + "user", + "key", + "region", +]; + +export type BrowserOpts = { + id: string; + version?: string; + state?: Record; + emitter?: AsyncEmitter; +}; + +export type BrowserState = { + testXReqId?: string; + traceparent?: string; + isBroken?: boolean; +}; + +export type CustomCommend = { name: string; elementScope: boolean }; + +export class Browser { + protected _config: BrowserConfig; + protected _debug: boolean; + protected _session: WebdriverIO.Browser | null; + protected _callstackHistory: Callstack | null; + protected _state: BrowserState; + protected _customCommands: Set; + id: string; + version?: string; + + static create( + this: new (config: Config, opts: BrowserOpts) => T, + config: Config, + opts: BrowserOpts, + ): T { + return new this(config, opts); + } + + constructor(config: Config, opts: BrowserOpts) { + this.id = opts.id; + this.version = opts.version; + + this._config = config.forBrowser(this.id); + this._debug = config.system.debug; + this._session = null; + this._callstackHistory = null; + this._state = { + ...opts.state, + isBroken: false, + }; + this._customCommands = new Set(); + } + + setHttpTimeout(timeout: number | null): void { + if (timeout === null) { + timeout = this._config.httpTimeout; + } + + this._session!.extendOptions({ connectionRetryTimeout: timeout }); + } + + restoreHttpTimeout(): void { + this.setHttpTimeout(this._config.httpTimeout); + } + + applyState(state: Record): void { + _.extend(this._state, state); + } + + protected _addCommands(): void { + this._addExtendOptionsMethod(this._session!); + } + + protected _addSteps(): void { + addRunStepCommand(this); + } + + protected _extendStacktrace(): void { + enhanceStacktraces(this._session!); + } + + protected _addHistory(): void { + if (this._config.saveHistoryMode !== SAVE_HISTORY_MODE.NONE) { + this._callstackHistory = history.initCommandHistory(this._session); + } + } + + protected _addExtendOptionsMethod(session: WebdriverIO.Browser): void { + session.addCommand("extendOptions", opts => { + _.extend(session.options, opts); + }); + } + + protected _getSessionOptsFromConfig(optNames = CUSTOM_SESSION_OPTS): Record { + return optNames.reduce((options: Record, optName) => { + if (optName === "transformRequest") { + options[optName] = (req: RequestOptions): RequestOptions => { + if (!_.isNull(this._config[optName])) { + req = this._config[optName](req); + } + + if (!req.headers!["X-Request-ID"]) { + req.headers!["X-Request-ID"] = `${ + this.state.testXReqId + }${X_REQUEST_ID_DELIMITER}${crypto.randomUUID()}`; + } + if (!req.headers!["traceparent"] && this.state.traceparent) { + req.headers!["traceparent"] = this.state.traceparent; + } + + return req; + }; + } else if (!_.isNull(this._config[optName as keyof BrowserConfig])) { + options[optName] = this._config[optName as keyof BrowserConfig]; + } + + return options; + }, {}); + } + + protected _startCollectingCustomCommands(): void { + const browserCommands = getBrowserCommands(); + const elementCommands = getElementCommands(); + + this._session!.overwriteCommand("addCommand", (origCommand, name, wrapper, elementScope, ...rest) => { + const isKnownCommand = elementScope ? elementCommands.includes(name) : browserCommands.includes(name); + + if (!isKnownCommand) { + this._customCommands.add({ name, elementScope: Boolean(elementScope) }); + } + + return origCommand(name, wrapper, elementScope, ...rest); + }); + } + + get fullId(): string { + return this.version ? `${this.id}.${this.version}` : this.id; + } + + get publicAPI(): WebdriverIO.Browser { + return this._session!; // exposing webdriver API as is + } + + get sessionId(): string { + return this.publicAPI.sessionId; + } + + get config(): BrowserConfig { + return this._config; + } + + get state(): BrowserState { + return this._state; + } + + get capabilities(): RemoteCapability { + return this.publicAPI.capabilities; + } + + get callstackHistory(): Callstack { + return this._callstackHistory!; + } + + get customCommands(): CustomCommend[] { + const allCustomCommands = Array.from(this._customCommands); + return _.uniqWith(allCustomCommands, _.isEqual); + } +} diff --git a/src/browser/client-bridge/error.ts b/src/browser/client-bridge/error.ts index 8e373825a..170e46585 100644 --- a/src/browser/client-bridge/error.ts +++ b/src/browser/client-bridge/error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class ClientBridgeError extends Error { constructor(message: string) { super(message); diff --git a/src/browser/commands/assert-view/errors/assert-view-error.ts b/src/browser/commands/assert-view/errors/assert-view-error.ts index 8c2b8e729..60cc4ff49 100644 --- a/src/browser/commands/assert-view/errors/assert-view-error.ts +++ b/src/browser/commands/assert-view/errors/assert-view-error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class AssertViewError extends Error { constructor(public message: string = "image comparison failed") { super(); diff --git a/src/browser/commands/assert-view/errors/image-diff-error.ts b/src/browser/commands/assert-view/errors/image-diff-error.ts index 7bd60607b..82635ba1d 100644 --- a/src/browser/commands/assert-view/errors/image-diff-error.ts +++ b/src/browser/commands/assert-view/errors/image-diff-error.ts @@ -37,6 +37,9 @@ interface ImageDiffErrorData { diffRatio: number; } +/** + * @category Errors + */ export class ImageDiffError extends BaseStateError { message: string; diffOpts: DiffOptions; diff --git a/src/browser/commands/assert-view/errors/no-ref-image-error.ts b/src/browser/commands/assert-view/errors/no-ref-image-error.ts index 4ee532e81..1da3f7da6 100644 --- a/src/browser/commands/assert-view/errors/no-ref-image-error.ts +++ b/src/browser/commands/assert-view/errors/no-ref-image-error.ts @@ -9,6 +9,9 @@ interface NoRefImageErrorData { refImg: RefImageInfo; } +/** + * @category Errors + */ export class NoRefImageError extends BaseStateError { static create( this: NoRefImageErrorConstructor, diff --git a/src/browser/commands/runStep.js b/src/browser/commands/runStep.ts similarity index 60% rename from src/browser/commands/runStep.js rename to src/browser/commands/runStep.ts index fcb69cd67..33f38794a 100644 --- a/src/browser/commands/runStep.js +++ b/src/browser/commands/runStep.ts @@ -1,10 +1,9 @@ -"use strict"; +import _ from "lodash"; +import { Browser } from "../browser"; -const _ = require("lodash"); - -module.exports.default = browser => { +const addRunStepCommand = (browser: Browser): void => { const { publicAPI: session } = browser; - session.addCommand("runStep", (stepName, stepCb) => { + session.addCommand("runStep", (stepName: string, stepCb: () => void) => { if (!_.isString(stepName)) { throw Error(`First argument must be a string, but got ${typeof stepName}`); } @@ -16,3 +15,5 @@ module.exports.default = browser => { return stepCb(); }); }; + +export default addRunStepCommand; diff --git a/src/browser/core-error.ts b/src/browser/core-error.ts index ad373ba5b..51fd223de 100644 --- a/src/browser/core-error.ts +++ b/src/browser/core-error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class CoreError extends Error { name = "CoreError"; diff --git a/src/browser/existing-browser.js b/src/browser/existing-browser.js index 135024b33..77ef5d857 100644 --- a/src/browser/existing-browser.js +++ b/src/browser/existing-browser.js @@ -6,7 +6,7 @@ const Promise = require("bluebird"); const _ = require("lodash"); const webdriverio = require("webdriverio"); const { sessionEnvironmentDetector } = require("@wdio/utils-cjs"); -const Browser = require("./browser"); +const { Browser } = require("./browser"); const commandsList = require("./commands"); const Camera = require("./camera"); const clientBridge = require("./client-bridge"); diff --git a/src/browser/history/callstack.js b/src/browser/history/callstack.js index 655a48a0b..87cd67376 100644 --- a/src/browser/history/callstack.js +++ b/src/browser/history/callstack.js @@ -1,7 +1,7 @@ "use strict"; const _ = require("lodash"); -const { historyDataMap } = require("./utils"); +const { TestStepKey } = require("../../types"); module.exports = class Callstack { constructor() { @@ -12,13 +12,13 @@ module.exports = class Callstack { enter(data) { this._stack.push({ ...data, - [historyDataMap.TIME_START]: Date.now(), - [historyDataMap.CHILDREN]: [], + [TestStepKey.TimeStart]: Date.now(), + [TestStepKey.Children]: [], }); } leave(key) { - const currentNodeIndex = _.findLastIndex(this._stack, node => node[historyDataMap.KEY] === key); + const currentNodeIndex = _.findLastIndex(this._stack, node => node[TestStepKey.Key] === key); const wasRemovedByParent = currentNodeIndex === -1; if (wasRemovedByParent) { @@ -30,11 +30,10 @@ module.exports = class Callstack { const parentNode = _.last(this._stack); const isCurrentNodeRoot = this._stack.length === 0; - currentNode[historyDataMap.TIME_END] = Date.now(); - currentNode[historyDataMap.DURATION] = - currentNode[historyDataMap.TIME_END] - currentNode[historyDataMap.TIME_START]; + currentNode[TestStepKey.TimeEnd] = Date.now(); + currentNode[TestStepKey.Duration] = currentNode[TestStepKey.TimeEnd] - currentNode[TestStepKey.TimeStart]; - isCurrentNodeRoot ? this._history.push(currentNode) : parentNode[historyDataMap.CHILDREN].push(currentNode); + isCurrentNodeRoot ? this._history.push(currentNode) : parentNode[TestStepKey.Children].push(currentNode); } markError(shouldPropagateFn) { @@ -43,10 +42,10 @@ module.exports = class Callstack { let shouldContinue = Boolean(currentNode); while (shouldContinue) { - currentNode[historyDataMap.IS_FAILED] = true; + currentNode[TestStepKey.IsFailed] = true; parentNode = currentNode; - currentNode = _.last(currentNode[historyDataMap.CHILDREN]); + currentNode = _.last(currentNode[TestStepKey.Children]); shouldContinue = currentNode && shouldPropagateFn(parentNode, currentNode); } } diff --git a/src/browser/history/index.js b/src/browser/history/index.js index f5277a06c..424ac9060 100644 --- a/src/browser/history/index.js +++ b/src/browser/history/index.js @@ -2,25 +2,26 @@ const Callstack = require("./callstack"); const cmds = require("./commands"); -const { runWithHooks, normalizeCommandArgs, historyDataMap, isGroup } = require("./utils"); +const { runWithHooks, normalizeCommandArgs, isGroup } = require("./utils"); +const { TestStepKey } = require("./../../types"); const shouldNotWrapCommand = commandName => ["addCommand", "overwriteCommand", "extendOptions", "setMeta", "getMeta", "runStep"].includes(commandName); const mkHistoryNode = ({ name, args, elementScope, key, overwrite, isGroup }) => { const map = { - [historyDataMap.NAME]: name, - [historyDataMap.ARGS]: normalizeCommandArgs(name, args), - [historyDataMap.SCOPE]: cmds.createScope(elementScope), - [historyDataMap.KEY]: key, + [TestStepKey.Name]: name, + [TestStepKey.Args]: normalizeCommandArgs(name, args), + [TestStepKey.Scope]: cmds.createScope(elementScope), + [TestStepKey.Key]: key, }; if (overwrite) { - map[historyDataMap.IS_OVERWRITTEN] = Number(overwrite); + map[TestStepKey.IsOverwritten] = Number(overwrite); } if (isGroup) { - map[historyDataMap.IS_GROUP] = true; + map[TestStepKey.IsGroup] = true; } return map; diff --git a/src/browser/history/utils.js b/src/browser/history/utils.js index a2b3e1462..83399a683 100644 --- a/src/browser/history/utils.js +++ b/src/browser/history/utils.js @@ -1,6 +1,7 @@ "use strict"; const _ = require("lodash"); +const { TestStepKey } = require("../../types"); const MAX_STRING_LENGTH = 50; @@ -22,23 +23,9 @@ exports.normalizeCommandArgs = (name, args = []) => { }); }; -exports.historyDataMap = { - NAME: "n", - ARGS: "a", - SCOPE: "s", - DURATION: "d", - TIME_START: "ts", - TIME_END: "te", - IS_OVERWRITTEN: "o", - IS_GROUP: "g", - IS_FAILED: "f", - CHILDREN: "c", - KEY: "k", -}; - const isPromise = val => typeof _.get(val, "then") === "function"; -exports.isGroup = node => Boolean(node && node[exports.historyDataMap.IS_GROUP]); +exports.isGroup = node => Boolean(node && node[TestStepKey.IsGroup]); exports.runWithHooks = ({ fn, before, after, error }) => { let isReturnedValuePromise = false; diff --git a/src/browser/new-browser.js b/src/browser/new-browser.js deleted file mode 100644 index b786c77cb..000000000 --- a/src/browser/new-browser.js +++ /dev/null @@ -1,177 +0,0 @@ -"use strict"; - -const { URLSearchParams } = require("url"); -const URI = require("urijs"); -const _ = require("lodash"); -const webdriverio = require("webdriverio"); -const Browser = require("./browser"); -const signalHandler = require("../signal-handler"); -const history = require("./history"); -const logger = require("../utils/logger"); -const RuntimeConfig = require("../config/runtime-config"); -const { DEVTOOLS_PROTOCOL } = require("../constants/config"); - -const DEFAULT_PORT = 4444; - -const headlessBrowserOptions = { - chrome: { - capabilityName: "goog:chromeOptions", - getArgs: headlessMode => { - const headlessValue = _.isBoolean(headlessMode) ? "headless" : `headless=${headlessMode}`; - - return [headlessValue, "disable-gpu"]; - }, - }, - firefox: { - capabilityName: "moz:firefoxOptions", - getArgs: () => ["-headless"], - }, - msedge: { - capabilityName: "ms:edgeOptions", - getArgs: () => ["--headless"], - }, - edge: { - capabilityName: "ms:edgeOptions", - getArgs: () => ["--headless"], - }, -}; - -module.exports = class NewBrowser extends Browser { - constructor(config, opts) { - super(config, opts); - - signalHandler.on("exit", () => this.quit()); - } - - async init() { - this._session = await this._createSession(); - - this._extendStacktrace(); - this._addSteps(); - this._addHistory(); - - await history.runGroup(this._callstackHistory, "testplane: init browser", async () => { - this._addCommands(); - this.restoreHttpTimeout(); - await this._setPageLoadTimeout(); - }); - - return this; - } - - reset() { - return Promise.resolve(); - } - - async quit() { - try { - this.setHttpTimeout(this._config.sessionQuitTimeout); - await this._session.deleteSession(); - } catch (e) { - logger.warn(`WARNING: Can not close session: ${e.message}`); - } - } - - _createSession() { - const sessionOpts = this._getSessionOpts(); - - return webdriverio.remote(sessionOpts); - } - - async _setPageLoadTimeout() { - if (!this._config.pageLoadTimeout) { - return; - } - - try { - await this._session.setTimeout({ pageLoad: this._config.pageLoadTimeout }); - } catch (e) { - // edge with w3c does not support setting page load timeout - if (this._session.isW3C && this._session.capabilities.browserName === "MicrosoftEdge") { - logger.warn(`WARNING: Can not set page load timeout: ${e.message}`); - } else { - throw e; - } - } - } - - _getSessionOpts() { - const config = this._config; - const gridUri = new URI(config.gridUrl); - const capabilities = this._extendCapabilities(config); - const { devtools } = RuntimeConfig.getInstance(); - - const options = { - protocol: gridUri.protocol(), - hostname: this._getGridHost(gridUri), - port: gridUri.port() ? parseInt(gridUri.port(), 10) : DEFAULT_PORT, - path: gridUri.path(), - queryParams: this._getQueryParams(gridUri.query()), - capabilities, - automationProtocol: devtools ? DEVTOOLS_PROTOCOL : config.automationProtocol, - connectionRetryTimeout: config.sessionRequestTimeout || config.httpTimeout, - connectionRetryCount: 0, // testplane has its own advanced retries - baseUrl: config.baseUrl, - waitforTimeout: config.waitTimeout, - waitforInterval: config.waitInterval, - ...this._getSessionOptsFromConfig(), - }; - - return options; - } - - _extendCapabilities(config) { - const capabilitiesExtendedByVersion = this.version - ? this._extendCapabilitiesByVersion() - : config.desiredCapabilities; - const capabilitiesWithAddedHeadless = this._addHeadlessCapability( - config.headless, - capabilitiesExtendedByVersion, - ); - return capabilitiesWithAddedHeadless; - } - - _addHeadlessCapability(headless, capabilities) { - if (!headless) { - return capabilities; - } - const capabilitySettings = headlessBrowserOptions[capabilities.browserName]; - if (!capabilitySettings) { - logger.warn(`WARNING: Headless setting is not supported for ${capabilities.browserName} browserName`); - return capabilities; - } - const browserCapabilities = capabilities[capabilitySettings.capabilityName] ?? {}; - capabilities[capabilitySettings.capabilityName] = { - ...browserCapabilities, - args: [...(browserCapabilities.args ?? []), ...capabilitySettings.getArgs(headless)], - }; - return capabilities; - } - - _extendCapabilitiesByVersion() { - const { desiredCapabilities, sessionEnvFlags } = this._config; - const versionKeyName = - desiredCapabilities.browserVersion || sessionEnvFlags.isW3C ? "browserVersion" : "version"; - - return _.assign({}, desiredCapabilities, { [versionKeyName]: this.version }); - } - - _getGridHost(url) { - return new URI({ - username: url.username(), - password: url.password(), - hostname: url.hostname(), - }) - .toString() - .slice(2); // URIjs leaves `//` prefix, removing it - } - - _getQueryParams(query) { - if (_.isEmpty(query)) { - return {}; - } - - const urlParams = new URLSearchParams(query); - return Object.fromEntries(urlParams); - } -}; diff --git a/src/browser/new-browser.ts b/src/browser/new-browser.ts new file mode 100644 index 000000000..7e231a6b3 --- /dev/null +++ b/src/browser/new-browser.ts @@ -0,0 +1,196 @@ +import { URLSearchParams } from "url"; + +import URI from "urijs"; +import { isBoolean, assign, isEmpty } from "lodash"; +import { remote, RemoteOptions } from "webdriverio"; + +import { Browser, BrowserOpts } from "./browser"; +import signalHandler from "../signal-handler"; +import { runGroup } from "./history"; +import { warn } from "../utils/logger"; +import { getInstance } from "../config/runtime-config"; +import { DEVTOOLS_PROTOCOL } from "../constants/config"; +import { Config } from "../config"; +import { BrowserConfig } from "../config/browser-config"; + +export type CapabilityName = "goog:chromeOptions" | "moz:firefoxOptions" | "ms:edgeOptions"; +export type HeadlessBrowserOptions = Record< + string, + { + capabilityName: CapabilityName; + getArgs: (headlessMode: BrowserConfig["headless"]) => string[]; + } +>; +const DEFAULT_PORT = 4444; + +const headlessBrowserOptions: HeadlessBrowserOptions = { + chrome: { + capabilityName: "goog:chromeOptions", + getArgs: (headlessMode: BrowserConfig["headless"]): string[] => { + const headlessValue = isBoolean(headlessMode) ? "headless" : `headless=${headlessMode}`; + + return [headlessValue, "disable-gpu"]; + }, + }, + firefox: { + capabilityName: "moz:firefoxOptions", + getArgs: (): string[] => ["-headless"], + }, + msedge: { + capabilityName: "ms:edgeOptions", + getArgs: (): string[] => ["--headless"], + }, + edge: { + capabilityName: "ms:edgeOptions", + getArgs: (): string[] => ["--headless"], + }, +}; + +export class NewBrowser extends Browser { + constructor(config: Config, opts: BrowserOpts) { + super(config, opts); + + signalHandler.on("exit", () => this.quit()); + } + + async init(): Promise { + this._session = await this._createSession(); + + this._extendStacktrace(); + this._addSteps(); + this._addHistory(); + + await runGroup(this._callstackHistory, "testplane: init browser", async () => { + this._addCommands(); + this.restoreHttpTimeout(); + await this._setPageLoadTimeout(); + }); + + return this; + } + + reset(): Promise { + return Promise.resolve(); + } + + async quit(): Promise { + try { + this.setHttpTimeout(this._config.sessionQuitTimeout); + await this._session!.deleteSession(); + } catch (e) { + warn(`WARNING: Can not close session: ${(e as Error).message}`); + } + } + + protected _createSession(): Promise { + const sessionOpts = this._getSessionOpts(); + + return remote(sessionOpts); + } + + protected async _setPageLoadTimeout(): Promise { + if (!this._config.pageLoadTimeout) { + return; + } + + try { + await this._session!.setTimeout({ pageLoad: this._config.pageLoadTimeout }); + } catch (e) { + // edge with w3c does not support setting page load timeout + if ( + this._session!.isW3C && + (this._session!.capabilities as { browserName: string }).browserName === "MicrosoftEdge" + ) { + warn(`WARNING: Can not set page load timeout: ${(e as Error).message}`); + } else { + throw e; + } + } + } + + protected _getSessionOpts(): RemoteOptions { + const config = this._config; + const gridUri = new URI(config.gridUrl); + const capabilities = this._extendCapabilities(config); + const { devtools } = getInstance(); + + const options = { + protocol: gridUri.protocol(), + hostname: this._getGridHost(gridUri), + port: gridUri.port() ? parseInt(gridUri.port(), 10) : DEFAULT_PORT, + path: gridUri.path(), + queryParams: this._getQueryParams(gridUri.query()), + capabilities, + automationProtocol: devtools ? DEVTOOLS_PROTOCOL : config.automationProtocol, + connectionRetryTimeout: config.sessionRequestTimeout || config.httpTimeout, + connectionRetryCount: 0, // testplane has its own advanced retries + baseUrl: config.baseUrl, + waitforTimeout: config.waitTimeout, + waitforInterval: config.waitInterval, + ...this._getSessionOptsFromConfig(), + }; + + return options as RemoteOptions; + } + + protected _extendCapabilities(config: BrowserConfig): WebdriverIO.Capabilities { + const capabilitiesExtendedByVersion = this.version + ? this._extendCapabilitiesByVersion() + : config.desiredCapabilities; + const capabilitiesWithAddedHeadless = this._addHeadlessCapability( + config.headless, + capabilitiesExtendedByVersion!, + ); + return capabilitiesWithAddedHeadless; + } + + protected _addHeadlessCapability( + headless: BrowserConfig["headless"], + capabilities: WebdriverIO.Capabilities, + ): WebdriverIO.Capabilities { + if (!headless) { + return capabilities; + } + const capabilitySettings = + headlessBrowserOptions[capabilities.browserName as keyof typeof headlessBrowserOptions]; + if (!capabilitySettings) { + warn(`WARNING: Headless setting is not supported for ${capabilities.browserName} browserName`); + return capabilities; + } + const browserCapabilities = (capabilities[capabilitySettings.capabilityName as CapabilityName] ?? + {}) as WebdriverIO.Capabilities[CapabilityName]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (capabilities as any)[capabilitySettings.capabilityName] = { + ...browserCapabilities, + args: [...(browserCapabilities!.args ?? []), ...capabilitySettings.getArgs(headless)], + }; + return capabilities; + } + + protected _extendCapabilitiesByVersion(): WebdriverIO.Capabilities { + const { desiredCapabilities, sessionEnvFlags } = this._config; + const versionKeyName = + desiredCapabilities!.browserVersion || sessionEnvFlags.isW3C ? "browserVersion" : "version"; + + return assign({}, desiredCapabilities, { [versionKeyName]: this.version }); + } + + protected _getGridHost(url: URI): string { + return new URI({ + username: url.username(), + password: url.password(), + hostname: url.hostname(), + }) + .toString() + .slice(2); // URIjs leaves `//` prefix, removing it + } + + protected _getQueryParams(query: string): Record { + if (isEmpty(query)) { + return {}; + } + + const urlParams = new URLSearchParams(query); + return Object.fromEntries(urlParams); + } +} diff --git a/src/browser/screen-shooter/viewport/coord-validator/errors/height-viewport-error.ts b/src/browser/screen-shooter/viewport/coord-validator/errors/height-viewport-error.ts index cdca55a67..950a451ee 100644 --- a/src/browser/screen-shooter/viewport/coord-validator/errors/height-viewport-error.ts +++ b/src/browser/screen-shooter/viewport/coord-validator/errors/height-viewport-error.ts @@ -1,5 +1,6 @@ /** * Height of the element is larger than viewport + * @category Errors */ export class HeightViewportError extends Error { constructor(message: string) { diff --git a/src/browser/screen-shooter/viewport/coord-validator/errors/offset-viewport-error.ts b/src/browser/screen-shooter/viewport/coord-validator/errors/offset-viewport-error.ts index 909e860a2..9d7eab5a1 100644 --- a/src/browser/screen-shooter/viewport/coord-validator/errors/offset-viewport-error.ts +++ b/src/browser/screen-shooter/viewport/coord-validator/errors/offset-viewport-error.ts @@ -1,5 +1,6 @@ /** * Position of an element is outside of a viewport left, top or right bounds + * @category Errors */ export class OffsetViewportError extends Error { constructor(message: string) { diff --git a/src/cli/index.js b/src/cli/index.ts similarity index 77% rename from src/cli/index.js rename to src/cli/index.ts index a3eb04518..de5fee94b 100644 --- a/src/cli/index.js +++ b/src/cli/index.ts @@ -1,18 +1,18 @@ -"use strict"; +import util from "util"; +import { Command } from "@gemini-testing/commander"; +import escapeRe from "escape-string-regexp"; -const util = require("util"); -const { Command } = require("@gemini-testing/commander"); -const escapeRe = require("escape-string-regexp"); +import defaults from "../config/defaults"; +import { configOverriding } from "./info"; +import { Testplane } from "../testplane"; +import pkg from "../../package.json"; +import logger from "../utils/logger"; +import { requireModule } from "../utils/module"; +import { shouldIgnoreUnhandledRejection } from "../utils/errors"; -const defaults = require("../config/defaults"); -const info = require("./info"); -const { Testplane } = require("../testplane"); -const pkg = require("../../package.json"); -const logger = require("../utils/logger"); -const { requireModule } = require("../utils/module"); -const { shouldIgnoreUnhandledRejection } = require("../utils/errors"); +export type TestplaneRunOpts = { cliName?: string }; -let testplane; +let testplane: Testplane; process.on("uncaughtException", err => { logger.error(util.inspect(err)); @@ -20,7 +20,7 @@ process.on("uncaughtException", err => { }); process.on("unhandledRejection", (reason, p) => { - if (shouldIgnoreUnhandledRejection(reason)) { + if (shouldIgnoreUnhandledRejection(reason as Error)) { logger.warn(`Unhandled Rejection "${reason}" in testplane:master:${process.pid} was ignored`); return; } @@ -32,23 +32,23 @@ process.on("unhandledRejection", (reason, p) => { ].join("\n"); if (testplane) { - testplane.halt(error); + testplane.halt(new Error(error)); } else { logger.error(error); process.exit(1); } }); -exports.run = (opts = {}) => { +export const run = (opts: TestplaneRunOpts = {}): void => { const program = new Command(opts.cliName || "testplane"); program.version(pkg.version).allowUnknownOption().option("-c, --config ", "path to configuration file"); - const configPath = preparseOption(program, "config"); + const configPath = preparseOption(program, "config") as string; testplane = Testplane.create(configPath); program - .on("--help", () => logger.log(info.configOverriding(opts))) + .on("--help", () => logger.log(configOverriding(opts))) .option("-b, --browser ", "run tests only in specified browser", collect) .option("-s, --set ", "run tests only in the specified set", collect) .option("-r, --require ", "require module", collect) @@ -70,7 +70,7 @@ exports.run = (opts = {}) => { .option("--repl-on-fail [type]", "open repl interface on test fail only", Boolean, false) .option("--devtools", "switches the browser to the devtools mode with using CDP protocol") .arguments("[paths...]") - .action(async paths => { + .action(async (paths: string[]) => { try { const { reporter: reporters, @@ -107,7 +107,7 @@ exports.run = (opts = {}) => { process.exit(isTestsSuccess ? 0 : 1); } catch (err) { - logger.error(err.stack || err); + logger.error((err as Error).stack || err); process.exit(1); } }); @@ -117,11 +117,11 @@ exports.run = (opts = {}) => { program.parse(process.argv); }; -function collect(newValue, array = []) { +function collect(newValue: string | string[], array: string[] = []): string[] { return array.concat(newValue); } -function preparseOption(program, option) { +function preparseOption(program: Command, option: string): unknown { // do not display any help, do not exit const configFileParser = Object.create(program); configFileParser.options = [].concat(program.options); @@ -131,7 +131,7 @@ function preparseOption(program, option) { return configFileParser[option]; } -function compileGrep(grep) { +function compileGrep(grep: string): RegExp { try { return new RegExp(`(${grep})|(${escapeRe(grep)})`); } catch (error) { @@ -140,7 +140,7 @@ function compileGrep(grep) { } } -async function handleRequires(requires = []) { +async function handleRequires(requires: string[] = []): Promise { for (const modulePath of requires) { await requireModule(modulePath); } diff --git a/src/cli/info.js b/src/cli/info.ts similarity index 86% rename from src/cli/info.js rename to src/cli/info.ts index f4e267a5f..bc0c26b81 100644 --- a/src/cli/info.js +++ b/src/cli/info.ts @@ -1,6 +1,6 @@ -"use strict"; +import { TestplaneRunOpts } from "."; -exports.configOverriding = (opts = {}) => { +export const configOverriding = (opts: TestplaneRunOpts = {}): string => { const cliName = opts.cliName || "testplane"; return ` Overriding config diff --git a/src/config/types.ts b/src/config/types.ts index c85e840d4..62d4769c2 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -2,6 +2,7 @@ import type { BrowserConfig } from "./browser-config"; import type { BrowserTestRunEnvOptions } from "../runner/browser-env/vite/types"; import type { Test } from "../types"; import type { ChildProcessWithoutNullStreams } from "child_process"; +import { RequestOptions } from "https"; export interface CompareOptsConfig { shouldCluster: boolean; @@ -95,6 +96,10 @@ export interface CommonConfig { configPath?: string; automationProtocol: "webdriver" | "devtools"; desiredCapabilities: WebdriverIO.Capabilities | null; + sessionEnvFlags: Record< + "isW3C" | "isChrome" | "isMobile" | "isIOS" | "isAndroid" | "isSauce" | "isSeleniumStandalone", + boolean + >; gridUrl: string; baseUrl: string; sessionsPerBrowser: number; @@ -108,6 +113,7 @@ export interface CommonConfig { sessionQuitTimeout: number | null; testTimeout: number | null; waitTimeout: number; + waitInterval: number; saveHistoryMode: "all" | "none" | "onlyFailed"; takeScreenshotOnFails: { testFail: boolean; @@ -135,6 +141,14 @@ export interface CommonConfig { resetCursor: boolean; headers: Record | null; + transformRequest: (req: RequestOptions) => RequestOptions; + transformResponse: (res: Response, req: RequestOptions) => Response; + + strictSSL: boolean | null; + user: string | null; + key: string | null; + region: string | null; + system: SystemConfig; headless: "old" | "new" | boolean | null; isolation: boolean; @@ -169,6 +183,12 @@ export interface SetsConfig { browsers?: Array; } +export interface SetsConfigParsed { + files: Array; + ignoreFiles: Array; + browsers: Array; +} + // Only browsers desiredCapabilities are required in input config export type ConfigInput = Partial & { browsers: Record & { desiredCapabilities: WebdriverIO.Capabilities }>; @@ -186,7 +206,7 @@ declare module "." { export interface Config extends CommonConfig { browsers: Record; plugins: Record>; - sets: Record; + sets: Record; prepareEnvironment?: () => void | null; } } diff --git a/src/errors/abort-on-reconnect-error.ts b/src/errors/abort-on-reconnect-error.ts index c0284dbbf..e8db53249 100644 --- a/src/errors/abort-on-reconnect-error.ts +++ b/src/errors/abort-on-reconnect-error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class AbortOnReconnectError extends Error { constructor() { super("Operation was aborted because client has been reconnected"); diff --git a/src/errors/testplane-internal-error.ts b/src/errors/testplane-internal-error.ts index 4b8d4d665..b234d0e72 100644 --- a/src/errors/testplane-internal-error.ts +++ b/src/errors/testplane-internal-error.ts @@ -1,3 +1,6 @@ +/** + * @category Errors + */ export class TestplaneInternalError extends Error { constructor(message: string) { super(message); diff --git a/src/events/index.ts b/src/events/index.ts index 80eb5c15a..9658a0c32 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,8 +1,8 @@ -import { ValueOf } from "type-fest"; - export * from "./async-emitter"; export * from "./types"; +type ValueOf = T[keyof T]; + export const TestReaderEvents = { NEW_BUILD_INSTRUCTION: "newBuildInstruction", } as const; diff --git a/src/index.ts b/src/index.ts index 0a06a6e8b..3b5b4127d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,10 +26,25 @@ export type { Config } from "./config"; export type { ConfigInput } from "./config/types"; export type { TestCollection } from "./test-collection"; +import type { TestDefinition, SuiteDefinition, TestHookDefinition } from "./test-reader/test-object/types"; + declare global { - const testplane: GlobalHelper; + /* eslint-disable no-var */ + // Here, we ignore clashes of types between Mocha and Testplane, because in production we don't include @types/mocha, + // but we need mocha types in development, so this is an issue only during development. + ///@ts-expect-error: see explanation above + var it: TestDefinition; + // @ts-expect-error: see explanation above + var describe: SuiteDefinition; + // @ts-expect-error: see explanation above + var beforeEach: TestHookDefinition; + // @ts-expect-error: see explanation above + var afterEach: TestHookDefinition; + + var testplane: GlobalHelper; /** * @deprecated Use `testplane` instead */ - const hermione: GlobalHelper; + var hermione: GlobalHelper; + /* eslint-enable no-var */ } diff --git a/src/runner/browser-env/vite/browser-modules/mocha/parser.ts b/src/runner/browser-env/vite/browser-modules/mocha/parser.ts index dcb6ddc13..523519287 100644 --- a/src/runner/browser-env/vite/browser-modules/mocha/parser.ts +++ b/src/runner/browser-env/vite/browser-modules/mocha/parser.ts @@ -1,5 +1,6 @@ import { ValueOf } from "type-fest"; import { MochaEvents } from "./events.js"; +import type {} from "mocha"; type RunnableHandler = (runnable: Mocha.Runnable) => void; diff --git a/src/runner/browser-env/vite/browser-modules/tsconfig.json b/src/runner/browser-env/vite/browser-modules/tsconfig.json index 3eda54197..533371880 100644 --- a/src/runner/browser-env/vite/browser-modules/tsconfig.json +++ b/src/runner/browser-env/vite/browser-modules/tsconfig.json @@ -9,6 +9,6 @@ "target": "es2022", "moduleResolution": "NodeNext", - "types": ["mocha", "vite/client", "webdriverio"] + "types": ["vite/client", "webdriverio"] } } diff --git a/src/runner/browser-env/vite/browser-modules/types.ts b/src/runner/browser-env/vite/browser-modules/types.ts index f32a2e9e3..b4a7740e3 100644 --- a/src/runner/browser-env/vite/browser-modules/types.ts +++ b/src/runner/browser-env/vite/browser-modules/types.ts @@ -101,7 +101,7 @@ export type MockMatcherFn = { declare global { interface Window { - Mocha: Mocha; + Mocha: unknown; __testplane__: { runUuid: string; errors: BrowserError[]; diff --git a/src/runner/runner.ts b/src/runner/runner.ts index 764e18c65..747e51cbd 100644 --- a/src/runner/runner.ts +++ b/src/runner/runner.ts @@ -1,6 +1,8 @@ -import { Constructor } from "type-fest"; import { AsyncEmitter } from "../events"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = new (...args: any[]) => T; + export abstract class Runner extends AsyncEmitter { static create(this: Constructor, ...args: unknown[]): T { return new this(...args); diff --git a/src/test-reader/build-instructions.js b/src/test-reader/build-instructions.ts similarity index 54% rename from src/test-reader/build-instructions.js rename to src/test-reader/build-instructions.ts index feafca55a..e76763da4 100644 --- a/src/test-reader/build-instructions.js +++ b/src/test-reader/build-instructions.ts @@ -1,18 +1,29 @@ -const _ = require("lodash"); -const validators = require("../validators"); -const env = require("../utils/env"); -const RuntimeConfig = require("../config/runtime-config"); +import _ from "lodash"; +import { validateUnknownBrowsers } from "../validators"; +import env from "../utils/env"; +import RuntimeConfig from "../config/runtime-config"; +import { TreeBuilder } from "./tree-builder"; +import { BrowserConfig } from "../config/browser-config"; +import { Config } from "../config"; + +export type InstructionFnArgs = { + treeBuilder: TreeBuilder; + browserId: string; + config: BrowserConfig & { passive?: boolean }; +}; + +export type InstructionFn = (args: InstructionFnArgs) => void; -class InstructionsList { - #commonInstructions; - #fileInstructions; +export class InstructionsList { + #commonInstructions: InstructionFn[]; + #fileInstructions: Map; constructor() { this.#commonInstructions = []; this.#fileInstructions = new Map(); } - push(fn, file) { + push(fn: InstructionFn, file?: string): InstructionsList { const instructions = file ? this.#fileInstructions.get(file) || [] : this.#commonInstructions; instructions.push(fn); @@ -24,7 +35,7 @@ class InstructionsList { return this; } - exec(files, ctx = {}) { + exec(files: string[], ctx: InstructionFnArgs): void { this.#commonInstructions.forEach(fn => fn(ctx)); files.forEach(file => { @@ -34,23 +45,25 @@ class InstructionsList { } } -function extendWithBrowserId({ treeBuilder, browserId }) { +function extendWithBrowserId({ treeBuilder, browserId }: InstructionFnArgs): void { treeBuilder.addTrap(testObject => { testObject.browserId = browserId; }); } -function extendWithBrowserVersion({ treeBuilder, config }) { +function extendWithBrowserVersion({ treeBuilder, config }: InstructionFnArgs): void { const { desiredCapabilities: { browserVersion, version }, - } = config; + } = config as unknown as { + desiredCapabilities: { browserVersion: string; version: string }; + }; treeBuilder.addTrap(testObject => { testObject.browserVersion = browserVersion || version; }); } -function extendWithTimeout({ treeBuilder, config }) { +function extendWithTimeout({ treeBuilder, config }: InstructionFnArgs): void { const { testTimeout } = config; const { replMode } = RuntimeConfig.getInstance(); @@ -63,7 +76,7 @@ function extendWithTimeout({ treeBuilder, config }) { }); } -function disableInPassiveBrowser({ treeBuilder, config }) { +function disableInPassiveBrowser({ treeBuilder, config }: InstructionFnArgs): void { const { passive } = config; if (!passive) { @@ -75,13 +88,13 @@ function disableInPassiveBrowser({ treeBuilder, config }) { }); } -function buildGlobalSkipInstruction(config) { +function buildGlobalSkipInstruction(config: Config): InstructionFn { const { value: skipBrowsers, key: envKey } = env.parseCommaSeparatedValue([ "TESTPLANE_SKIP_BROWSERS", "HERMIONE_SKIP_BROWSERS", ]); - validators.validateUnknownBrowsers(skipBrowsers, config.getBrowserIds()); + validateUnknownBrowsers(skipBrowsers, config.getBrowserIds()); return ({ treeBuilder, browserId }) => { if (!skipBrowsers.includes(browserId)) { @@ -94,13 +107,10 @@ function buildGlobalSkipInstruction(config) { }; } -module.exports = { - InstructionsList, - Instructions: { - extendWithBrowserId, - extendWithBrowserVersion, - extendWithTimeout, - disableInPassiveBrowser, - buildGlobalSkipInstruction, - }, +export const Instructions = { + extendWithBrowserId, + extendWithBrowserVersion, + extendWithTimeout, + disableInPassiveBrowser, + buildGlobalSkipInstruction, }; diff --git a/src/test-reader/index.js b/src/test-reader/index.ts similarity index 69% rename from src/test-reader/index.js rename to src/test-reader/index.ts index d4b188db6..1425248b2 100644 --- a/src/test-reader/index.js +++ b/src/test-reader/index.ts @@ -1,25 +1,34 @@ -const _ = require("lodash"); -const { EventEmitter } = require("events"); -const { passthroughEvent } = require("../events/utils"); -const SetsBuilder = require("./sets-builder"); -const { TestParser } = require("./test-parser"); -const { MasterEvents } = require("../events"); -const env = require("../utils/env"); - -module.exports = class TestReader extends EventEmitter { +import _ from "lodash"; +import { EventEmitter } from "events"; +import { passthroughEvent } from "../events/utils"; +import { SetsBuilder } from "./sets-builder"; +import { TestParser } from "./test-parser"; +import { MasterEvents } from "../events"; +import env from "../utils/env"; +import { Config } from "../config"; +import { Test } from "./test-object"; +import { ReadTestsOpts } from "../testplane"; + +export type TestReaderOpts = { paths: string[] } & Partial; + +export class TestReader extends EventEmitter { #config; - static create(...args) { + static create( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this: new (...args: any[]) => T, + ...args: ConstructorParameters + ): T { return new this(...args); } - constructor(config) { + constructor(config: Config) { super(); this.#config = config; } - async read(options = {}) { + async read(options: TestReaderOpts): Promise> { const { paths, browsers, ignore, sets, grep } = options; const { fileExtensions } = this.#config.system; @@ -27,7 +36,7 @@ module.exports = class TestReader extends EventEmitter { const setCollection = await SetsBuilder.create(this.#config.sets, { defaultPaths: ["testplane", "hermione"] }) .useFiles(paths) .useSets((sets || []).concat(envSets)) - .useBrowsers(browsers) + .useBrowsers(browsers!) .build(process.cwd(), { ignore }, fileExtensions); const testRunEnv = _.isArray(this.#config.system.testRunEnv) @@ -48,9 +57,9 @@ module.exports = class TestReader extends EventEmitter { return testsByBro; } -}; +} -function validateTests(testsByBro, options) { +function validateTests(testsByBro: Record, options: TestReaderOpts): void { const tests = _.flatten(Object.values(testsByBro)); if (options.replMode?.enabled) { @@ -78,12 +87,12 @@ function validateTests(testsByBro, options) { } } -function convertOptions(obj) { +function convertOptions(obj: Record): string { let result = ""; - for (let key of _.keys(obj)) { + for (const key of _.keys(obj)) { if (!_.isEmpty(obj[key]) || obj[key] instanceof RegExp) { if (_.isArray(obj[key])) { - result += `- ${key}: ${obj[key].join(", ")}\n`; + result += `- ${key}: ${(obj[key] as string[]).join(", ")}\n`; } else { result += `- ${key}: ${obj[key]}\n`; } diff --git a/src/test-reader/sets-builder/index.js b/src/test-reader/sets-builder/index.ts similarity index 53% rename from src/test-reader/sets-builder/index.js rename to src/test-reader/sets-builder/index.ts index 0171461c3..ca2c0b68f 100644 --- a/src/test-reader/sets-builder/index.js +++ b/src/test-reader/sets-builder/index.ts @@ -1,27 +1,31 @@ -const path = require("path"); -const globExtra = require("glob-extra"); -const _ = require("lodash"); -const Promise = require("bluebird"); +import path from "path"; +import * as globExtra from "glob-extra"; +import _ from "lodash"; -const SetCollection = require("./set-collection"); -const TestSet = require("./test-set"); +import { SetCollection } from "./set-collection"; +import { TestSet, TestSetData } from "./test-set"; +import { SetsConfigParsed } from "../../config/types"; + +export type SetsBuilderOpts = { + defaultPaths: string[]; +}; const FILE_EXTENSIONS = [".js", ".mjs"]; -module.exports = class SetsBuilder { - #sets; +export class SetsBuilder { + #sets: Record; #filesToUse; - static create(sets, opts) { + static create(sets: Record, opts: SetsBuilderOpts): SetsBuilder { return new SetsBuilder(sets, opts); } - constructor(sets, opts) { + constructor(sets: Record, opts: SetsBuilderOpts) { this.#sets = _.mapValues(sets, set => TestSet.create(set)); this.#filesToUse = this.#hasFiles() ? [] : opts.defaultPaths; } - useSets(setsToUse) { + useSets(setsToUse: string[]): SetsBuilder { this.#validateUnknownSets(setsToUse); if (!_.isEmpty(setsToUse)) { @@ -31,7 +35,7 @@ module.exports = class SetsBuilder { return this; } - #validateUnknownSets(setsToUse) { + #validateUnknownSets(setsToUse: string[]): void { const setsNames = _.keys(this.#sets); const unknownSets = _.difference(setsToUse, setsNames); @@ -48,7 +52,7 @@ module.exports = class SetsBuilder { throw new Error(error); } - useFiles(files) { + useFiles(files: string[]): SetsBuilder { if (!_.isEmpty(files)) { this.#filesToUse = files; } @@ -56,50 +60,56 @@ module.exports = class SetsBuilder { return this; } - useBrowsers(browsers) { + useBrowsers(browsers: string[]): SetsBuilder { _.forEach(this.#sets, set => set.useBrowsers(browsers)); return this; } - build(projectRoot, globOpts = {}, fileExtensions = FILE_EXTENSIONS) { + build( + projectRoot: string, + globOpts: { ignore?: string[] | string } = {}, + fileExtensions = FILE_EXTENSIONS, + ): Promise { const expandOpts = { formats: fileExtensions, root: projectRoot }; if (globOpts.ignore) { - globOpts.ignore = [].concat(globOpts.ignore).map(ignorePattern => path.resolve(projectRoot, ignorePattern)); + globOpts.ignore = ([] as string[]) + .concat(globOpts.ignore) + .map(ignorePattern => path.resolve(projectRoot, ignorePattern)); } return this.#transformDirsToMasks() .then(() => this.#resolvePaths(projectRoot)) - .then(() => globExtra.expandPaths(this.#filesToUse, expandOpts, globOpts)) + .then(() => globExtra.expandPaths(this.#filesToUse, expandOpts, globOpts as { ignore: string[] })) .then(expandedFiles => { this.#validateFoundFiles(expandedFiles); this.#useFiles(expandedFiles); }) - .then(() => this.#expandFiles(expandOpts, globOpts)) + .then(() => this.#expandFiles(expandOpts, globOpts as { ignore: string[] })) .then(() => SetCollection.create(this.#sets)); } - #transformDirsToMasks() { - return Promise.map(this.#getSets(), set => set.transformDirsToMasks()); + #transformDirsToMasks(): Promise { + return Promise.all(this.#getSets().map(set => set.transformDirsToMasks())); } - #getSets() { + #getSets(): TestSet[] { return _.values(this.#sets); } - #resolvePaths(projectRoot) { + #resolvePaths(projectRoot: string): void { _.forEach(this.#sets, set => set.resolveFiles(projectRoot)); } - #validateFoundFiles(foundFiles) { + #validateFoundFiles(foundFiles: string[]): void { if (!_.isEmpty(this.#filesToUse) && _.isEmpty(foundFiles)) { - const paths = [].concat(this.#filesToUse).join(", "); + const paths = ([] as string[]).concat(this.#filesToUse).join(", "); throw new Error(`Cannot find files by specified paths: ${paths}`); } } - #useFiles(filesToUse) { + #useFiles(filesToUse: string[]): void { _.forEach(this.#sets, set => set.useFiles(filesToUse)); if (!this.#hasFiles()) { @@ -107,11 +117,11 @@ module.exports = class SetsBuilder { } } - #expandFiles(expandOpts, globOpts) { - return Promise.map(this.#getSets(), set => set.expandFiles(expandOpts, globOpts)); + #expandFiles(expandOpts: globExtra.ExpandOpts, globOpts: globExtra.GlobOpts): Promise { + return Promise.all(this.#getSets().map(set => set.expandFiles(expandOpts, globOpts))); } - #hasFiles() { + #hasFiles(): boolean { return _.some(this.#sets, set => !_.isEmpty(set.getFiles())); } -}; +} diff --git a/src/test-reader/sets-builder/set-collection.js b/src/test-reader/sets-builder/set-collection.ts similarity index 54% rename from src/test-reader/sets-builder/set-collection.js rename to src/test-reader/sets-builder/set-collection.ts index 2a5b200d9..996bac0f4 100644 --- a/src/test-reader/sets-builder/set-collection.js +++ b/src/test-reader/sets-builder/set-collection.ts @@ -1,47 +1,48 @@ -const _ = require("lodash"); +import _ from "lodash"; +import { TestSet } from "./test-set"; -module.exports = class SetCollection { - #sets; +export class SetCollection { + #sets: Record; - static create(sets) { + static create(sets: Record): SetCollection { return new SetCollection(sets); } - constructor(sets) { + constructor(sets: Record) { this.#sets = sets; } - groupByFile() { + groupByFile(): Record { const files = this.getAllFiles(); const browsers = files.map(file => this.#getBrowsersForFile(file)); return _.zipObject(files, browsers); } - getAllFiles() { + getAllFiles(): string[] { return _.uniq(this.#getFromSets(set => set.getFiles())); } - #getBrowsersForFile(path) { + #getBrowsersForFile(path: string): string[] { return this.#getFromSets(set => set.getBrowsersForFile(path)); } - groupByBrowser() { + groupByBrowser(): Record { const browsers = this.#getBrowsers(); const files = browsers.map(browser => this.#getFilesForBrowser(browser)); return _.zipObject(browsers, files); } - #getBrowsers() { + #getBrowsers(): string[] { return this.#getFromSets(set => set.getBrowsers()); } - #getFilesForBrowser(browser) { + #getFilesForBrowser(browser: string): string[] { return this.#getFromSets(set => set.getFilesForBrowser(browser)); } - #getFromSets(cb) { - return _(this.#sets).map(cb).flatten().uniq().value(); + #getFromSets(cb: (data: TestSet) => T): T { + return _(this.#sets).map(cb).flatten().uniq().value() as T; } -}; +} diff --git a/src/test-reader/sets-builder/test-set.js b/src/test-reader/sets-builder/test-set.js deleted file mode 100644 index 57d26371a..000000000 --- a/src/test-reader/sets-builder/test-set.js +++ /dev/null @@ -1,74 +0,0 @@ -const globExtra = require("glob-extra"); -const _ = require("lodash"); -const mm = require("micromatch"); -const path = require("path"); -const Promise = require("bluebird"); - -const fs = Promise.promisifyAll(require("fs")); - -module.exports = class TestSet { - #set; - - static create(set) { - return new TestSet(set); - } - - constructor(set) { - this.#set = _.clone(set); - } - - expandFiles(expandOpts, globOpts = {}) { - const { files, ignoreFiles = [] } = this.#set; - globOpts = _.clone(globOpts); - globOpts.ignore = [].concat(globOpts.ignore || [], ignoreFiles).map(p => path.resolve(expandOpts.root, p)); - - return globExtra - .expandPaths(files, expandOpts, globOpts) - .then(expandedFiles => (this.#set = _.extend(this.#set, { files: expandedFiles }))); - } - - transformDirsToMasks() { - return Promise.map(this.#set.files, file => { - if (globExtra.isMask(file)) { - return file; - } - - return fs - .statAsync(file) - .then(stat => (stat.isDirectory() ? path.join(file, "**") : file)) - .catch(() => Promise.reject(new Error(`Cannot read such file or directory: '${file}'`))); - }).then(files => (this.#set.files = files)); - } - - resolveFiles(projectRoot) { - this.#set.files = this.#set.files.map(file => path.resolve(projectRoot, file)); - } - - getFiles() { - return this.#set.files; - } - - getBrowsers() { - return this.#set.browsers; - } - - getFilesForBrowser(browser) { - return _.includes(this.#set.browsers, browser) ? this.#set.files : []; - } - - getBrowsersForFile(file) { - return _.includes(this.#set.files, file) ? this.#set.browsers : []; - } - - useFiles(files) { - if (_.isEmpty(files)) { - return; - } - - this.#set.files = _.isEmpty(this.#set.files) ? files : mm(files, this.#set.files); - } - - useBrowsers(browsers) { - this.#set.browsers = _.isEmpty(browsers) ? this.#set.browsers : _.intersection(this.#set.browsers, browsers); - } -}; diff --git a/src/test-reader/sets-builder/test-set.ts b/src/test-reader/sets-builder/test-set.ts new file mode 100644 index 000000000..ef1d95c82 --- /dev/null +++ b/src/test-reader/sets-builder/test-set.ts @@ -0,0 +1,83 @@ +import * as globExtra from "glob-extra"; +import _ from "lodash"; +import mm from "micromatch"; +import path from "path"; +import fs from "fs/promises"; +import { SetsConfigParsed } from "../../config/types"; + +export type TestSetData = { + files: Array; + ignoreFiles?: Array; + browsers?: Array; +}; + +export class TestSet { + #set: TestSetData; + + static create(set: SetsConfigParsed): TestSet { + return new TestSet(set); + } + + constructor(set: SetsConfigParsed) { + this.#set = _.clone(set); + } + + async expandFiles(expandOpts: globExtra.ExpandOpts, globOpts: globExtra.GlobOpts = {}): Promise { + const { files, ignoreFiles = [] } = this.#set; + globOpts = _.clone(globOpts); + globOpts.ignore = ([] as string[]) + .concat(globOpts.ignore || [], ignoreFiles) + .map(p => path.resolve(expandOpts.root, p)); + + return globExtra + .expandPaths(files, expandOpts, globOpts) + .then(expandedFiles => (this.#set = _.extend(this.#set, { files: expandedFiles }))); + } + + async transformDirsToMasks(): Promise { + return Promise.all( + this.#set.files.map(file => { + if (globExtra.isMask(file)) { + return file; + } + + return fs + .stat(file) + .then(stat => (stat.isDirectory() ? path.join(file, "**") : file)) + .catch(() => Promise.reject(new Error(`Cannot read such file or directory: '${file}'`))); + }), + ).then(files => (this.#set.files = files)); + } + + resolveFiles(projectRoot: string): void { + this.#set.files = this.#set.files.map(file => path.resolve(projectRoot, file)); + } + + getFiles(): string[] { + return this.#set.files; + } + + getBrowsers(): string[] { + return this.#set.browsers!; + } + + getFilesForBrowser(browser: string): string[] { + return _.includes(this.#set.browsers, browser) ? this.#set.files : []; + } + + getBrowsersForFile(file: string): string[] { + return _.includes(this.#set.files, file) ? this.#set.browsers! : []; + } + + useFiles(files: string[]): void { + if (_.isEmpty(files)) { + return; + } + + this.#set.files = _.isEmpty(this.#set.files) ? files : mm(files, this.#set.files); + } + + useBrowsers(browsers: string[]): void { + this.#set.browsers = _.isEmpty(browsers) ? this.#set.browsers : _.intersection(this.#set.browsers, browsers); + } +} diff --git a/src/test-reader/test-parser-api.ts b/src/test-reader/test-parser-api.ts index e4eb3a8d3..bc43938cb 100644 --- a/src/test-reader/test-parser-api.ts +++ b/src/test-reader/test-parser-api.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "events"; import { GlobalHelper } from "../types"; import type { TreeBuilder } from "./tree-builder"; -type Context = GlobalHelper & Record>; +export type Context = GlobalHelper & Record>; type Methods = Record unknown>; interface NewBuildEventOpts { diff --git a/src/test-reader/test-parser.js b/src/test-reader/test-parser.ts similarity index 63% rename from src/test-reader/test-parser.js rename to src/test-reader/test-parser.ts index 857253dcf..a8a9b35f5 100644 --- a/src/test-reader/test-parser.js +++ b/src/test-reader/test-parser.ts @@ -1,35 +1,44 @@ -const { EventEmitter } = require("events"); -const { InstructionsList, Instructions } = require("./build-instructions"); -const { SkipController } = require("./controllers/skip-controller"); -const { OnlyController } = require("./controllers/only-controller"); -const { AlsoController } = require("./controllers/also-controller"); -const { ConfigController } = require("./controllers/config-controller"); -const browserVersionController = require("./controllers/browser-version-controller"); -const { TreeBuilder } = require("./tree-builder"); -const { readFiles } = require("./mocha-reader"); -const { TestReaderEvents } = require("../events"); -const { TestParserAPI } = require("./test-parser-api"); -const { setupTransformHook } = require("./test-transformer"); -const { MasterEvents } = require("../events"); -const _ = require("lodash"); -const clearRequire = require("clear-require"); -const path = require("path"); -const fs = require("fs-extra"); -const logger = require("../utils/logger"); -const { getShortMD5 } = require("../utils/crypto"); - -const getFailedTestId = test => getShortMD5(`${test.fullTitle}${test.browserId}${test.browserVersion}`); - -class TestParser extends EventEmitter { - #opts; - #failedTests; - #buildInstructions; - - /** - * @param {object} opts - * @param {"nodejs" | "browser" | undefined} opts.testRunEnv - environment to parse tests for - */ - constructor(opts = {}) { +import { EventEmitter } from "events"; +import { InstructionsList, Instructions } from "./build-instructions"; +import { SkipController } from "./controllers/skip-controller"; +import { OnlyController } from "./controllers/only-controller"; +import { AlsoController } from "./controllers/also-controller"; +import { ConfigController } from "./controllers/config-controller"; +import { mkProvider } from "./controllers/browser-version-controller"; +import { TreeBuilder } from "./tree-builder"; +import { readFiles } from "./mocha-reader"; +import { TestReaderEvents } from "../events"; +import { Context, TestParserAPI } from "./test-parser-api"; +import { setupTransformHook } from "./test-transformer"; +import { MasterEvents } from "../events"; +import _ from "lodash"; +import clearRequire from "clear-require"; +import path from "path"; +import fs from "fs-extra"; +import logger from "../utils/logger"; +import { getShortMD5 } from "../utils/crypto"; +import { Test } from "./test-object"; +import { Config } from "../config"; +import { BrowserConfig } from "../config/browser-config"; + +export type TestParserOpts = { + testRunEnv?: "nodejs" | "browser"; +}; +export type TestParserParseOpts = { + browserId: string; + grep?: RegExp; + config: BrowserConfig; +}; + +const getFailedTestId = (test: { fullTitle: string; browserId: string; browserVersion?: string }): string => + getShortMD5(`${test.fullTitle}${test.browserId}${test.browserVersion}`); + +export class TestParser extends EventEmitter { + #opts: TestParserOpts; + #failedTests: Set; + #buildInstructions: InstructionsList; + + constructor(opts: TestParserOpts = {}) { super(); this.#opts = opts; @@ -37,14 +46,14 @@ class TestParser extends EventEmitter { this.#buildInstructions = new InstructionsList(); } - async loadFiles(files, config) { + async loadFiles(files: string[], config: Config): Promise { const eventBus = new EventEmitter(); const { system: { ctx, mochaOpts }, } = config; const toolGlobals = { - browser: browserVersionController.mkProvider(config.getBrowserIds(), eventBus), + browser: mkProvider(config.getBrowserIds(), eventBus), config: ConfigController.create(eventBus), ctx: _.clone(ctx), only: OnlyController.create(eventBus), @@ -63,14 +72,14 @@ class TestParser extends EventEmitter { .push(Instructions.buildGlobalSkipInstruction(config)); this.#applyInstructionsEvents(eventBus); - this.#passthroughFileEvents(eventBus, toolGlobals); + this.#passthroughFileEvents(eventBus, toolGlobals as unknown as Context); this.#clearRequireCache(files); const revertTransformHook = setupTransformHook({ removeNonJsImports: this.#opts.testRunEnv === "browser" }); const rand = Math.random(); - const esmDecorator = f => f + `?rand=${rand}`; + const esmDecorator = (f: string): string => f + `?rand=${rand}`; await readFiles(files, { esmDecorator, config: mochaOpts, eventBus }); if (config.lastFailed.only) { @@ -94,8 +103,8 @@ class TestParser extends EventEmitter { revertTransformHook(); } - #applyInstructionsEvents(eventBus) { - let currentFile; + #applyInstructionsEvents(eventBus: EventEmitter): void { + let currentFile: string | undefined; eventBus .on(MasterEvents.BEFORE_FILE_READ, ({ file }) => (currentFile = file)) @@ -105,8 +114,8 @@ class TestParser extends EventEmitter { ); } - #passthroughFileEvents(eventBus, testplane) { - const passthroughEvent_ = (event, customOpts = {}) => { + #passthroughFileEvents(eventBus: EventEmitter, testplane: Context): void { + const passthroughEvent_ = (event: MasterEvents[keyof MasterEvents], customOpts = {}): void => { eventBus.on(event, data => this.emit(event, { ...data, @@ -121,7 +130,7 @@ class TestParser extends EventEmitter { passthroughEvent_(MasterEvents.AFTER_FILE_READ); } - #clearRequireCache(files) { + #clearRequireCache(files: string[]): void { files.forEach(filename => { if (path.extname(filename) !== ".mjs") { clearRequire(path.resolve(filename)); @@ -129,13 +138,13 @@ class TestParser extends EventEmitter { }); } - parse(files, { browserId, config, grep }) { + parse(files: string[], { browserId, config, grep }: TestParserParseOpts): Test[] { const treeBuilder = new TreeBuilder(); this.#buildInstructions.exec(files, { treeBuilder, browserId, config }); if (grep) { - treeBuilder.addTestFilter(test => grep.test(test.fullTitle())); + treeBuilder.addTestFilter((test: Test) => grep.test(test.fullTitle())); } if (config.lastFailed && config.lastFailed.only && this.#failedTests.size) { @@ -152,15 +161,15 @@ class TestParser extends EventEmitter { const rootSuite = treeBuilder.applyFilters().getRootSuite(); - const tests = rootSuite.getTests(); + const tests = rootSuite!.getTests(); this.#validateUniqTitles(tests); return tests; } - #validateUniqTitles(tests) { - const titles = {}; + #validateUniqTitles(tests: Test[]): void { + const titles: Record = {}; tests.forEach(test => { const fullTitle = test.fullTitle(); @@ -184,7 +193,3 @@ class TestParser extends EventEmitter { }); } } - -module.exports = { - TestParser, -}; diff --git a/src/test-reader/tree-builder.js b/src/test-reader/tree-builder.ts similarity index 59% rename from src/test-reader/tree-builder.js rename to src/test-reader/tree-builder.ts index fe9506ec3..2ca05b4eb 100644 --- a/src/test-reader/tree-builder.js +++ b/src/test-reader/tree-builder.ts @@ -1,7 +1,12 @@ -class TreeBuilder { - #traps; - #filters; - #rootSuite; +import { Hook, Suite, Test } from "./test-object"; + +export type TrapFn = (test: Test | Suite) => void; +export type FilterFn = (test: Test) => boolean; + +export class TreeBuilder { + #traps: TrapFn[]; + #filters: FilterFn[]; + #rootSuite: Suite | null; constructor() { this.#traps = []; @@ -10,7 +15,7 @@ class TreeBuilder { this.#rootSuite = null; } - addSuite(suite, parent = null) { + addSuite(suite: Suite, parent: Suite | null = null): TreeBuilder { if (!this.#rootSuite) { this.#rootSuite = suite; } @@ -24,43 +29,43 @@ class TreeBuilder { return this; } - addTest(test, parent) { + addTest(test: Test, parent: Suite): TreeBuilder { parent.addTest(test); this.#applyTraps(test); return this; } - addBeforeEachHook(hook, parent) { + addBeforeEachHook(hook: Hook, parent: Suite): TreeBuilder { parent.addBeforeEachHook(hook); return this; } - addAfterEachHook(hook, parent) { + addAfterEachHook(hook: Hook, parent: Suite): TreeBuilder { parent.addAfterEachHook(hook); return this; } - addTrap(fn) { + addTrap(fn: TrapFn): TreeBuilder { this.#traps.push(fn); return this; } - #applyTraps(obj) { + #applyTraps(obj: Test | Suite): void { this.#traps.forEach(trap => trap(obj)); this.#traps = []; } - addTestFilter(fn) { + addTestFilter(fn: FilterFn): TreeBuilder { this.#filters.push(fn); return this; } - applyFilters() { + applyFilters(): TreeBuilder { if (this.#rootSuite && this.#filters.length !== 0) { this.#rootSuite.filterTests(test => { return this.#filters.every(f => f(test)); @@ -70,11 +75,7 @@ class TreeBuilder { return this; } - getRootSuite() { + getRootSuite(): Suite | null { return this.#rootSuite; } } - -module.exports = { - TreeBuilder, -}; diff --git a/src/testplane.ts b/src/testplane.ts index 7fc00cf5a..8f8a3c12b 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -1,4 +1,4 @@ -import { CommanderStatic } from "@gemini-testing/commander"; +import { Command } from "@gemini-testing/commander"; import _ from "lodash"; import fs from "fs-extra"; import { Stats as RunnerStats } from "./stats"; @@ -9,7 +9,7 @@ import RuntimeConfig from "./config/runtime-config"; import { MasterAsyncEvents, MasterEvents, MasterSyncEvents } from "./events"; import eventsUtils from "./events/utils"; import signalHandler from "./signal-handler"; -import TestReader from "./test-reader"; +import { TestReader } from "./test-reader"; import { TestCollection } from "./test-collection"; import { validateUnknownBrowsers } from "./validators"; import { initReporters } from "./reporters"; @@ -44,7 +44,7 @@ export type FailedListItem = { fullTitle: string; }; -interface ReadTestsOpts extends Pick { +export interface ReadTestsOpts extends Pick { silent: boolean; ignore: string | string[]; failed: FailedListItem[]; @@ -69,7 +69,7 @@ export class Testplane extends BaseTestplane { this.runner = null; } - extendCli(parser: CommanderStatic): void { + extendCli(parser: Command): void { this.emit(MasterEvents.CLI, parser); } @@ -97,7 +97,7 @@ export class Testplane extends BaseTestplane { reporters = [], }: Partial = {}, ): Promise { - validateUnknownBrowsers(browsers, _.keys(this._config.browsers)); + validateUnknownBrowsers(browsers!, _.keys(this._config.browsers)); RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode, replMode, devtools }); diff --git a/src/types/index.ts b/src/types/index.ts index 4bccc41b6..5e562d26a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -104,21 +104,32 @@ export interface AssertViewResultNoRefImage { export type AssertViewResult = AssertViewResultSuccess | AssertViewResultDiff | AssertViewResultNoRefImage; -export interface CommandHistory { - /** Name: command name */ - n: string; - /** Arguments: array of passed arguments */ - a: unknown[]; - /** Time start */ - ts: number; - /** Time end */ - te: number; - /** Duration */ - d: number; - /** Scope: scope of execution (browser or element) */ - s: "b" | "e"; - /** Children: array of children commands */ - c: CommandHistory[]; +export enum TestStepKey { + Name = "n", + Args = "a", + Scope = "s", + Duration = "d", + TimeStart = "ts", + TimeEnd = "te", + IsOverwritten = "o", + IsGroup = "g", + IsFailed = "f", + Children = "c", + Key = "k", +} + +export interface TestStep { + [TestStepKey.Name]: string; + [TestStepKey.Args]: string[]; + [TestStepKey.TimeStart]: number; + [TestStepKey.TimeEnd]: number; + [TestStepKey.Duration]: number; + [TestStepKey.Scope]: "b" | "e"; + [TestStepKey.Children]: TestStep[]; + [TestStepKey.Key]: symbol; + [TestStepKey.IsOverwritten]: boolean; + [TestStepKey.IsGroup]: boolean; + [TestStepKey.IsFailed]: boolean; } export interface ExecutionThreadToolCtx { @@ -141,7 +152,8 @@ export interface TestResult extends Test { * @deprecated Use `testplaneCtx` instead */ hermioneCtx: ExecutionThreadToolCtx; - history: CommandHistory; + /** @note history is not available for skipped tests */ + history?: TestStep[]; meta: { [name: string]: unknown }; sessionId: string; startTime: number; diff --git a/src/validators.js b/src/validators.ts similarity index 61% rename from src/validators.js rename to src/validators.ts index ad8e0343b..93b7d5c02 100644 --- a/src/validators.js +++ b/src/validators.ts @@ -1,11 +1,9 @@ -"use strict"; +import { format } from "util"; +import chalk from "chalk"; +import _ from "lodash"; +import logger from "./utils/logger"; -const format = require("util").format; -const chalk = require("chalk"); -const _ = require("lodash"); -const logger = require("./utils/logger"); - -exports.validateUnknownBrowsers = (browsers, configBrowsers) => { +export const validateUnknownBrowsers = (browsers: string[], configBrowsers: string[]): void => { const unknownBrowsers = getUnknownBrowsers(browsers, configBrowsers); if (_.isEmpty(unknownBrowsers)) { @@ -22,6 +20,6 @@ exports.validateUnknownBrowsers = (browsers, configBrowsers) => { ); }; -function getUnknownBrowsers(browsers, configBrowsers) { +function getUnknownBrowsers(browsers: string[], configBrowsers: string[]): string[] { return _(browsers).compact().uniq().difference(configBrowsers).value(); } diff --git a/src/worker/runner/browser-agent.js b/src/worker/runner/browser-agent.js deleted file mode 100644 index 3b51e76df..000000000 --- a/src/worker/runner/browser-agent.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; - -module.exports = class BrowserAgent { - static create(opts) { - return new this(opts); - } - - constructor({ id, version, pool }) { - this.browserId = id; - this.browserVersion = version; - - this._pool = pool; - } - - getBrowser({ sessionId, sessionCaps, sessionOpts, state }) { - return this._pool.getBrowser({ - browserId: this.browserId, - browserVersion: this.browserVersion, - sessionId, - sessionCaps, - sessionOpts, - state, - }); - } - - freeBrowser(browser) { - this._pool.freeBrowser(browser); - } -}; diff --git a/src/worker/runner/browser-agent.ts b/src/worker/runner/browser-agent.ts new file mode 100644 index 000000000..d0b1cc750 --- /dev/null +++ b/src/worker/runner/browser-agent.ts @@ -0,0 +1,53 @@ +import ExistingBrowser from "../../browser/existing-browser"; +import { WdioBrowser } from "../../types"; +import BrowserPool from "./browser-pool"; + +export type BrowserAgentBrowserOpts = { + sessionId: string; + sessionCaps: WdioBrowser["capabilities"]; + sessionOpts: WdioBrowser["options"]; + state: Record; +}; + +export type CreateBrowserAgentOpts = { + id: string; + version: string; + pool: BrowserPool; +}; + +export class BrowserAgent { + browserId: string; + browserVersion: string; + private _pool: BrowserPool; + + static create(opts: CreateBrowserAgentOpts): BrowserAgent { + return new this(opts); + } + + constructor({ id, version, pool }: CreateBrowserAgentOpts) { + this.browserId = id; + this.browserVersion = version; + + this._pool = pool; + } + + async getBrowser({ + sessionId, + sessionCaps, + sessionOpts, + state, + }: BrowserAgentBrowserOpts): Promise { + return this._pool.getBrowser({ + browserId: this.browserId, + browserVersion: this.browserVersion, + sessionId, + sessionCaps, + sessionOpts, + state, + }); + } + + freeBrowser(browser: ExistingBrowser): void { + this._pool.freeBrowser(browser); + } +} diff --git a/src/worker/runner/caching-test-parser.js b/src/worker/runner/caching-test-parser.js deleted file mode 100644 index a8a631bfc..000000000 --- a/src/worker/runner/caching-test-parser.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; - -const { EventEmitter } = require("events"); -const { passthroughEvent } = require("../../events/utils"); -const SequenceTestParser = require("./sequence-test-parser"); -const { TestCollection } = require("../../test-collection"); -const { WorkerEvents } = require("../../events"); - -module.exports = class CachingTestParser extends EventEmitter { - static create(...args) { - return new this(...args); - } - - constructor(config) { - super(); - - this._config = config; - this._cache = {}; - - this._sequenceTestParser = SequenceTestParser.create(config); - passthroughEvent(this._sequenceTestParser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); - } - - async parse({ file, browserId }) { - const cached = this._getFromCache({ file, browserId }); - if (cached) { - return cached; - } - - const testsPromise = this._sequenceTestParser.parse({ file, browserId }); - this._putToCache(testsPromise, { file, browserId }); - - const tests = await testsPromise; - - this.emit(WorkerEvents.AFTER_TESTS_READ, TestCollection.create({ [browserId]: tests }, this._config)); - - return tests; - } - - _getFromCache({ file, browserId }) { - return this._cache[browserId] && this._cache[browserId][file]; - } - - _putToCache(testsPromise, { file, browserId }) { - this._cache[browserId] = this._cache[browserId] || {}; - this._cache[browserId][file] = testsPromise; - } -}; diff --git a/src/worker/runner/caching-test-parser.ts b/src/worker/runner/caching-test-parser.ts new file mode 100644 index 000000000..547731d34 --- /dev/null +++ b/src/worker/runner/caching-test-parser.ts @@ -0,0 +1,64 @@ +import { EventEmitter } from "events"; +import { passthroughEvent } from "../../events/utils"; +import { SequenceTestParser } from "./sequence-test-parser"; +import { TestCollection } from "../../test-collection"; +import { WorkerEvents } from "../../events"; +import { Config } from "../../config"; +import { Test } from "../../types"; + +export type CacheKey = { + file: string; + browserId: string; +}; + +export type ParseArgs = { + file: string; + browserId: string; +}; + +export class CachingTestParser extends EventEmitter { + private _cache: Record>>; + private _sequenceTestParser: SequenceTestParser; + + static create( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this: new (...args: any[]) => T, + ...args: ConstructorParameters + ): T { + return new this(...args); + } + + constructor(config: Config) { + super(); + + this._cache = {}; + + this._sequenceTestParser = SequenceTestParser.create(config); + passthroughEvent(this._sequenceTestParser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); + } + + async parse({ file, browserId }: ParseArgs): Promise { + const cached = this._getFromCache({ file, browserId }); + if (cached) { + return cached; + } + + const testsPromise = this._sequenceTestParser.parse({ file, browserId }); + this._putToCache(testsPromise, { file, browserId }); + + const tests = await testsPromise; + + this.emit(WorkerEvents.AFTER_TESTS_READ, TestCollection.create({ [browserId]: tests })); + + return tests; + } + + private _getFromCache({ file, browserId }: CacheKey): Promise { + return this._cache[browserId] && this._cache[browserId][file]; + } + + private _putToCache(testsPromise: Promise, { file, browserId }: CacheKey): void { + this._cache[browserId] = this._cache[browserId] || {}; + this._cache[browserId][file] = testsPromise; + } +} diff --git a/src/worker/runner/index.js b/src/worker/runner/index.js index 364d5a53a..c96beab00 100644 --- a/src/worker/runner/index.js +++ b/src/worker/runner/index.js @@ -4,10 +4,10 @@ const { AsyncEmitter } = require("../../events/async-emitter"); const { passthroughEvent } = require("../../events/utils"); const { WorkerEvents } = require("../../events"); const BrowserPool = require("./browser-pool"); -const BrowserAgent = require("./browser-agent"); +const { BrowserAgent } = require("./browser-agent"); const NodejsEnvTestRunner = require("./test-runner"); const { TestRunner: BrowserEnvTestRunner } = require("../browser-env/runner/test-runner"); -const CachingTestParser = require("./caching-test-parser"); +const { CachingTestParser } = require("./caching-test-parser"); const { isRunInNodeJsEnv } = require("../../utils/config"); module.exports = class Runner extends AsyncEmitter { diff --git a/src/worker/runner/sequence-test-parser.js b/src/worker/runner/sequence-test-parser.js deleted file mode 100644 index 462fe13e4..000000000 --- a/src/worker/runner/sequence-test-parser.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; - -const { EventEmitter } = require("events"); -const { passthroughEvent } = require("../../events/utils"); -const SimpleTestParser = require("./simple-test-parser"); -const { WorkerEvents } = require("../../events"); -const fastq = require("fastq"); - -module.exports = class SequenceTestParser extends EventEmitter { - static create(...args) { - return new this(...args); - } - - constructor(config) { - super(); - - this._parser = SimpleTestParser.create(config); - passthroughEvent(this._parser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); - - this._queue = fastq.promise(fn => fn(), 1); - } - - async parse({ file, browserId }) { - return this._queue.push(() => this._parser.parse({ file, browserId })); - } -}; diff --git a/src/worker/runner/sequence-test-parser.ts b/src/worker/runner/sequence-test-parser.ts new file mode 100644 index 000000000..0ad4d6eae --- /dev/null +++ b/src/worker/runner/sequence-test-parser.ts @@ -0,0 +1,38 @@ +import { EventEmitter } from "events"; +import { passthroughEvent } from "../../events/utils"; +import { SimpleTestParser } from "./simple-test-parser"; +import { WorkerEvents } from "../../events"; +import fastq from "fastq"; +import { Config } from "../../config"; +import { Test } from "../../test-reader/test-object"; + +export type ParseArgs = { + file: string; + browserId: string; +}; + +export class SequenceTestParser extends EventEmitter { + private _parser: SimpleTestParser; + private _queue: fastq.queueAsPromised<() => Promise, Test[]>; + + static create( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this: new (...args: any[]) => T, + ...args: ConstructorParameters + ): T { + return new this(...args); + } + + constructor(config: Config) { + super(); + + this._parser = SimpleTestParser.create(config); + passthroughEvent(this._parser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); + + this._queue = fastq.promise(fn => fn(), 1); + } + + async parse({ file, browserId }: ParseArgs): Promise { + return this._queue.push(() => this._parser.parse({ file, browserId })); + } +} diff --git a/src/worker/runner/simple-test-parser.js b/src/worker/runner/simple-test-parser.js deleted file mode 100644 index 7789133a0..000000000 --- a/src/worker/runner/simple-test-parser.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const { EventEmitter } = require("events"); -const { passthroughEvent } = require("../../events/utils"); -const { TestParser } = require("../../test-reader/test-parser"); -const { WorkerEvents } = require("../../events"); - -module.exports = class SimpleTestParser extends EventEmitter { - static create(...args) { - return new this(...args); - } - - constructor(config) { - super(); - - this._config = config; - } - - async parse({ file, browserId }) { - const testRunEnv = _.isArray(this._config.system.testRunEnv) - ? this._config.system.testRunEnv[0] - : this._config.system.testRunEnv; - - const parser = new TestParser({ testRunEnv }); - - passthroughEvent(parser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); - - await parser.loadFiles([file], this._config); - - return parser.parse([file], { browserId, config: this._config.forBrowser(browserId) }); - } -}; diff --git a/src/worker/runner/simple-test-parser.ts b/src/worker/runner/simple-test-parser.ts new file mode 100644 index 000000000..70b8fd2fe --- /dev/null +++ b/src/worker/runner/simple-test-parser.ts @@ -0,0 +1,45 @@ +import { Config } from "../../config"; + +import _ from "lodash"; +import { EventEmitter } from "events"; +import { passthroughEvent } from "../../events/utils"; +import { TestParser } from "../../test-reader/test-parser"; +import { WorkerEvents } from "../../events"; +import { Test } from "../../types"; + +export type ParseArgs = { + file: string; + browserId: string; +}; + +export class SimpleTestParser extends EventEmitter { + private _config: Config; + + static create( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this: new (...args: any[]) => T, + ...args: ConstructorParameters + ): T { + return new this(...args); + } + + constructor(config: Config) { + super(); + + this._config = config; + } + + async parse({ file, browserId }: ParseArgs): Promise { + const testRunEnv = _.isArray(this._config.system.testRunEnv) + ? this._config.system.testRunEnv[0] + : this._config.system.testRunEnv; + + const parser = new TestParser({ testRunEnv }); + + passthroughEvent(parser, this, [WorkerEvents.BEFORE_FILE_READ, WorkerEvents.AFTER_FILE_READ]); + + await parser.loadFiles([file], this._config); + + return parser.parse([file], { browserId, config: this._config.forBrowser(browserId) }); + } +} diff --git a/src/worker/runner/test-runner/types.ts b/src/worker/runner/test-runner/types.ts index 8466ad7a0..5f866b3ba 100644 --- a/src/worker/runner/test-runner/types.ts +++ b/src/worker/runner/test-runner/types.ts @@ -1,7 +1,7 @@ import type { WorkerRunTestOpts, WorkerRunTestTestplaneCtx } from "../../testplane"; import type { Test } from "../../../test-reader/test-object/test"; import type { BrowserConfig } from "../../../config/browser-config"; -import type BrowserAgent from "../browser-agent"; +import type { BrowserAgent } from "../browser-agent"; import type { Browser } from "../../../browser/types"; import type OneTimeScreenshooter from "./one-time-screenshooter"; diff --git a/src/worker/testplane-facade.js b/src/worker/testplane-facade.js deleted file mode 100644 index 021eb3057..000000000 --- a/src/worker/testplane-facade.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; - -const { Testplane } = require("./testplane"); -const RuntimeConfig = require("../config/runtime-config"); -const Promise = require("bluebird"); -const debug = require("debug")(`testplane:worker:${process.pid}`); -const ipc = require("../utils/ipc"); -const { MASTER_INIT, MASTER_SYNC_CONFIG, WORKER_INIT, WORKER_SYNC_CONFIG } = require("../constants/process-messages"); -const { requireModule } = require("../utils/module"); - -module.exports = class TestplaneFacade { - static create() { - return new this(); - } - - constructor() { - this.promise = Promise.resolve(); - this._testplane = null; - } - - init() { - this.init = () => this.promise; - - this.promise = this._init() - .then(testplane => (this._testplane = testplane)) - .then(() => this._testplane.init()); - - return this.promise; - } - - syncConfig() { - this.syncConfig = () => this.promise; - - this.promise = this.init().then(() => this._syncConfig()); - - return this.promise; - } - - runTest(...args) { - return this.syncConfig().then(() => this._testplane.runTest(...args)); - } - - _init() { - return new Promise((resolve, reject) => { - debug("init worker"); - - ipc.on(MASTER_INIT, ({ configPath, runtimeConfig } = {}) => { - try { - const promise = Promise.resolve(); - - if (runtimeConfig.requireModules) { - runtimeConfig.requireModules.forEach(modulePath => { - promise.then(requireModule(modulePath)); - }); - } - - RuntimeConfig.getInstance().extend(runtimeConfig); - const testplane = Testplane.create(configPath); - - promise.then(() => { - debug("worker initialized"); - resolve(testplane); - }); - } catch (e) { - debug("worker initialization failed"); - reject(e); - } - }); - - ipc.emit(WORKER_INIT); - }); - } - - _syncConfig() { - return new Promise(resolve => { - debug("sync config"); - - ipc.on(MASTER_SYNC_CONFIG, ({ config } = {}) => { - delete config.system.mochaOpts.grep; // grep affects only master - this._testplane.config.mergeWith(config); - - debug("config synced"); - resolve(); - }); - - ipc.emit(WORKER_SYNC_CONFIG); - }); - } -}; diff --git a/src/worker/testplane-facade.ts b/src/worker/testplane-facade.ts new file mode 100644 index 000000000..535bcf11c --- /dev/null +++ b/src/worker/testplane-facade.ts @@ -0,0 +1,101 @@ +import { Testplane, WorkerRunTestOpts, WorkerRunTestResult } from "./testplane"; +import RuntimeConfig from "../config/runtime-config"; +import debug from "debug"; +import ipc from "../utils/ipc"; +import { MASTER_INIT, MASTER_SYNC_CONFIG, WORKER_INIT, WORKER_SYNC_CONFIG } from "../constants/process-messages"; +import { requireModule } from "../utils/module"; +import { Config } from "../config"; + +debug(`testplane:worker:${process.pid}`); + +module.exports = class TestplaneFacade { + promise: Promise; + _testplane: Testplane | null; + + static create(): TestplaneFacade { + return new this(); + } + + constructor() { + this.promise = Promise.resolve(); + this._testplane = null; + } + + init(): Promise { + this.init = (): Promise => this.promise; + + this.promise = this._init() + .then(testplane => (this._testplane = testplane)) + .then(() => this._testplane!.init()); + + return this.promise; + } + + syncConfig(): Promise { + this.syncConfig = (): Promise => this.promise; + + this.promise = this.init().then(() => this._syncConfig()); + + return this.promise; + } + + runTest(fullTitle: string, options: WorkerRunTestOpts): Promise { + return this.syncConfig().then(() => this._testplane!.runTest(fullTitle, options)); + } + + private _init(): Promise { + return new Promise((resolve, reject) => { + debug("init worker"); + + ipc.on( + MASTER_INIT, + ({ + configPath, + runtimeConfig, + }: { + configPath: string; + runtimeConfig: { requireModules?: string[] }; + }) => { + try { + const promise = Promise.resolve(); + + if (runtimeConfig.requireModules) { + runtimeConfig.requireModules.forEach(modulePath => { + promise.then(() => requireModule(modulePath as string)); + }); + } + + RuntimeConfig.getInstance().extend(runtimeConfig); + const testplane = Testplane.create(configPath); + + promise.then(() => { + debug("worker initialized"); + resolve(testplane); + }); + } catch (e) { + debug("worker initialization failed"); + reject(e); + } + }, + ); + + ipc.emit(WORKER_INIT); + }); + } + + _syncConfig(): Promise { + return new Promise(resolve => { + debug("sync config"); + + ipc.on(MASTER_SYNC_CONFIG, ({ config }: { config: Config }) => { + delete config.system.mochaOpts.grep; // grep affects only master + this._testplane!.config.mergeWith(config); + + debug("config synced"); + resolve(); + }); + + ipc.emit(WORKER_SYNC_CONFIG); + }); + } +}; diff --git a/src/worker/testplane.ts b/src/worker/testplane.ts index 676f1827c..6abd1a34e 100644 --- a/src/worker/testplane.ts +++ b/src/worker/testplane.ts @@ -3,6 +3,7 @@ import { WorkerEvents } from "../events"; import Runner from "./runner"; import { BaseTestplane } from "../base-testplane"; import { RefImageInfo, WdioBrowser, WorkerEventHandler } from "../types"; +import { ConfigInput } from "../config/types"; export interface WorkerRunTestOpts { browserId: string; @@ -40,8 +41,8 @@ export interface Testplane { export class Testplane extends BaseTestplane { protected runner: Runner; - constructor(configPath: string) { - super(configPath); + constructor(config?: string | ConfigInput) { + super(config); this.runner = Runner.create(this._config); diff --git a/test/src/browser-pool/basic-pool.js b/test/src/browser-pool/basic-pool.js index 73b72f8ed..ec2de54e3 100644 --- a/test/src/browser-pool/basic-pool.js +++ b/test/src/browser-pool/basic-pool.js @@ -1,8 +1,8 @@ "use strict"; const { AsyncEmitter } = require("src/events/async-emitter"); -const BasicPool = require("src/browser-pool/basic-pool"); -const Browser = require("src/browser/new-browser"); +const { BasicPool } = require("src/browser-pool/basic-pool"); +const { NewBrowser } = require("src/browser/new-browser"); const { CancelledError } = require("src/browser-pool/cancelled-error"); const { MasterEvents: Events } = require("src/events"); const { stubBrowser } = require("./util"); @@ -23,7 +23,7 @@ describe("browser-pool/basic-pool", () => { }; beforeEach(() => { - sandbox.stub(Browser, "create").returns(stubBrowser()); + sandbox.stub(NewBrowser, "create").returns(stubBrowser()); }); afterEach(() => sandbox.restore()); @@ -33,18 +33,18 @@ describe("browser-pool/basic-pool", () => { await mkPool_({ config }).getBrowser("broId"); - assert.calledWith(Browser.create, config, { id: "broId" }); + assert.calledWith(NewBrowser.create, config, { id: "broId" }); }); it("should create new browser with specified version when requested", async () => { await mkPool_().getBrowser("broId", { version: "1.0" }); - assert.calledWith(Browser.create, sinon.match.any, { id: "broId", version: "1.0" }); + assert.calledWith(NewBrowser.create, sinon.match.any, { id: "broId", version: "1.0" }); }); it("should init browser", async () => { const browser = stubBrowser(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); await mkPool_().getBrowser(); @@ -55,7 +55,7 @@ describe("browser-pool/basic-pool", () => { const publicAPI = null; const browser = stubBrowser("some-id", "some-version", publicAPI); browser.init.rejects(new Error("foo")); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const pool = mkPool_(); @@ -68,7 +68,7 @@ describe("browser-pool/basic-pool", () => { const publicAPI = {}; const browser = stubBrowser("some-id", "some-version", publicAPI); browser.init.resolves(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const emitter = new AsyncEmitter().on(Events.SESSION_START, () => Promise.reject(new Error("foo"))); @@ -82,7 +82,7 @@ describe("browser-pool/basic-pool", () => { describe("SESSION_START event", () => { it("should be emitted after browser init", async () => { const browser = stubBrowser(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const onSessionStart = sinon.stub().named("onSessionStart"); const emitter = new AsyncEmitter().on(Events.SESSION_START, onSessionStart); @@ -94,7 +94,7 @@ describe("browser-pool/basic-pool", () => { it("handler should be waited by pool", async () => { const browser = stubBrowser(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const afterSessionStart = sinon.stub().named("afterSessionStart"); const emitter = new AsyncEmitter().on(Events.SESSION_START, () => Promise.delay(1).then(afterSessionStart)); @@ -114,7 +114,7 @@ describe("browser-pool/basic-pool", () => { it("on handler fail browser should be finalized", async () => { const browser = stubBrowser(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const emitter = new AsyncEmitter().on(Events.SESSION_START, () => Promise.reject(new Error())); @@ -205,7 +205,7 @@ describe("browser-pool/basic-pool", () => { it("should quit browser once if it was launched after cancel", async () => { const browser = stubBrowser(); - Browser.create.returns(browser); + NewBrowser.create.returns(browser); const emitter = new AsyncEmitter(); const pool = mkPool_({ emitter }); diff --git a/test/src/browser-pool/caching-pool.js b/test/src/browser-pool/caching-pool.js index 4ae116588..9a3e3022e 100644 --- a/test/src/browser-pool/caching-pool.js +++ b/test/src/browser-pool/caching-pool.js @@ -1,7 +1,7 @@ "use strict"; const Promise = require("bluebird"); -const Pool = require("src/browser-pool/caching-pool"); +const { CachingPool } = require("src/browser-pool/caching-pool"); const { buildCompositeBrowserId } = require("src/browser-pool/utils"); const stubBrowser = require("./util").stubBrowser; @@ -20,7 +20,7 @@ describe("browser-pool/caching-pool", () => { }, }; - return new Pool(underlyingPool, config, {}); + return new CachingPool(underlyingPool, config, {}); }; const makePool_ = () => poolWithReuseLimits_({ bro: Infinity }); diff --git a/test/src/browser-pool/index.js b/test/src/browser-pool/index.js index 156a2e277..b2cf793df 100644 --- a/test/src/browser-pool/index.js +++ b/test/src/browser-pool/index.js @@ -1,8 +1,8 @@ "use strict"; -const BasicPool = require("src/browser-pool/basic-pool"); -const LimitedPool = require("src/browser-pool/limited-pool"); -const PerBrowserLimitedPool = require("src/browser-pool/per-browser-limited-pool"); +const { BasicPool } = require("src/browser-pool/basic-pool"); +const { LimitedPool } = require("src/browser-pool/limited-pool"); +const { PerBrowserLimitedPool } = require("src/browser-pool/per-browser-limited-pool"); const pool = require("src/browser-pool"); const _ = require("lodash"); const { EventEmitter } = require("events"); diff --git a/test/src/browser-pool/limited-pool.js b/test/src/browser-pool/limited-pool.js index bfcb818ce..787644577 100644 --- a/test/src/browser-pool/limited-pool.js +++ b/test/src/browser-pool/limited-pool.js @@ -1,7 +1,7 @@ "use strict"; const Promise = require("bluebird"); -const LimitedPool = require("src/browser-pool/limited-pool"); +const { LimitedPool } = require("src/browser-pool/limited-pool"); const { CancelledError } = require("src/browser-pool/cancelled-error"); const stubBrowser = require("./util").stubBrowser; @@ -170,6 +170,11 @@ describe("browser-pool/limited-pool", () => { it("taking into account number of failed browser requests", () => { const browser = stubBrowser(); const pool = makePool_({ limit: 2 }); + const reflect = promise => { + return promise + .then(value => ({ isFulfilled: true, value })) + .catch(error => ({ isFulfilled: false, error })); + }; underlyingPool.getBrowser .withArgs("first") @@ -177,7 +182,7 @@ describe("browser-pool/limited-pool", () => { .withArgs("second") .returns(Promise.reject()); - return Promise.all([pool.getBrowser("first"), pool.getBrowser("second").reflect()]) + return Promise.all([pool.getBrowser("first"), reflect(pool.getBrowser("second"))]) .then(() => pool.freeBrowser(browser)) .then(() => assert.calledWith(underlyingPool.freeBrowser, browser, sinon.match({ force: true }))); }); @@ -213,9 +218,17 @@ describe("browser-pool/limited-pool", () => { it("should not launch browsers out of limit", () => { underlyingPool.getBrowser.returns(Promise.resolve(stubBrowser())); + const withTimeout = async (promise, ms, timeoutMessage) => { + let timeout; + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(timeoutMessage)), ms); + }); + + return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout)); + }; const pool = makePool_({ limit: 1 }); - const result = pool.getBrowser("first").then(() => pool.getBrowser("second").timeout(100, "timeout")); + const result = pool.getBrowser("first").then(() => withTimeout(pool.getBrowser("second"), 100, "timeout")); return assert.isRejected(result, /timeout$/); }); diff --git a/test/src/browser-pool/per-browser-limited-pool.js b/test/src/browser-pool/per-browser-limited-pool.js index 5a295e98d..61c1527df 100644 --- a/test/src/browser-pool/per-browser-limited-pool.js +++ b/test/src/browser-pool/per-browser-limited-pool.js @@ -1,9 +1,9 @@ "use strict"; const _ = require("lodash"); -const PerBrowserLimitedPool = require("src/browser-pool/per-browser-limited-pool"); -const LimitedPool = require("src/browser-pool/limited-pool"); -const BasicPool = require("src/browser-pool/basic-pool"); +const { PerBrowserLimitedPool } = require("src/browser-pool/per-browser-limited-pool"); +const { LimitedPool } = require("src/browser-pool/limited-pool"); +const { BasicPool } = require("src/browser-pool/basic-pool"); const stubBrowser = require("./util").stubBrowser; describe("browser-pool/per-browser-limited-pool", () => { diff --git a/test/src/browser/history/callstack.js b/test/src/browser/history/callstack.js index 423925fbe..45fcb32ac 100644 --- a/test/src/browser/history/callstack.js +++ b/test/src/browser/history/callstack.js @@ -1,7 +1,7 @@ "use strict"; const Callstack = require("src/browser/history/callstack"); -const { historyDataMap } = require("src/browser/history/utils"); +const { TestStepKey } = require("src/types"); describe("commands-history", () => { describe("callstack", () => { @@ -70,8 +70,8 @@ describe("commands-history", () => { }); it("should remove child nodes when parent leaves a stack", () => { - stack.enter({ [historyDataMap.KEY]: 2 }); - stack.enter({ [historyDataMap.KEY]: 3 }); + stack.enter({ [TestStepKey.Key]: 2 }); + stack.enter({ [TestStepKey.Key]: 3 }); stack.leave(2); diff --git a/test/src/browser/utils.js b/test/src/browser/utils.js index 21747e385..30af7983d 100644 --- a/test/src/browser/utils.js +++ b/test/src/browser/utils.js @@ -2,7 +2,7 @@ const _ = require("lodash"); const EventEmitter = require("events"); -const NewBrowser = require("src/browser/new-browser"); +const { NewBrowser } = require("src/browser/new-browser"); const ExistingBrowser = require("src/browser/existing-browser"); const { WEBDRIVER_PROTOCOL } = require("src/constants/config"); diff --git a/test/src/cli/index.js b/test/src/cli/index.js index 97cdec943..2942881dc 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -3,7 +3,7 @@ const { Command } = require("@gemini-testing/commander"); const proxyquire = require("proxyquire").noCallThru(); const testplaneCli = require("src/cli"); -const info = require("src/cli/info"); +const { configOverriding } = require("src/cli/info"); const defaults = require("src/config/defaults"); const { Testplane } = require("src/testplane"); const logger = require("src/utils/logger"); @@ -43,18 +43,18 @@ describe("cli", () => { await run_("--help"); assert.calledOnce(logger.log); - assert.calledWith(logger.log, info.configOverriding()); + assert.calledWith(logger.log, configOverriding()); }); it("should show information about testplane by default", async () => { - const defaultResult = info.configOverriding(); + const defaultResult = configOverriding(); assert.isTrue(defaultResult.includes("testplane")); assert.isFalse(defaultResult.includes("hermione")); }); it("should show information about hermione", async () => { - const result = info.configOverriding({ cliName: "hermione" }); + const result = configOverriding({ cliName: "hermione" }); assert.isTrue(result.includes("hermione")); assert.isFalse(result.includes("testplane")); diff --git a/test/src/runner/browser-agent.js b/test/src/runner/browser-agent.js index 2c9459350..9dcbc4396 100644 --- a/test/src/runner/browser-agent.js +++ b/test/src/runner/browser-agent.js @@ -1,7 +1,7 @@ "use strict"; const BrowserAgent = require("src/runner/browser-agent"); -const BrowserPool = require("src/browser-pool/basic-pool"); +const { BasicPool } = require("src/browser-pool/basic-pool"); describe("runner/browser-agent", () => { const sandbox = sinon.createSandbox(); @@ -9,7 +9,7 @@ describe("runner/browser-agent", () => { function mkAgent_({ id, version, pool } = {}) { id = id || "some-default-browser"; version = version || "some.default.version"; - pool = pool || Object.create(BrowserPool.prototype); + pool = pool || Object.create(BasicPool.prototype); return BrowserAgent.create({ id, version, pool }); } @@ -26,8 +26,8 @@ describe("runner/browser-agent", () => { } beforeEach(() => { - sandbox.stub(BrowserPool.prototype, "getBrowser").resolves(mkBrowser_()); - sandbox.stub(BrowserPool.prototype, "freeBrowser").resolves({}); + sandbox.stub(BasicPool.prototype, "getBrowser").resolves(mkBrowser_()); + sandbox.stub(BasicPool.prototype, "freeBrowser").resolves({}); }); afterEach(() => { @@ -44,13 +44,13 @@ describe("runner/browser-agent", () => { it("should request browser associated with agent", async () => { await mkAgent_({ id: "bro" }).getBrowser(); - assert.calledOnceWith(BrowserPool.prototype.getBrowser, "bro"); + assert.calledOnceWith(BasicPool.prototype.getBrowser, "bro"); }); it("should request browser with passed options", async () => { await mkAgent_({ version: "100.500" }).getBrowser({ foo: "bar" }); - assert.calledOnceWith(BrowserPool.prototype.getBrowser, sinon.match.any, { + assert.calledOnceWith(BasicPool.prototype.getBrowser, sinon.match.any, { foo: "bar", version: "100.500", }); @@ -58,7 +58,7 @@ describe("runner/browser-agent", () => { it("should return browser returned by pool", async () => { const bro = mkBrowser_(); - BrowserPool.prototype.getBrowser.resolves(bro); + BasicPool.prototype.getBrowser.resolves(bro); const browser = await mkAgent_().getBrowser(); @@ -69,24 +69,24 @@ describe("runner/browser-agent", () => { const broFoo = mkBrowser_({ sessionId: "foo" }); const broBar = mkBrowser_({ sessionId: "bar" }); - BrowserPool.prototype.getBrowser.resolves(broFoo); + BasicPool.prototype.getBrowser.resolves(broFoo); const browserAgent = mkAgent_(); await browserAgent.getBrowser(); - BrowserPool.prototype.getBrowser.reset(); + BasicPool.prototype.getBrowser.reset(); - BrowserPool.prototype.getBrowser.onFirstCall().resolves(broFoo).onSecondCall().resolves(broBar); + BasicPool.prototype.getBrowser.onFirstCall().resolves(broFoo).onSecondCall().resolves(broBar); const bro2 = await browserAgent.getBrowser(); assert.equal(bro2, broBar); - assert.calledTwice(BrowserPool.prototype.getBrowser); - assert.calledOnceWith(BrowserPool.prototype.freeBrowser, sinon.match.any, { force: true }); + assert.calledTwice(BasicPool.prototype.getBrowser); + assert.calledOnceWith(BasicPool.prototype.freeBrowser, sinon.match.any, { force: true }); }); it("should always request browser with passed options", async () => { - BrowserPool.prototype.getBrowser + BasicPool.prototype.getBrowser .onCall(0) .resolves(mkBrowser_({ sessionId: "foo" })) .onCall(1) @@ -99,7 +99,7 @@ describe("runner/browser-agent", () => { await browserAgent.getBrowser({ some: "opt" }); await browserAgent.getBrowser({ some: "opt" }); - assert.alwaysCalledWith(BrowserPool.prototype.getBrowser, "bro", { some: "opt", version: "100.500" }); + assert.alwaysCalledWith(BasicPool.prototype.getBrowser, "bro", { some: "opt", version: "100.500" }); }); }); @@ -109,7 +109,7 @@ describe("runner/browser-agent", () => { await mkAgent_().freeBrowser(bro); - assert.calledOnceWith(BrowserPool.prototype.freeBrowser, bro); + assert.calledOnceWith(BasicPool.prototype.freeBrowser, bro); }); it("should not force free if browser is ok", async () => { @@ -117,7 +117,7 @@ describe("runner/browser-agent", () => { await mkAgent_().freeBrowser(bro); - assert.calledWith(BrowserPool.prototype.freeBrowser, sinon.match.any, { force: false }); + assert.calledWith(BasicPool.prototype.freeBrowser, sinon.match.any, { force: false }); }); it("should force free broken browser", async () => { @@ -125,12 +125,12 @@ describe("runner/browser-agent", () => { await mkAgent_().freeBrowser(bro); - assert.calledWith(BrowserPool.prototype.freeBrowser, sinon.match.any, { force: true }); + assert.calledWith(BasicPool.prototype.freeBrowser, sinon.match.any, { force: true }); }); it("should resolve with pool free result", async () => { const freeResult = { foo: "bar" }; - BrowserPool.prototype.freeBrowser.resolves(freeResult); + BasicPool.prototype.freeBrowser.resolves(freeResult); const result = await mkAgent_().freeBrowser(mkBrowser_()); diff --git a/test/src/test-reader/build-instructions.js b/test/src/test-reader/build-instructions.js index e4ef5290b..ffdb702bb 100644 --- a/test/src/test-reader/build-instructions.js +++ b/test/src/test-reader/build-instructions.js @@ -1,16 +1,24 @@ "use strict"; const _ = require("lodash"); -const { InstructionsList, Instructions } = require("src/test-reader/build-instructions"); +const proxyquire = require("proxyquire"); const { TreeBuilder } = require("src/test-reader/tree-builder"); -const validators = require("src/validators"); const env = require("src/utils/env"); const RuntimeConfig = require("src/config/runtime-config"); const { makeConfigStub } = require("../../utils"); describe("test-reader/build-instructions", () => { + let InstructionsList, Instructions, validateUnknownBrowsers; const sandbox = sinon.createSandbox(); + beforeEach(() => { + validateUnknownBrowsers = sandbox.stub(); + ({ InstructionsList, Instructions } = proxyquire("src/test-reader/build-instructions", { + "../validators": { + validateUnknownBrowsers, + }, + })); + }); afterEach(() => { sandbox.restore(); }); @@ -251,7 +259,6 @@ describe("test-reader/build-instructions", () => { describe("buildGlobalSkipInstruction", () => { beforeEach(() => { - sandbox.stub(validators, "validateUnknownBrowsers"); sandbox.stub(env, "parseCommaSeparatedValue").returns({ value: [] }); }); @@ -262,7 +269,7 @@ describe("test-reader/build-instructions", () => { Instructions.buildGlobalSkipInstruction(makeConfigStub({ browsers: ["foo", "bar"] })); - assert.calledOnceWith(validators.validateUnknownBrowsers, ["baz"], ["foo", "bar"]); + assert.calledOnceWith(validateUnknownBrowsers, ["baz"], ["foo", "bar"]); }); it("should set noop instruction if skip list is not specified", () => { diff --git a/test/src/test-reader/index.js b/test/src/test-reader/index.js index 53415487b..05f03eedb 100644 --- a/test/src/test-reader/index.js +++ b/test/src/test-reader/index.js @@ -1,10 +1,10 @@ "use strict"; -const TestReader = require("src/test-reader"); +const { TestReader } = require("src/test-reader"); const { TestParser } = require("src/test-reader/test-parser"); const { MasterEvents: Events } = require("src/events"); -const SetsBuilder = require("src/test-reader/sets-builder"); -const SetCollection = require("src/test-reader/sets-builder/set-collection"); +const { SetsBuilder } = require("src/test-reader/sets-builder"); +const { SetCollection } = require("src/test-reader/sets-builder/set-collection"); const { makeConfigStub } = require("../../utils"); const _ = require("lodash"); diff --git a/test/src/test-reader/sets-builder/index.js b/test/src/test-reader/sets-builder/index.js index 2897634f8..3b0ad063d 100644 --- a/test/src/test-reader/sets-builder/index.js +++ b/test/src/test-reader/sets-builder/index.js @@ -1,29 +1,47 @@ "use strict"; const globExtra = require("glob-extra"); -const fs = require("fs"); -const SetBuilder = require("src/test-reader/sets-builder"); -const SetCollection = require("src/test-reader/sets-builder/set-collection"); -const TestSet = require("src/test-reader/sets-builder/test-set"); +const fs = require("fs/promises"); +const proxyquire = require("proxyquire"); describe("test-reader/sets-builder", () => { + let globExtraStub, SetsBuilder, SetCollection, TestSet, setCollection; const sandbox = sinon.createSandbox(); - const setCollection = sinon.createStubInstance(SetCollection); - const createSetBuilder = (sets, opts) => SetBuilder.create(sets || { all: { files: ["some/path"] } }, opts || {}); + const createSetBuilder = (sets, opts) => SetsBuilder.create(sets || { all: { files: ["some/path"] } }, opts || {}); beforeEach(() => { - sandbox.stub(SetCollection, "create").resolves(); - sandbox.stub(globExtra, "expandPaths").resolves([]); + sandbox.stub(fs, "stat").resolves({ isDirectory: () => false }); + globExtraStub = { + expandPaths: sandbox.stub().resolves([]), + isMask: globExtra.isMask, + }; + ({ TestSet } = proxyquire("src/test-reader/sets-builder/test-set", { + "glob-extra": globExtraStub, + })); sandbox.stub(TestSet.prototype, "resolveFiles"); - sandbox.stub(fs, "stat").yields(null, { isDirectory: () => false }); + ({ SetCollection } = proxyquire("src/test-reader/sets-builder/set-collection", { + "glob-extra": globExtraStub, + })); + ({ SetsBuilder } = proxyquire("src/test-reader/sets-builder", { + "glob-extra": globExtraStub, + "./test-set": { + TestSet, + }, + "./set-collection": { + SetCollection, + }, + })); + sandbox.stub(SetCollection, "create").resolves(); + + setCollection = sinon.createStubInstance(SetCollection); }); afterEach(() => sandbox.restore()); describe("build", () => { it("should create set collection for all sets if sets to use are not specified", () => { - globExtra.expandPaths + globExtraStub.expandPaths .withArgs(["some/files"]) .resolves(["some/files/file1.js"]) .withArgs(["other/files"]) @@ -79,7 +97,7 @@ describe("test-reader/sets-builder", () => { it("should use default paths", () => { sandbox.stub(TestSet.prototype, "expandFiles"); - globExtra.expandPaths.withArgs(["project/path"]).resolves(["project/path"]); + globExtraStub.expandPaths.withArgs(["project/path"]).resolves(["project/path"]); const setStub = TestSet.create({ files: ["project/path"] }); @@ -88,7 +106,7 @@ describe("test-reader/sets-builder", () => { return createSetBuilder({ default: { files: [] } }, { defaultPaths: ["project/path"] }) .build() .then(result => { - assert.calledWith(globExtra.expandPaths, ["project/path"]); + assert.calledWith(globExtraStub.expandPaths, ["project/path"]); assert.deepEqual(result, setCollection); }); }); @@ -135,11 +153,11 @@ describe("test-reader/sets-builder", () => { describe("useSets", () => { it("should be chainable", () => { - assert.instanceOf(createSetBuilder().useSets(), SetBuilder); + assert.instanceOf(createSetBuilder().useSets(), SetsBuilder); }); it("should create set collection for specified sets", () => { - globExtra.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); + globExtraStub.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); const sets = { set1: { files: ["some/files"] }, @@ -172,11 +190,11 @@ describe("test-reader/sets-builder", () => { beforeEach(() => sandbox.stub(TestSet.prototype, "expandFiles")); it("should be chainable", () => { - assert.instanceOf(createSetBuilder().useFiles(), SetBuilder); + assert.instanceOf(createSetBuilder().useFiles(), SetsBuilder); }); it("should throw an error if sets do not contain paths from opts", () => { - globExtra.expandPaths.withArgs(["other/files"]).resolves(["other/files/file.js"]); + globExtraStub.expandPaths.withArgs(["other/files"]).resolves(["other/files/file.js"]); const sets = { all: { files: ["some/files"] }, @@ -202,15 +220,15 @@ describe("test-reader/sets-builder", () => { it("should expand passed files with passed glob options", () => { const globOpts = sandbox.stub(); sandbox.stub(TestSet.prototype, "useFiles"); - globExtra.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); + globExtraStub.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); return createSetBuilder() .useFiles(["some/files"]) .build("", globOpts) .then(() => { - assert.calledOnce(globExtra.expandPaths); + assert.calledOnce(globExtraStub.expandPaths); assert.calledWith( - globExtra.expandPaths, + globExtraStub.expandPaths, ["some/files"], sinon.match({ formats: [".js", ".mjs"] }), globOpts, @@ -219,7 +237,7 @@ describe("test-reader/sets-builder", () => { }); it("should throw an error if no files were found with specified paths", () => { - globExtra.expandPaths.withArgs(["some/files"]).resolves([]); + globExtraStub.expandPaths.withArgs(["some/files"]).resolves([]); return assert.isRejected( createSetBuilder().useFiles(["some/files", "another/files"]).build(), @@ -229,7 +247,7 @@ describe("test-reader/sets-builder", () => { it("should apply files to all sets if sets are specified", () => { sandbox.stub(TestSet.prototype, "useFiles"); - globExtra.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); + globExtraStub.expandPaths.withArgs(["some/files"]).resolves(["some/files/file.js"]); const sets = { all: { files: ["some/files"] }, @@ -245,7 +263,7 @@ describe("test-reader/sets-builder", () => { }); it("should use default directory if sets are not specified and paths are not passed", () => { - globExtra.expandPaths.withArgs(["project/path"]).resolves(["project/path"]); + globExtraStub.expandPaths.withArgs(["project/path"]).resolves(["project/path"]); const setStub = TestSet.create({ files: ["project/path"] }); @@ -255,7 +273,7 @@ describe("test-reader/sets-builder", () => { .useFiles([]) .build() .then(result => { - assert.calledWith(globExtra.expandPaths, ["project/path"]); + assert.calledWith(globExtraStub.expandPaths, ["project/path"]); assert.deepEqual(result, setCollection); }); }); @@ -265,7 +283,7 @@ describe("test-reader/sets-builder", () => { beforeEach(() => sandbox.stub(TestSet.prototype, "useBrowsers")); it("should be chainable", () => { - assert.instanceOf(createSetBuilder().useBrowsers(), SetBuilder); + assert.instanceOf(createSetBuilder().useBrowsers(), SetsBuilder); }); it("should use passed browsers in sets", () => { diff --git a/test/src/test-reader/sets-builder/set-collection.js b/test/src/test-reader/sets-builder/set-collection.js index bb4b2d5fb..1547a16a7 100644 --- a/test/src/test-reader/sets-builder/set-collection.js +++ b/test/src/test-reader/sets-builder/set-collection.js @@ -1,7 +1,7 @@ "use strict"; -const SetCollection = require("src/test-reader/sets-builder/set-collection"); -const TestSet = require("src/test-reader/sets-builder/test-set"); +const { SetCollection } = require("src/test-reader/sets-builder/set-collection"); +const { TestSet } = require("src/test-reader/sets-builder/test-set"); describe("test-reader/sets-builder/set-collection", () => { const sandbox = sinon.createSandbox(); diff --git a/test/src/test-reader/sets-builder/test-set.js b/test/src/test-reader/sets-builder/test-set.js index e9b9254f9..481fa9dca 100644 --- a/test/src/test-reader/sets-builder/test-set.js +++ b/test/src/test-reader/sets-builder/test-set.js @@ -1,7 +1,8 @@ "use strict"; -const fs = require("fs"); +const fs = require("fs/promises"); const proxyquire = require("proxyquire"); +const globExtra = require("glob-extra"); describe("test-reader/sets-builder/test-set", () => { const sandbox = sinon.createSandbox(); @@ -10,10 +11,11 @@ describe("test-reader/sets-builder/test-set", () => { beforeEach(() => { globExtraStub = { expandPaths: sinon.stub(), + isMask: globExtra.isMask, }; - TestSet = proxyquire("src/test-reader/sets-builder/test-set", { + ({ TestSet } = proxyquire("src/test-reader/sets-builder/test-set", { "glob-extra": globExtraStub, - }); + })); }); afterEach(() => sandbox.restore()); @@ -208,14 +210,14 @@ describe("test-reader/sets-builder/test-set", () => { beforeEach(() => sandbox.stub(fs, "stat")); it("should transform set paths to masks if paths are directories", () => { - fs.stat.yields(null, { isDirectory: () => true }); + fs.stat.resolves({ isDirectory: () => true }); const set = TestSet.create({ files: ["some/path"] }); return set.transformDirsToMasks().then(() => assert.deepEqual(set.getFiles(), ["some/path/**"])); }); it("should not mutate constructor arguments", () => { - fs.stat.yields(null, { isDirectory: () => true }); + fs.stat.resolves({ isDirectory: () => true }); const input = { files: ["some/path"] }; const set = TestSet.create(input); @@ -229,14 +231,14 @@ describe("test-reader/sets-builder/test-set", () => { }); it("should not transform set paths to masks if paths are files", () => { - fs.stat.yields(null, { isDirectory: () => false }); + fs.stat.resolves({ isDirectory: () => false }); const set = TestSet.create({ files: ["some/path/file.js"] }); return set.transformDirsToMasks().then(() => assert.deepEqual(set.getFiles(), ["some/path/file.js"])); }); it("should throw an error if passed path does not exist", () => { - fs.stat.throws(new Error()); + fs.stat.rejects(new Error()); const set = TestSet.create({ files: ["some/error/file.js"] }); diff --git a/test/src/testplane.js b/test/src/testplane.js index 8c4f826fd..1cd157ed3 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -13,7 +13,7 @@ const { AsyncEmitter } = require("src/events/async-emitter"); const eventsUtils = require("src/events/utils"); const { default: Errors } = require("src/errors"); const { Stats: RunnerStats } = require("src/stats"); -const TestReader = require("src/test-reader"); +const { TestReader } = require("src/test-reader"); const { TestCollection } = require("src/test-collection"); const { MasterEvents: RunnerEvents, CommonSyncEvents, MasterAsyncEvents, MasterSyncEvents } = require("src/events"); const { MainRunner: NodejsEnvRunner } = require("src/runner"); diff --git a/test/src/worker/browser-env/runner/test-runner/index.ts b/test/src/worker/browser-env/runner/test-runner/index.ts index bb35aaef4..022d0d259 100644 --- a/test/src/worker/browser-env/runner/test-runner/index.ts +++ b/test/src/worker/browser-env/runner/test-runner/index.ts @@ -17,7 +17,7 @@ import { import { VITE_RUN_UUID_ROUTE } from "../../../../../../src/runner/browser-env/vite/constants"; import { makeBrowserConfigStub } from "../../../../../utils"; import { Test, Suite } from "../../../../../../src/test-reader/test-object"; -import BrowserAgent from "../../../../../../src/worker/runner/browser-agent"; +import { BrowserAgent } from "../../../../../../src/worker/runner/browser-agent"; import history from "../../../../../../src/browser/history"; import logger from "../../../../../../src/utils/logger"; import OneTimeScreenshooter from "../../../../../../src/worker/runner/test-runner/one-time-screenshooter"; @@ -42,6 +42,7 @@ import type { Test as TestType } from "../../../../../../src/test-reader/test-ob import type { BrowserConfig } from "../../../../../../src/config/browser-config"; import type { WorkerRunTestResult } from "../../../../../../src/worker/testplane"; import { AbortOnReconnectError } from "../../../../../../src/errors/abort-on-reconnect-error"; +import ExistingBrowser from "../../../../../../src/browser/existing-browser"; interface TestOpts { title: string; @@ -118,24 +119,25 @@ describe("worker/browser-env/runner/test-runner", () => { return promise; }; - const mkBrowser_ = (opts: Partial = {}): Browser => ({ - publicAPI: { - url: sandbox.stub().resolves(), - } as unknown as Browser["publicAPI"], - config: makeBrowserConfigStub({ saveHistoryMode: "none" }) as BrowserConfig, - state: { - isBroken: false, - }, - applyState: sandbox.stub(), - customCommands: [], - callstackHistory: { - enter: sandbox.stub(), - leave: sandbox.stub(), - markError: sandbox.stub(), - release: sandbox.stub(), - } as unknown as Browser["callstackHistory"], - ...opts, - }); + const mkBrowser_ = (opts: Partial = {}): ExistingBrowser => + ({ + publicAPI: { + url: sandbox.stub().resolves(), + } as unknown as Browser["publicAPI"], + config: makeBrowserConfigStub({ saveHistoryMode: "none" }) as BrowserConfig, + state: { + isBroken: false, + }, + applyState: sandbox.stub(), + customCommands: [], + callstackHistory: { + enter: sandbox.stub(), + leave: sandbox.stub(), + markError: sandbox.stub(), + release: sandbox.stub(), + } as unknown as Browser["callstackHistory"], + ...opts, + } as ExistingBrowser); const mkElement_ = (opts: Partial = {}): WebdriverIO.Element => { return { @@ -695,7 +697,8 @@ describe("worker/browser-env/runner/test-runner", () => { let browser: Browser; beforeEach(() => { - global.expect = sandbox.stub() as unknown as ExpectWebdriverIO.Expect; + (global as Partial<{ expect: ExpectWebdriverIO.Expect }>).expect = + sandbox.stub() as unknown as ExpectWebdriverIO.Expect; browser = mkBrowser_(); (BrowserAgent.prototype.getBrowser as SinonStub).resolves(browser); @@ -707,7 +710,8 @@ describe("worker/browser-env/runner/test-runner", () => { describe("should return error if", () => { it("expect module is not found", done => { - global.expect = undefined as unknown as ExpectWebdriverIO.Expect; + (global as Partial<{ expect: ExpectWebdriverIO.Expect }>).expect = + undefined as unknown as ExpectWebdriverIO.Expect; const socket = mkSocket_() as BrowserViteSocket; socketClientStub.returns(socket); diff --git a/test/src/worker/runner/browser-agent.js b/test/src/worker/runner/browser-agent.js index 9be24b218..7b506c61e 100644 --- a/test/src/worker/runner/browser-agent.js +++ b/test/src/worker/runner/browser-agent.js @@ -1,6 +1,6 @@ "use strict"; -const BrowserAgent = require("src/worker/runner/browser-agent"); +const { BrowserAgent } = require("src/worker/runner/browser-agent"); const BrowserPool = require("src/worker/runner/browser-pool"); describe("worker/browser-agent", () => { @@ -9,7 +9,7 @@ describe("worker/browser-agent", () => { beforeEach(() => (browserPool = sinon.createStubInstance(BrowserPool))); describe("getBrowser", () => { - it("should get a browser from the pool", () => { + it("should get a browser from the pool", async () => { browserPool.getBrowser .withArgs({ browserId: "bro-id", @@ -22,7 +22,7 @@ describe("worker/browser-agent", () => { .returns({ some: "browser" }); const browserAgent = BrowserAgent.create({ id: "bro-id", version: null, pool: browserPool }); - const browser = browserAgent.getBrowser({ + const browser = await browserAgent.getBrowser({ sessionId: "100-500", sessionCaps: "some-caps", sessionOpts: "some-opts", @@ -32,7 +32,7 @@ describe("worker/browser-agent", () => { assert.deepEqual(browser, { some: "browser" }); }); - it("should get a browser with specific version from the pool", () => { + it("should get a browser with specific version from the pool", async () => { browserPool.getBrowser .withArgs({ browserId: "bro-id", @@ -45,7 +45,7 @@ describe("worker/browser-agent", () => { .returns({ some: "browser" }); const browserAgent = BrowserAgent.create({ id: "bro-id", version: "10.1", pool: browserPool }); - const browser = browserAgent.getBrowser({ + const browser = await browserAgent.getBrowser({ sessionId: "100-500", sessionCaps: "some-caps", sessionOpts: "some-opts", diff --git a/test/src/worker/runner/caching-test-parser.js b/test/src/worker/runner/caching-test-parser.js index 564765ef7..46cea098c 100644 --- a/test/src/worker/runner/caching-test-parser.js +++ b/test/src/worker/runner/caching-test-parser.js @@ -1,7 +1,7 @@ "use strict"; -const CachingTestParser = require("src/worker/runner/caching-test-parser"); -const SequenceTestParser = require("src/worker/runner/sequence-test-parser"); +const { CachingTestParser } = require("src/worker/runner/caching-test-parser"); +const { SequenceTestParser } = require("src/worker/runner/sequence-test-parser"); const { WorkerEvents: RunnerEvents } = require("src/events"); const { TestCollection } = require("src/test-collection"); const { makeConfigStub, makeTest } = require("../../../utils"); diff --git a/test/src/worker/runner/index.js b/test/src/worker/runner/index.js index 08c50da27..114d9b9c9 100644 --- a/test/src/worker/runner/index.js +++ b/test/src/worker/runner/index.js @@ -2,8 +2,8 @@ const Runner = require("src/worker/runner"); const BrowserPool = require("src/worker/runner/browser-pool"); -const CachingTestParser = require("src/worker/runner/caching-test-parser"); -const BrowserAgent = require("src/worker/runner/browser-agent"); +const { CachingTestParser } = require("src/worker/runner/caching-test-parser"); +const { BrowserAgent } = require("src/worker/runner/browser-agent"); const { WorkerEvents: RunnerEvents } = require("src/events"); const NodejsEnvTestRunner = require("src/worker/runner/test-runner"); const { TestRunner: BrowserEnvTestRunner } = require("src/worker/browser-env/runner/test-runner"); diff --git a/test/src/worker/runner/sequence-test-parser.js b/test/src/worker/runner/sequence-test-parser.js index 5fb4db980..fccf34fac 100644 --- a/test/src/worker/runner/sequence-test-parser.js +++ b/test/src/worker/runner/sequence-test-parser.js @@ -1,7 +1,7 @@ "use strict"; -const SequenceTestParser = require("src/worker/runner/sequence-test-parser"); -const SimpleTestParser = require("src/worker/runner/simple-test-parser"); +const { SequenceTestParser } = require("src/worker/runner/sequence-test-parser"); +const { SimpleTestParser } = require("src/worker/runner/simple-test-parser"); const { WorkerEvents: RunnerEvents } = require("src/events"); const { makeConfigStub, makeTest } = require("../../../utils"); const Promise = require("bluebird"); diff --git a/test/src/worker/runner/simple-test-parser.js b/test/src/worker/runner/simple-test-parser.js index 58904aeb6..6c7350a70 100644 --- a/test/src/worker/runner/simple-test-parser.js +++ b/test/src/worker/runner/simple-test-parser.js @@ -1,6 +1,6 @@ "use strict"; -const SimpleTestParser = require("src/worker/runner/sequence-test-parser"); +const { SimpleTestParser } = require("src/worker/runner/simple-test-parser"); const { WorkerEvents: RunnerEvents } = require("src/events"); const { TestParser } = require("src/test-reader/test-parser"); const { makeConfigStub, makeTest } = require("../../../utils"); diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index e3e8d940a..c0c5c9db2 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -6,7 +6,7 @@ const TestRunner = require("src/worker/runner/test-runner"); const HookRunner = require("src/worker/runner/test-runner/hook-runner"); const ExecutionThread = require("src/worker/runner/test-runner/execution-thread"); const OneTimeScreenshooter = require("src/worker/runner/test-runner/one-time-screenshooter"); -const BrowserAgent = require("src/worker/runner/browser-agent"); +const { BrowserAgent } = require("src/worker/runner/browser-agent"); const { AssertViewError } = require("src/browser/commands/assert-view/errors/assert-view-error"); const AssertViewResults = require("src/browser/commands/assert-view/assert-view-results"); const { Suite, Test } = require("src/test-reader/test-object"); diff --git a/test/utils.js b/test/utils.js index 581469f2f..9b67fcf56 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,7 +1,7 @@ "use strict"; const _ = require("lodash"); -const Browser = require("../src/browser/new-browser"); +const Browser = require("../src/browser/new-browser").default; const { NODEJS_TEST_RUN_ENV } = require("../src/constants/config"); function browserWithId(id) { diff --git a/typings/api.d.ts b/typings/api.d.ts deleted file mode 100644 index db06e0adb..000000000 --- a/typings/api.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -type TestDefinition = import("../build/src/test-reader/test-object/types").TestDefinition; -type SuiteDefinition = import("../build/src/test-reader/test-object/types").SuiteDefinition; -type TestHookDefinition = import("../build/src/test-reader/test-object/types").TestHookDefinition; - -declare const it: TestDefinition; -declare const describe: SuiteDefinition; -declare const beforeEach: TestHookDefinition; -declare const afterEach: TestHookDefinition; diff --git a/typings/clear-require.d.ts b/typings/clear-require.d.ts new file mode 100644 index 000000000..ab6e73ecc --- /dev/null +++ b/typings/clear-require.d.ts @@ -0,0 +1,3 @@ +declare module "clear-require" { + export default function clearRequire(moduleId: string): void; +} diff --git a/typings/escape-string-regexp.d.ts b/typings/escape-string-regexp.d.ts new file mode 100644 index 000000000..c47a88ca4 --- /dev/null +++ b/typings/escape-string-regexp.d.ts @@ -0,0 +1,3 @@ +declare module "escape-string-regexp" { + export default function escapeRe(str: string): string; +} diff --git a/typings/global.d.ts b/typings/global.d.ts index 8f89011bb..a64033877 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -1,11 +1,14 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// -/// +/* eslint-disable @typescript-eslint/triple-slash-reference*/ +/// +/// +/* eslint-enable @typescript-eslint/triple-slash-reference */ +/// /// -declare namespace globalThis { - // eslint-disable-next-line no-var - var expect: ExpectWebdriverIO.Expect; +//Temporary workaround to get rid of Cannot find module 'rollup/parseAst' or its corresponding type declarations. There are types at '/node_modules/rollup/dist/parseAst.d.ts', but this result could not be resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'. +declare module "rollup/parseAst" { + export function parseAst(...args: unknown[]): unknown; + export function parseAstAsync(...args: unknown[]): Promise; } // remove after updating expect-webdriverio@4 (should migrate to esm first)