diff --git a/.circleci/config.yml b/.circleci/config.yml index d4029cf5c..733977edb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,40 +3,22 @@ jobs: build: working_directory: ~/html-reporter docker: - - image: cimg/node:16.20-browsers + - image: yinfra/html-reporter-browsers environment: - CHROME_VERSION: 116 SERVER_HOST: localhost - + steps: - - checkout - run: npm ci - run: name: Build html-reporter - command: npm run build - - run: - name: Download Chromium - command: >- - cd test/func/docker/browser-utils && - npm ci && - npm run install-chromium + - run: name: Install Chromium for Playwright command: npx playwright install chromium - - run: - name: Download Selenium - command: npm install selenium-standalone@9.1.1 -g - - run: - name: Start Selenium - command: >- - selenium-standalone install --drivers.chrome.version=$CHROME_VERSION && - selenium-standalone start --drivers.chrome.version=$CHROME_VERSION - background: true - - run: name: Functional tests command: npm run e2e diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f39acee9c..de6a5e4e0 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..b03d8f794 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,37 @@ +name: NPM publish +on: + workflow_dispatch: + inputs: + versionType: + type: choice + description: Version Type + required: true + options: + - patch + - minor + - major +permissions: + contents: write +jobs: + publish: + name: Publishing to NPM + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + token: ${{ secrets.GH_ACCESS_TOKEN }} + - name: Setup Node JS + uses: actions/setup-node@v2 + with: + node-version: 18 + registry-url: https://registry.npmjs.org + - run: npm ci + - run: git config --global user.email "y-infra@yandex.ru" + - run: git config --global user.name "y-infra" + - run: npm run release -- --release-as ${{ github.event.inputs.versionType }} + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: git push --follow-tags diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5c059c7..e4e19b389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,93 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [10.3.2](https://github.com/gemini-testing/html-reporter/compare/v10.3.1...v10.3.2) (2024-07-02) + + +### Bug Fixes + +* fix copy button text ([#563](https://github.com/gemini-testing/html-reporter/issues/563)) ([e197d22](https://github.com/gemini-testing/html-reporter/commit/e197d22)) + + + + +## [10.3.1](https://github.com/gemini-testing/html-reporter/compare/v10.3.0...v10.3.1) (2024-06-26) + + +### Bug Fixes + +* result state after screenshot-accepter image accept ([fa18026](https://github.com/gemini-testing/html-reporter/commit/fa18026)) + + + + +# [10.3.0](https://github.com/gemini-testing/html-reporter/compare/v10.0.0...v10.3.0) (2024-06-21) + + +### Bug Fixes + +* escape regexp chars when copying a test link ([#552](https://github.com/gemini-testing/html-reporter/issues/552)) ([e7b2103](https://github.com/gemini-testing/html-reporter/commit/e7b2103)) +* save RunMode to localStorage ([#554](https://github.com/gemini-testing/html-reporter/issues/554)) ([e523e21](https://github.com/gemini-testing/html-reporter/commit/e523e21)) +* **pwt:** ignore private report options from pwt ([88e9e5d](https://github.com/gemini-testing/html-reporter/commit/88e9e5d)) + + +### Features + +* add ability to run cli commands using html-reporter binary ([db0862c](https://github.com/gemini-testing/html-reporter/commit/db0862c)) +* add copy to clipboard button to all fields in meta ([fb651d0](https://github.com/gemini-testing/html-reporter/commit/fb651d0)) +* add relativePath to refImg ([b722899](https://github.com/gemini-testing/html-reporter/commit/b722899)) + + + + +# [10.2.0](https://github.com/gemini-testing/html-reporter/compare/v10.0.0...v10.2.0) (2024-06-20) + + +### Features + +* add relativePath to refImg ([b722899](https://github.com/gemini-testing/html-reporter/commit/b722899)) + + + + +# [10.1.0](https://github.com/gemini-testing/html-reporter/compare/v10.0.0...v10.1.0) (2024-06-20) + + +### Bug Fixes + +* escape regexp chars when copying a test link ([#552](https://github.com/gemini-testing/html-reporter/issues/552)) ([e7b2103](https://github.com/gemini-testing/html-reporter/commit/e7b2103)) +* save RunMode to localStorage ([#554](https://github.com/gemini-testing/html-reporter/issues/554)) ([e523e21](https://github.com/gemini-testing/html-reporter/commit/e523e21)) +* **pwt:** ignore private report options from pwt ([88e9e5d](https://github.com/gemini-testing/html-reporter/commit/88e9e5d)) + + +### Features + +* add ability to run cli commands using html-reporter binary ([db0862c](https://github.com/gemini-testing/html-reporter/commit/db0862c)) + + + + +# [10.0.0](https://github.com/gemini-testing/html-reporter/compare/v9.19.0...v10.0.0) (2024-05-31) + + +### Bug Fixes + +* align the menu icon with the rest of the items ([0d493cc](https://github.com/gemini-testing/html-reporter/commit/0d493cc)) +* handle edge case when error is null ([#523](https://github.com/gemini-testing/html-reporter/issues/523)) ([65b3808](https://github.com/gemini-testing/html-reporter/commit/65b3808)) + + +### Features + +* drop node versions less than 18 ([5569be6](https://github.com/gemini-testing/html-reporter/commit/5569be6)) + + +### BREAKING CHANGES + +* node versions less than 18.0.0 are no longer supported + + + # [9.19.0](https://github.com/gemini-testing/html-reporter/compare/v9.18.1...v9.19.0) (2024-05-01) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cc7b2bac..14fca0793 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,9 @@ In order to make e2e/screenshot tests stable and reproducible across different e you need to launch browsers inside a Docker container. 1. Make sure you have Docker installed. -
How to? +
+ How to? + 1. If you want to make a personal open-source contribution, you may use Docker free of charge and follow the [official guide](https://docs.docker.com/get-docker/). 2. If you are acting on behalf of a company, you may not have access to Docker Desktop. In this case: - On Linux, you may follow the official installation guide. @@ -20,9 +22,9 @@ you need to launch browsers inside a Docker container. - On Windows, you may use Windows Subsystem for Linux to run the Docker CLI without the Desktop application.
-2. Build and start an image with browsers: +2. Start an image with browsers: ``` - npm run e2e:build-browsers && npm run e2e:launch-browsers + npm run browsers:launch ``` 3. Run e2e tests: ```bash @@ -38,3 +40,36 @@ If you want a finer-grained control over the process, the following commands may - `npm run e2e:generate-fixtures` — generate fixture reports to run tests on - `npm run --workspace=test/func/tests gui:plugins` — launch hermione GUI for the `plugins` tests set - `npm run e2e:test` — run e2e tests only, without building packages or generating fixtures + +### Working with browser docker images + +#### Building an image for current platform + +If you want to build an image with browsers you can use this command: +- `npm run browsers:build:local` + +#### Building a multiplatform image on Mac (Apple Silicon) + +If you use colima then you can follow these steps to build a multiplatform image: +1. Install buildx + - `brew install docker-buildx` + - `docker buildx install` +2. Start 2 instances + - `colima start --profile amd --arch amd` + - `colima start --profile arm --arch arm` +3. Create a buildx context to use the created instances as nodes + - `docker buildx create --use --name custom colima-amd` + - `docker buildx create --append --name custom colima-arm` +4. Build and publish an image + - `npm run browsers:build-and-push` + +Note: to use already created buildx instance, execute this command: +- `docker buildx use custom` + +#### Managing multiple colima instances + +To get the list of all colima instances you can use `colima list`. +To use specific colima instance, you have to set DOCKER_HOST environment variable. +To get the desired value for DOCKER_HOST, use `colima status [INSTANCE]` + +If you want to update chromedriver or chrome version, change the variables at the beginning of the [Dockerfile](/test/func/docker/Dockerfile). diff --git a/bin/html-reporter b/bin/html-reporter new file mode 100755 index 000000000..7a5f491bc --- /dev/null +++ b/bin/html-reporter @@ -0,0 +1,6 @@ +#!/usr/bin/env node +'use strict'; + +(async () => { + await require('../build/lib/cli').run(); +})(); diff --git a/lib/adapters/test-result/hermione.ts b/lib/adapters/test-result/hermione.ts new file mode 100644 index 000000000..d51cfa40c --- /dev/null +++ b/lib/adapters/test-result/hermione.ts @@ -0,0 +1,5 @@ +export { + TestplaneTestResultAdapter as HermioneTestResultAdapter, + TestplaneTestResultAdapterOptions as HermioneTestResultAdapterOptions, + getStatus +} from './testplane'; diff --git a/lib/test-adapter/index.ts b/lib/adapters/test-result/index.ts similarity index 92% rename from lib/test-adapter/index.ts rename to lib/adapters/test-result/index.ts index db772e3bf..de07e5cc8 100644 --- a/lib/test-adapter/index.ts +++ b/lib/adapters/test-result/index.ts @@ -1,5 +1,5 @@ -import {TestStatus} from '../constants'; -import {ErrorDetails, ImageBase64, ImageFile, ImageInfoFull, TestError} from '../types'; +import {TestStatus} from '../../constants'; +import {ErrorDetails, ImageBase64, ImageFile, ImageInfoFull, TestError} from '../../types'; export interface ReporterTestResult { readonly attempt: number; diff --git a/lib/test-adapter/playwright.ts b/lib/adapters/test-result/playwright.ts similarity index 98% rename from lib/test-adapter/playwright.ts rename to lib/adapters/test-result/playwright.ts index 36612e733..cefeae6db 100644 --- a/lib/test-adapter/playwright.ts +++ b/lib/adapters/test-result/playwright.ts @@ -5,9 +5,9 @@ import _ from 'lodash'; import stripAnsi from 'strip-ansi'; import {ReporterTestResult} from './index'; -import {getError, getShortMD5, isImageDiffError, isNoRefImageError} from '../common-utils'; -import {ERROR, FAIL, PWT_TITLE_DELIMITER, SUCCESS, TestStatus} from '../constants'; -import {ErrorName} from '../errors'; +import {getError, getShortMD5, isImageDiffError, isNoRefImageError} from '../../common-utils'; +import {ERROR, FAIL, PWT_TITLE_DELIMITER, SUCCESS, TestStatus} from '../../constants'; +import {ErrorName} from '../../errors'; import { DiffOptions, ErrorDetails, @@ -16,7 +16,7 @@ import { ImageInfoFull, ImageInfoNoRef, ImageInfoPageError, ImageInfoPageSuccess, ImageInfoSuccess, ImageSize, TestError -} from '../types'; +} from '../../types'; import type {CoordBounds} from 'looks-same'; export type PlaywrightAttachment = PlaywrightTestResult['attachments'][number]; @@ -127,7 +127,7 @@ const getImageData = (attachment: PlaywrightAttachment | undefined): ImageFile | }; }; -export class PlaywrightTestAdapter implements ReporterTestResult { +export class PlaywrightTestResultAdapter implements ReporterTestResult { private readonly _testCase: PlaywrightTestCase; private readonly _testResult: PlaywrightTestResult; private _attempt: number; diff --git a/lib/test-adapter/reporter.ts b/lib/adapters/test-result/reporter.ts similarity index 95% rename from lib/test-adapter/reporter.ts rename to lib/adapters/test-result/reporter.ts index bcd0ac593..cdf8ed81b 100644 --- a/lib/test-adapter/reporter.ts +++ b/lib/adapters/test-result/reporter.ts @@ -1,9 +1,9 @@ -import {TestStatus} from '../constants'; -import {TestError, ErrorDetails, ImageInfoFull, ImageBase64, ImageFile} from '../types'; +import {TestStatus} from '../../constants'; +import {TestError, ErrorDetails, ImageInfoFull, ImageBase64, ImageFile} from '../../types'; import {ReporterTestResult} from './index'; import _ from 'lodash'; import {extractErrorDetails} from './utils'; -import {getShortMD5, getTestHash} from '../common-utils'; +import {getShortMD5, getTestHash} from '../../common-utils'; // This class is primarily useful when cloning ReporterTestResult. // It allows to override some properties while keeping computable diff --git a/lib/test-adapter/sqlite.ts b/lib/adapters/test-result/sqlite.ts similarity index 94% rename from lib/test-adapter/sqlite.ts rename to lib/adapters/test-result/sqlite.ts index 658ba3ee7..945d4bee1 100644 --- a/lib/test-adapter/sqlite.ts +++ b/lib/adapters/test-result/sqlite.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {DB_COLUMN_INDEXES, TestStatus} from '../constants'; +import {DB_COLUMN_INDEXES, TestStatus} from '../../constants'; import { AssertViewResult, TestError, @@ -8,10 +8,10 @@ import { ImageBase64, ImageFile, RawSuitesRow -} from '../types'; +} from '../../types'; import {ReporterTestResult} from './index'; import {Writable} from 'type-fest'; -import {getTestHash} from '../common-utils'; +import {getTestHash} from '../../common-utils'; const tryParseJson = (json: string): unknown | undefined => { try { @@ -21,16 +21,16 @@ const tryParseJson = (json: string): unknown | undefined => { } }; -interface SqliteTestAdapterOptions { +interface SqliteTestResultAdapterOptions { titleDelimiter: string; } -export class SqliteTestAdapter implements ReporterTestResult { +export class SqliteTestResultAdapter implements ReporterTestResult { private _testResult: RawSuitesRow; private _parsedTestResult: Writable>; private _titleDelimiter: string; - constructor(testResult: RawSuitesRow, attempt: number, options: SqliteTestAdapterOptions) { + constructor(testResult: RawSuitesRow, attempt: number, options: SqliteTestResultAdapterOptions) { this._testResult = testResult; this._parsedTestResult = {attempt}; this._titleDelimiter = options.titleDelimiter; diff --git a/lib/test-adapter/testplane.ts b/lib/adapters/test-result/testplane.ts similarity index 92% rename from lib/test-adapter/testplane.ts rename to lib/adapters/test-result/testplane.ts index 884b742a6..b9d898b01 100644 --- a/lib/test-adapter/testplane.ts +++ b/lib/adapters/test-result/testplane.ts @@ -4,9 +4,9 @@ import type Testplane from 'testplane'; import type {Test as TestplaneTest} from 'testplane'; import {ValueOf} from 'type-fest'; -import {getCommandsHistory} from '../history-utils'; -import {ERROR, FAIL, SUCCESS, TestStatus, UNKNOWN_SESSION_ID, UPDATED} from '../constants'; -import {getError, hasFailedImages, isImageDiffError, isNoRefImageError, wrapLinkByTag} from '../common-utils'; +import {getCommandsHistory} from '../../history-utils'; +import {ERROR, FAIL, SUCCESS, TestStatus, UNKNOWN_SESSION_ID, UPDATED} from '../../constants'; +import {getError, hasFailedImages, isImageDiffError, isNoRefImageError, wrapLinkByTag} from '../../common-utils'; import { ErrorDetails, TestplaneSuite, @@ -21,9 +21,9 @@ import { ImageInfoSuccess, ImageInfoUpdated, TestError -} from '../types'; +} from '../../types'; import {ReporterTestResult} from './index'; -import {getSuitePath} from '../plugin-utils'; +import {getSuitePath} from '../../plugin-utils'; import {extractErrorDetails} from './utils'; export const getStatus = (eventName: ValueOf, events: Testplane['events'], testResult: TestplaneTestResult): TestStatus => { @@ -47,23 +47,23 @@ const wrapSkipComment = (skipComment: string | null | undefined): string => { return skipComment ? wrapLinkByTag(skipComment) : 'Unknown reason'; }; -export interface TestplaneTestAdapterOptions { +export interface TestplaneTestResultAdapterOptions { attempt: number; status: TestStatus; } -export class TestplaneTestAdapter implements ReporterTestResult { +export class TestplaneTestResultAdapter implements ReporterTestResult { private _testResult: TestplaneTest | TestplaneTestResult; private _errorDetails: ErrorDetails | null; private _timestamp: number | undefined; private _attempt: number; private _status: TestStatus; - static create(this: new (testResult: TestplaneTestResult, options: TestplaneTestAdapterOptions) => T, testResult: TestplaneTestResult, options: TestplaneTestAdapterOptions): T { + static create(this: new (testResult: TestplaneTestResult, options: TestplaneTestResultAdapterOptions) => T, testResult: TestplaneTestResult, options: TestplaneTestResultAdapterOptions): T { return new this(testResult, options); } - constructor(testResult: TestplaneTest | TestplaneTestResult, {attempt, status}: TestplaneTestAdapterOptions) { + constructor(testResult: TestplaneTest | TestplaneTestResult, {attempt, status}: TestplaneTestResultAdapterOptions) { this._testResult = testResult; this._errorDetails = null; this._timestamp = (this._testResult as TestplaneTestResult).timestamp ?? diff --git a/lib/test-adapter/transformers/db.ts b/lib/adapters/test-result/transformers/db.ts similarity index 90% rename from lib/test-adapter/transformers/db.ts rename to lib/adapters/test-result/transformers/db.ts index 8ae83ca1c..635d2def6 100644 --- a/lib/test-adapter/transformers/db.ts +++ b/lib/adapters/test-result/transformers/db.ts @@ -1,6 +1,6 @@ import {ReporterTestResult} from '../index'; -import {DbTestResult} from '../../sqlite-client'; -import {getError, getRelativeUrl, getUrlWithBase} from '../../common-utils'; +import {DbTestResult} from '../../../sqlite-client'; +import {getError, getRelativeUrl, getUrlWithBase} from '../../../common-utils'; import _ from 'lodash'; interface Options { diff --git a/lib/test-adapter/transformers/tree.ts b/lib/adapters/test-result/transformers/tree.ts similarity index 91% rename from lib/test-adapter/transformers/tree.ts rename to lib/adapters/test-result/transformers/tree.ts index f407243d9..e54f001d2 100644 --- a/lib/test-adapter/transformers/tree.ts +++ b/lib/adapters/test-result/transformers/tree.ts @@ -1,6 +1,6 @@ import {ReporterTestResult} from '../index'; import _ from 'lodash'; -import {BaseTreeTestResult} from '../../tests-tree-builder/base'; +import {BaseTreeTestResult} from '../../../tests-tree-builder/base'; import {DbTestResultTransformer} from './db'; import {extractErrorDetails} from '../utils'; diff --git a/lib/test-adapter/utils/index.ts b/lib/adapters/test-result/utils/index.ts similarity index 93% rename from lib/test-adapter/utils/index.ts rename to lib/adapters/test-result/utils/index.ts index 2ccd1476d..8998bef70 100644 --- a/lib/test-adapter/utils/index.ts +++ b/lib/adapters/test-result/utils/index.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; import {ReporterTestResult} from '../index'; import {TupleToUnion} from 'type-fest'; -import {ErrorDetails, ImageInfoDiff, ImageInfoFull} from '../../types'; -import {ERROR_DETAILS_PATH} from '../../constants'; +import {ErrorDetails, ImageInfoDiff, ImageInfoFull} from '../../../types'; +import {ERROR_DETAILS_PATH} from '../../../constants'; import {ReporterTestAdapter} from '../reporter'; -import {getDetailsFileName, isImageBufferData} from '../../common-utils'; +import {getDetailsFileName, isImageBufferData} from '../../../common-utils'; export const copyAndUpdate = ( original: ReporterTestResult, diff --git a/lib/adapters/tool/index.ts b/lib/adapters/tool/index.ts new file mode 100644 index 000000000..ff276f409 --- /dev/null +++ b/lib/adapters/tool/index.ts @@ -0,0 +1,48 @@ +import type {Config, TestCollection} from 'testplane'; +import type {CommanderStatic} from '@gemini-testing/commander'; + +import {GuiApi} from '../../gui/api'; +import {EventSource} from '../../gui/event-source'; +import {GuiReportBuilder} from '../../report-builder/gui'; +import {ToolName} from '../../constants'; + +import type {ReporterConfig, ImageFile} from '../../types'; +import type {TestSpec} from './types'; + +export interface ToolAdapterOptionsFromCli { + toolName: ToolName; + configPath?: string; +} + +export interface UpdateReferenceOpts { + refImg: ImageFile; + state: string; +} + +export interface ToolAdapter { + readonly toolName: ToolName; + readonly config: Config; + readonly reporterConfig: ReporterConfig; + readonly guiApi?: GuiApi; + + initGuiApi(): void; + readTests(paths: string[], cliTool: CommanderStatic): Promise; + run(testCollection: TestCollection, tests: TestSpec[], cliTool: CommanderStatic): Promise; + + updateReference(opts: UpdateReferenceOpts): void; + handleTestResults(reportBuilder: GuiReportBuilder, eventSource: EventSource): void; + + halt(err: Error, timeout: number): void; +} + +export const makeToolAdapter = async (opts: ToolAdapterOptionsFromCli): Promise => { + if (opts.toolName === ToolName.Testplane) { + const {TestplaneToolAdapter} = await import('./testplane'); + + return TestplaneToolAdapter.create(opts); + } else if (opts.toolName === ToolName.Playwright) { + throw new Error('Playwright is not supported yet'); + } else { + throw new Error(`Tool adapter with name: "${opts.toolName}" is not supported`); + } +}; diff --git a/lib/adapters/tool/testplane/index.ts b/lib/adapters/tool/testplane/index.ts new file mode 100644 index 000000000..3fdc5e1d1 --- /dev/null +++ b/lib/adapters/tool/testplane/index.ts @@ -0,0 +1,171 @@ +import _ from 'lodash'; +import Testplane, {type Config, type TestCollection} from 'testplane'; +import type {CommanderStatic} from '@gemini-testing/commander'; + +import {GuiApi} from '../../../gui/api'; +import {parseConfig} from '../../../config'; +import {HtmlReporter} from '../../../plugin-api'; +import {ApiFacade} from '../../../gui/api/facade'; +import {createTestRunner} from './runner'; +import {EventSource} from '../../../gui/event-source'; +import {GuiReportBuilder} from '../../../report-builder/gui'; +import {handleTestResults} from './test-results-handler'; +import {ToolName} from '../../../constants'; + +import type {ToolAdapter, ToolAdapterOptionsFromCli, UpdateReferenceOpts} from '../index'; +import type {TestSpec, CustomGuiActionPayload} from '../types'; +import type {ReporterConfig, CustomGuiItem} from '../../../types'; + +type HtmlReporterApi = { + gui: ApiFacade; + htmlReporter: HtmlReporter; +}; +type TestplaneWithHtmlReporter = Testplane & HtmlReporterApi; + +interface ReplModeOption { + enabled: boolean; + beforeTest: boolean; + onFail: boolean; +} + +interface OptionsFromPlugin { + toolName: ToolName.Testplane; + tool: Testplane; + reporterConfig: ReporterConfig; +} + +type Options = ToolAdapterOptionsFromCli | OptionsFromPlugin; + +export class TestplaneToolAdapter implements ToolAdapter { + private _toolName: ToolName; + private _tool: TestplaneWithHtmlReporter; + private _reporterConfig: ReporterConfig; + private _htmlReporter: HtmlReporter; + private _guiApi?: GuiApi; + + static create( + this: new (options: Options) => TestplaneToolAdapter, + options: Options + ): TestplaneToolAdapter { + return new this(options); + } + + constructor(opts: Options) { + if ('tool' in opts) { + this._tool = opts.tool as TestplaneWithHtmlReporter; + this._reporterConfig = opts.reporterConfig; + } else { + // in order to not use static report with gui simultaneously + process.env['html_reporter_enabled'] = false.toString(); + this._tool = Testplane.create(opts.configPath) as TestplaneWithHtmlReporter; + + const pluginOpts = getPluginOptions(this._tool.config); + this._reporterConfig = parseConfig(pluginOpts); + } + + this._toolName = opts.toolName; + this._htmlReporter = HtmlReporter.create(this._reporterConfig, {toolName: ToolName.Testplane}); + + // in order to be able to use it from other plugins as an API + this._tool.htmlReporter = this._htmlReporter; + } + + get toolName(): ToolName { + return this._toolName; + } + + get config(): Config { + return this._tool.config; + } + + get reporterConfig(): ReporterConfig { + return this._reporterConfig; + } + + get htmlReporter(): HtmlReporter { + return this._htmlReporter; + } + + get guiApi(): GuiApi | undefined { + return this._guiApi; + } + + initGuiApi(): void { + this._guiApi = GuiApi.create(); + + // in order to be able to use it from other plugins as an API + this._tool.gui = this._guiApi.gui; + } + + async readTests(paths: string[], cliTool: CommanderStatic): Promise { + const {grep, set: sets, browser: browsers} = cliTool; + const replMode = getReplModeOption(cliTool); + + return this._tool.readTests(paths, {grep, sets, browsers, replMode}); + } + + async run(testCollection: TestCollection, tests: TestSpec[] = [], cliTool: CommanderStatic): Promise { + const {grep, set: sets, browser: browsers, devtools = false} = cliTool; + const replMode = getReplModeOption(cliTool); + const runner = createTestRunner(testCollection, tests); + + return runner.run((collection) => this._tool.run(collection, {grep, sets, browsers, devtools, replMode})); + } + + updateReference(opts: UpdateReferenceOpts): void { + this._tool.emit(this._tool.events.UPDATE_REFERENCE, opts); + } + + handleTestResults(reportBuilder: GuiReportBuilder, eventSource: EventSource): void { + handleTestResults(this._tool, reportBuilder, eventSource); + } + + halt(err: Error, timeout: number): void { + this._tool.halt(err, timeout); + } + + async initGuiHandler(): Promise { + const {customGui} = this._reporterConfig; + + await Promise.all( + _(customGui) + .flatMap(_.identity) + .map((ctx) => ctx.initialize?.({testplane: this._tool, hermione: this._tool, ctx})) + .value() + ); + } + + async runCustomGuiAction(payload: CustomGuiActionPayload): Promise { + const {customGui} = this._reporterConfig; + + const {sectionName, groupIndex, controlIndex} = payload; + const ctx = customGui[sectionName][groupIndex]; + const control = ctx.controls[controlIndex]; + + await ctx.action({testplane: this._tool, hermione: this._tool, control, ctx}); + } +} + +function getPluginOptions(config: Config): Partial { + const defaultOpts = {}; + + for (const toolName of [ToolName.Testplane, 'hermione']) { + const opts = _.get(config.plugins, `html-reporter/${toolName}`, defaultOpts); + + if (!_.isEmpty(opts)) { + return opts; + } + } + + return defaultOpts; +} + +function getReplModeOption(cliTool: CommanderStatic): ReplModeOption { + const {repl = false, replBeforeTest = false, replOnFail = false} = cliTool; + + return { + enabled: repl || replBeforeTest || replOnFail, + beforeTest: replBeforeTest, + onFail: replOnFail + }; +} diff --git a/lib/gui/tool-runner/runner/all-test-runner.ts b/lib/adapters/tool/testplane/runner/all-test-runner.ts similarity index 100% rename from lib/gui/tool-runner/runner/all-test-runner.ts rename to lib/adapters/tool/testplane/runner/all-test-runner.ts diff --git a/lib/gui/tool-runner/runner/index.ts b/lib/adapters/tool/testplane/runner/index.ts similarity index 82% rename from lib/gui/tool-runner/runner/index.ts rename to lib/adapters/tool/testplane/runner/index.ts index e814200cd..ca2dbbaa1 100644 --- a/lib/gui/tool-runner/runner/index.ts +++ b/lib/adapters/tool/testplane/runner/index.ts @@ -1,9 +1,10 @@ import _ from 'lodash'; import type {TestCollection} from 'testplane'; -import {TestRunner, TestSpec} from './runner'; import {AllTestRunner} from './all-test-runner'; import {SpecificTestRunner} from './specific-test-runner'; +import type {TestRunner} from './runner'; +import type {TestSpec} from '../../types'; export const createTestRunner = (collection: TestCollection, tests: TestSpec[]): TestRunner => { return _.isEmpty(tests) diff --git a/lib/gui/tool-runner/runner/runner.ts b/lib/adapters/tool/testplane/runner/runner.ts similarity index 85% rename from lib/gui/tool-runner/runner/runner.ts rename to lib/adapters/tool/testplane/runner/runner.ts index 99ac3e704..40ee89a02 100644 --- a/lib/gui/tool-runner/runner/runner.ts +++ b/lib/adapters/tool/testplane/runner/runner.ts @@ -4,11 +4,6 @@ export interface TestRunner { run(handler: (testCollection: TestCollection) => U): U; } -export interface TestSpec { - testName: string; - browserName: string; -} - export class BaseRunner implements TestRunner { protected _collection: TestCollection; diff --git a/lib/gui/tool-runner/runner/specific-test-runner.ts b/lib/adapters/tool/testplane/runner/specific-test-runner.ts similarity index 88% rename from lib/gui/tool-runner/runner/specific-test-runner.ts rename to lib/adapters/tool/testplane/runner/specific-test-runner.ts index 5e653d15c..474822ca8 100644 --- a/lib/gui/tool-runner/runner/specific-test-runner.ts +++ b/lib/adapters/tool/testplane/runner/specific-test-runner.ts @@ -1,5 +1,6 @@ import type {TestCollection} from 'testplane'; -import {BaseRunner, TestSpec} from './runner'; +import {BaseRunner} from './runner'; +import type {TestSpec} from '../../types'; export class SpecificTestRunner extends BaseRunner { private _tests: TestSpec[]; diff --git a/lib/gui/tool-runner/report-subscriber.ts b/lib/adapters/tool/testplane/test-results-handler.ts similarity index 76% rename from lib/gui/tool-runner/report-subscriber.ts rename to lib/adapters/tool/testplane/test-results-handler.ts index 042e0ae27..24b24be35 100644 --- a/lib/gui/tool-runner/report-subscriber.ts +++ b/lib/adapters/tool/testplane/test-results-handler.ts @@ -2,17 +2,17 @@ import os from 'os'; import PQueue from 'p-queue'; import type Testplane from 'testplane'; import type {Test as TestplaneTest} from 'testplane'; -import {ClientEvents} from '../constants'; -import {getSuitePath} from '../../plugin-utils'; -import {createWorkers, CreateWorkersRunner} from '../../workers/create-workers'; -import {logError, formatTestResult} from '../../server-utils'; -import {TestStatus} from '../../constants'; -import {GuiReportBuilder} from '../../report-builder/gui'; -import {EventSource} from '../event-source'; -import {TestplaneTestResult} from '../../types'; -import {getStatus} from '../../test-adapter/testplane'; - -export const subscribeOnToolEvents = (testplane: Testplane, reportBuilder: GuiReportBuilder, client: EventSource): void => { +import {ClientEvents} from '../../../gui/constants'; +import {getSuitePath} from '../../../plugin-utils'; +import {createWorkers, CreateWorkersRunner} from '../../../workers/create-workers'; +import {logError, formatTestResult} from '../../../server-utils'; +import {TestStatus} from '../../../constants'; +import {GuiReportBuilder} from '../../../report-builder/gui'; +import {EventSource} from '../../../gui/event-source'; +import {TestplaneTestResult} from '../../../types'; +import {getStatus} from '../../test-result/testplane'; + +export const handleTestResults = (testplane: Testplane, reportBuilder: GuiReportBuilder, client: EventSource): void => { const queue = new PQueue({concurrency: os.cpus().length}); testplane.on(testplane.events.RUNNER_START, (runner) => { diff --git a/lib/adapters/tool/types.ts b/lib/adapters/tool/types.ts new file mode 100644 index 000000000..a93af7019 --- /dev/null +++ b/lib/adapters/tool/types.ts @@ -0,0 +1,10 @@ +export interface TestSpec { + testName: string; + browserName: string; +} + +export interface CustomGuiActionPayload { + sectionName: string; + groupIndex: number; + controlIndex: number; +} diff --git a/lib/cli-commands/index.ts b/lib/cli-commands/index.ts deleted file mode 100644 index f6ec574a7..000000000 --- a/lib/cli-commands/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const cliCommands = { - GUI: 'gui', - MERGE_REPORTS: 'merge-reports', - REMOVE_UNUSED_SCREENS: 'remove-unused-screens' -} as const; diff --git a/lib/cli-commands/gui.js b/lib/cli/commands/gui.js similarity index 58% rename from lib/cli-commands/gui.js rename to lib/cli/commands/gui.js index 9de0ee9db..2d1da0185 100644 --- a/lib/cli-commands/gui.js +++ b/lib/cli/commands/gui.js @@ -1,17 +1,16 @@ 'use strict'; -const {cliCommands} = require('.'); -const runGui = require('../gui').default; -const {Api} = require('../gui/api'); +const {commands} = require('..'); +const runGui = require('../../gui').default; -const {GUI: commandName} = cliCommands; +const {GUI: commandName} = commands; -module.exports = (program, pluginConfig, testplane) => { - // must be executed here because it adds `gui` field in `gemini`, `testplane` and `hermione tool`, +module.exports = (cliTool, toolAdapter) => { + // must be executed here because it adds `gui` field in tool instance, // which is available to other plugins and is an API for interacting with the current plugin - const guiApi = Api.create(testplane); + toolAdapter.initGuiApi(); - program + cliTool .command(`${commandName} [paths...]`) .allowUnknownOption() .description('update the changed screenshots or gather them if they does not exist') @@ -20,6 +19,6 @@ module.exports = (program, pluginConfig, testplane) => { .option('-a, --auto-run', 'auto run immediately') .option('-O, --no-open', 'not to open a browser window after starting the server') .action((paths, options) => { - runGui({paths, testplane, guiApi, configs: {options, program, pluginConfig}}); + runGui({paths, toolAdapter, cli: {options, tool: cliTool}}); }); }; diff --git a/lib/cli-commands/merge-reports.js b/lib/cli/commands/merge-reports.js similarity index 63% rename from lib/cli-commands/merge-reports.js rename to lib/cli/commands/merge-reports.js index 001c9949e..fea862ca4 100644 --- a/lib/cli-commands/merge-reports.js +++ b/lib/cli/commands/merge-reports.js @@ -1,23 +1,23 @@ 'use strict'; -const {cliCommands} = require('.'); -const mergeReports = require('../merge-reports'); -const {logError} = require('../server-utils'); +const {commands} = require('..'); +const mergeReports = require('../../merge-reports'); +const {logError} = require('../../server-utils'); -const {MERGE_REPORTS: commandName} = cliCommands; +const {MERGE_REPORTS: commandName} = commands; -module.exports = (program, pluginConfig, testplane) => { +module.exports = (program, toolAdapter) => { program .command(`${commandName} [paths...]`) .allowUnknownOption() .description('merge reports') - .option('-d, --destination ', 'path to directory with merged report', pluginConfig.path) + .option('-d, --destination ', 'path to directory with merged report', toolAdapter.reporterConfig.path) .option('-h, --header
', 'http header for databaseUrls.json files from source paths', collect, []) .action(async (paths, options) => { try { const {destination: destPath, header: headers} = options; - await mergeReports(pluginConfig, testplane, paths, {destPath, headers}); + await mergeReports(toolAdapter, paths, {destPath, headers}); } catch (err) { logError(err); process.exit(1); diff --git a/lib/cli-commands/remove-unused-screens/index.js b/lib/cli/commands/remove-unused-screens/index.js similarity index 87% rename from lib/cli-commands/remove-unused-screens/index.js rename to lib/cli/commands/remove-unused-screens/index.js index b73a2bc2c..22b3d0999 100644 --- a/lib/cli-commands/remove-unused-screens/index.js +++ b/lib/cli/commands/remove-unused-screens/index.js @@ -8,15 +8,15 @@ const chalk = require('chalk'); const filesize = require('filesize'); const Promise = require('bluebird'); -const {cliCommands} = require('..'); +const {commands} = require('../..'); const {getTestsFromFs, findScreens, askQuestion, identifyOutdatedScreens, identifyUnusedScreens, removeScreens} = require('./utils'); -const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../../constants/database'); -const {logger} = require('../../common-utils'); +const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../../../constants/database'); +const {logger} = require('../../../common-utils'); -const {REMOVE_UNUSED_SCREENS: commandName} = cliCommands; +const {REMOVE_UNUSED_SCREENS: commandName} = commands; // TODO: remove hack after add ability to add controllers from plugin in silent mode -function proxyTestplane() { +function proxyTool() { const proxyHandler = { get(target, prop) { return prop in target ? target[prop] : new Proxy(() => {}, this); @@ -29,7 +29,7 @@ function proxyTestplane() { global.hermione = global.testplane = new Proxy(global.testplane || global.hermione || {}, proxyHandler); } -module.exports = (program, pluginConfig, testplane) => { +module.exports = (program, toolAdapter) => { const toolName = program.name?.() || 'hermione'; program @@ -40,7 +40,7 @@ module.exports = (program, pluginConfig, testplane) => { .on('--help', () => logger.log(getHelpMessage(toolName))) .action(async (options) => { try { - proxyTestplane(); + proxyTool(); const {pattern: userPatterns} = options; @@ -52,7 +52,7 @@ module.exports = (program, pluginConfig, testplane) => { const spinner = ora({spinner: 'point'}); spinner.start(`Reading ${toolName} tests from file system`); - const fsTests = await getTestsFromFs(testplane); + const fsTests = await getTestsFromFs(toolAdapter); spinner.succeed(); logger.log(`${chalk.green(fsTests.count)} uniq tests were read in browsers: ${[...fsTests.browserIds].join(', ')}`); @@ -82,7 +82,7 @@ module.exports = (program, pluginConfig, testplane) => { }, options); if (shouldIdentifyUnused) { - await handleUnusedScreens(foundScreenPaths, fsTests, {testplane, pluginConfig, spinner, cliOpts: options}); + await handleUnusedScreens(foundScreenPaths, fsTests, {toolAdapter, spinner, cliOpts: options}); } } catch (err) { logger.error(err.stack || err); @@ -102,35 +102,36 @@ async function handleOutdatedScreens(screenPaths, screenPatterns, opts = {}) { } async function handleUnusedScreens(screenPaths, fsTests, opts = {}) { - const {testplane, pluginConfig, spinner, cliOpts} = opts; - const mainDatabaseUrls = path.resolve(pluginConfig.path, DATABASE_URLS_JSON_NAME); + const {toolAdapter, spinner, cliOpts} = opts; + const {reporterConfig} = toolAdapter; + const mainDatabaseUrls = path.resolve(reporterConfig.path, DATABASE_URLS_JSON_NAME); - const isReportPathExists = await fs.pathExists(pluginConfig.path); + const isReportPathExists = await fs.pathExists(reporterConfig.path); if (!isReportPathExists) { - throw new Error(`Can't find html-report in "${pluginConfig.path}" folder. You should run tests or download report from CI`); + throw new Error(`Can't find html-report in "${reporterConfig.path}" folder. You should run tests or download report from CI`); } spinner.start('Loading databases with the test results in order to identify unused screenshots in tests'); - const dbPaths = await testplane.htmlReporter.downloadDatabases([mainDatabaseUrls], {pluginConfig}); + const dbPaths = await toolAdapter.htmlReporter.downloadDatabases([mainDatabaseUrls], {pluginConfig: reporterConfig}); spinner.succeed(); if (_.isEmpty(dbPaths)) { throw new Error(`Databases were not loaded from "${mainDatabaseUrls}" file`); } - const mergedDbPath = path.resolve(pluginConfig.path, LOCAL_DATABASE_NAME); + const mergedDbPath = path.resolve(reporterConfig.path, LOCAL_DATABASE_NAME); const srcDbPaths = dbPaths.filter((dbPath) => dbPath !== mergedDbPath); if (!_.isEmpty(srcDbPaths)) { spinner.start('Merging databases'); - await testplane.htmlReporter.mergeDatabases(srcDbPaths, pluginConfig.path); + await toolAdapter.htmlReporter.mergeDatabases(srcDbPaths, reporterConfig.path); spinner.succeed(); logger.log(`${chalk.green(srcDbPaths.length)} databases were merged to ${chalk.green(mergedDbPath)}`); } spinner.start(`Identifying unused reference images (tests passed successfully for them, but they were not used during execution)`); - const unusedScreenPaths = identifyUnusedScreens(fsTests, {testplane, mergedDbPath}); + const unusedScreenPaths = identifyUnusedScreens(fsTests, {toolAdapter, mergedDbPath}); spinner.succeed(); await handleScreens(screenPaths, {paths: unusedScreenPaths, type: 'unused'}, {spinner, cliOpts}); diff --git a/lib/cli-commands/remove-unused-screens/utils.js b/lib/cli/commands/remove-unused-screens/utils.js similarity index 90% rename from lib/cli-commands/remove-unused-screens/utils.js rename to lib/cli/commands/remove-unused-screens/utils.js index 333c2ca95..814ae91a2 100644 --- a/lib/cli-commands/remove-unused-screens/utils.js +++ b/lib/cli/commands/remove-unused-screens/utils.js @@ -7,9 +7,9 @@ const Promise = require('bluebird'); const fs = require('fs-extra'); const inquirer = require('inquirer'); -const {SUCCESS} = require('../../constants/test-statuses'); +const {SUCCESS} = require('../../../constants/test-statuses'); -exports.getTestsFromFs = async (testplane) => { +exports.getTestsFromFs = async (toolAdapter) => { const tests = { byId: {}, screenPatterns: [], @@ -18,12 +18,12 @@ exports.getTestsFromFs = async (testplane) => { }; const uniqTests = new Set(); - const testCollection = await testplane.readTests([], {silent: true}); + const testCollection = await toolAdapter.readTests([], {silent: true}); testCollection.eachTest((test, browserId) => { const fullTitle = test.fullTitle(); const id = `${fullTitle} ${browserId}`; - const screenPattern = testplane.config.browsers[browserId].getScreenshotPath(test, '*'); + const screenPattern = toolAdapter.config.browsers[browserId].getScreenshotPath(test, '*'); tests.byId[id] = {screenPattern, ...test}; tests.browserIds.add(browserId); @@ -70,8 +70,8 @@ exports.identifyOutdatedScreens = (screenPaths, screenPatterns) => { return outdatedScreens; }; -exports.identifyUnusedScreens = (fsTests, {testplane, mergedDbPath} = {}) => { - const dbTree = testplane.htmlReporter.getTestsTreeFromDatabase(mergedDbPath); +exports.identifyUnusedScreens = (fsTests, {toolAdapter, mergedDbPath} = {}) => { + const dbTree = toolAdapter.htmlReporter.getTestsTreeFromDatabase(mergedDbPath); const screenPathsBySuccessTests = getScreenPathsBySuccessTests(dbTree); const unusedScreens = []; diff --git a/lib/cli/index.ts b/lib/cli/index.ts new file mode 100644 index 000000000..206dbff49 --- /dev/null +++ b/lib/cli/index.ts @@ -0,0 +1,48 @@ +import path from 'node:path'; +import _ from 'lodash'; +import {Command} from '@gemini-testing/commander'; +import pkg from '../../package.json'; + +import {ToolName} from '../constants'; +import {makeToolAdapter} from '../adapters/tool'; + +export const commands = { + GUI: 'gui', + MERGE_REPORTS: 'merge-reports', + REMOVE_UNUSED_SCREENS: 'remove-unused-screens' +} as const; + +export const run = async (): Promise => { + const program = new Command(pkg.name) + .version(pkg.version) + .allowUnknownOption() + .option('-c, --config ', 'path to configuration file') + .option('-t, --tool ', 'tool name which should be run', ToolName.Testplane); + + const {tool: toolName, config: configPath} = preparseOptions(program, ['config', 'tool']); + const availableToolNames = Object.values(ToolName); + + if (!availableToolNames.includes(toolName as ToolName)) { + throw new Error(`Tool with name: "${toolName}" is not supported, try to use one of these: ${availableToolNames.map(t => `"${t}"`).join(', ')}`); + } + + const toolAdapter = await makeToolAdapter({toolName: toolName as ToolName, configPath}); + + for (const commandName of _.values(commands)) { + const registerCmd = (await import(path.resolve(__dirname, './commands', commandName))).default; + + registerCmd(program, toolAdapter); + } + + program.parse(process.argv); +}; + +function preparseOptions(program: Command, options: string[]): Record { + const configFileParser = Object.create(program) as Command; + configFileParser.options = [].concat(program.options); + configFileParser.option('-h, --help'); + + configFileParser.parse(process.argv); + + return options.reduce((acc, val) => _.set(acc, val, configFileParser[val]), {}); +} diff --git a/lib/common-utils.ts b/lib/common-utils.ts index d3ca70a94..9302bf007 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -27,7 +27,7 @@ import { TestError } from './types'; import {ErrorName, ImageDiffError, NoRefImageError} from './errors'; -import {ReporterTestResult} from './test-adapter'; +import type {ReporterTestResult} from './adapters/test-result'; export const getShortMD5 = (str: string): string => { return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7); diff --git a/lib/db-utils/server.ts b/lib/db-utils/server.ts index c2d8623d0..bd3e60e8c 100644 --- a/lib/db-utils/server.ts +++ b/lib/db-utils/server.ts @@ -13,7 +13,7 @@ import {DATABASE_URLS_JSON_NAME, DB_COLUMNS, LOCAL_DATABASE_NAME, TestStatus, To import {DbLoadResult, HandleDatabasesOptions} from './common'; import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types'; import {Tree} from '../tests-tree-builder/base'; -import {ReporterTestResult} from '../test-adapter'; +import {ReporterTestResult} from '../adapters/test-result'; import {SqliteClient} from '../sqlite-client'; export * from './common'; diff --git a/lib/gui/api/index.ts b/lib/gui/api/index.ts index 212ca3182..d2addf60c 100644 --- a/lib/gui/api/index.ts +++ b/lib/gui/api/index.ts @@ -1,22 +1,19 @@ import {ApiFacade} from './facade'; -import type Testplane from 'testplane'; import {Express} from 'express'; export interface ServerReadyData { url: string; } -type TestplaneWithGui = Testplane & { gui: ApiFacade }; - -export class Api { +export class GuiApi { private _gui: ApiFacade; - static create(this: new (testplane: TestplaneWithGui) => T, testplane: TestplaneWithGui): T { - return new this(testplane); + static create(this: new () => T): T { + return new this(); } - constructor(testplane: TestplaneWithGui) { - this._gui = testplane.gui = ApiFacade.create(); + constructor() { + this._gui = ApiFacade.create(); } async initServer(server: Express): Promise { @@ -26,4 +23,8 @@ export class Api { async serverReady(data: ServerReadyData): Promise { await this._gui.emitAsync(this._gui.events.SERVER_READY, data); } + + get gui(): ApiFacade { + return this._gui; + } } diff --git a/lib/gui/app.ts b/lib/gui/app.ts index 4fd06877b..6c8f5d71f 100644 --- a/lib/gui/app.ts +++ b/lib/gui/app.ts @@ -2,28 +2,25 @@ import {Response} from 'express'; import _ from 'lodash'; import {ToolRunner, ToolRunnerTree, UndoAcceptImagesResult} from './tool-runner'; -import {HtmlReporterApi} from '../types'; -import type Testplane from 'testplane'; -import type {Config} from 'testplane'; -import {GuiConfigs} from './index'; -import {TestSpec} from './tool-runner/runner/runner'; import {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../tests-tree-builder/gui'; -type BrowserConfig = ReturnType; +import type {Config} from 'testplane'; +import type {ServerArgs} from './index'; +import type {TestSpec} from '../adapters/tool/types'; -type AppArgs = [paths: string[], testplane: Testplane & HtmlReporterApi, configs: GuiConfigs]; +type BrowserConfig = ReturnType; export class App { private _toolRunner: ToolRunner; private _browserConfigs: BrowserConfig[]; private _retryCache: Record; - static create(this: new (...args: AppArgs) => T, ...args: AppArgs): T { - return new this(...args); + static create(this: new (args: ServerArgs) => T, args: ServerArgs): T { + return new this(args); } - constructor(...[paths, testplane, configs]: AppArgs) { - this._toolRunner = ToolRunner.create(paths, testplane, configs); + constructor(args: ServerArgs) { + this._toolRunner = ToolRunner.create(args); this._browserConfigs = []; this._retryCache = {}; diff --git a/lib/gui/index.ts b/lib/gui/index.ts index e6e7ecb1c..de4c27bee 100644 --- a/lib/gui/index.ts +++ b/lib/gui/index.ts @@ -5,9 +5,8 @@ import opener from 'opener'; import * as server from './server'; import {logger} from '../common-utils'; import * as utils from '../server-utils'; -import {HtmlReporterApi, ReporterConfig} from '../types'; -import type Testplane from 'testplane'; -import {Api} from './api'; + +import type {TestplaneToolAdapter} from '../adapters/tool/testplane'; const {logError} = utils; @@ -18,24 +17,20 @@ export interface GuiCliOptions { hostname: string; } -export interface GuiConfigs { - options: GuiCliOptions; - program: CommanderStatic; - pluginConfig: ReporterConfig; -} - export interface ServerArgs { paths: string[]; - testplane: Testplane & HtmlReporterApi; - guiApi: Api; - configs: GuiConfigs; + toolAdapter: TestplaneToolAdapter; + cli: { + options: GuiCliOptions; + tool: CommanderStatic; + } } export default (args: ServerArgs): void => { server.start(args) .then(({url}: { url: string }) => { logger.log(`GUI is running at ${chalk.cyan(url)}`); - args.configs.options.open && opener(url); + args.cli.options.open && opener(url); }) .catch((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any logError(err); diff --git a/lib/gui/server.ts b/lib/gui/server.ts index 38a047fc4..996c0a53e 100644 --- a/lib/gui/server.ts +++ b/lib/gui/server.ts @@ -7,15 +7,21 @@ import {INTERNAL_SERVER_ERROR, OK} from 'http-codes'; import {App} from './app'; import {MAX_REQUEST_SIZE, KEEP_ALIVE_TIMEOUT, HEADERS_TIMEOUT} from './constants'; -import {initializeCustomGui, runCustomGuiAction} from '../server-utils'; import {logger} from '../common-utils'; import {initPluginsRoutes} from './routes/plugins'; import {ServerArgs} from './index'; import {ServerReadyData} from './api'; +import {ToolName} from '../constants'; -export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Promise => { - const {options, pluginConfig} = configs; - const app = App.create(paths, testplane, configs); +export const start = async (args: ServerArgs): Promise => { + const {toolAdapter} = args; + const {reporterConfig, guiApi} = toolAdapter; + + if (!guiApi) { + throw new Error('Gui API must be initialized before starting gui server'); + } + + const app = App.create(args); const server = express(); server.use(bodyParser.json({limit: MAX_REQUEST_SIZE})); @@ -23,10 +29,10 @@ export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Pr await guiApi.initServer(server); // allow plugins to precede default server routes - server.use(initPluginsRoutes(express.Router(), pluginConfig)); + server.use(initPluginsRoutes(express.Router(), reporterConfig)); server.use(express.static(path.join(__dirname, '../static'), {index: 'gui.html'})); - server.use(express.static(path.join(process.cwd(), pluginConfig.path))); + server.use(express.static(path.join(process.cwd(), reporterConfig.path))); server.get('/', (_req, res) => res.sendFile(path.join(__dirname, '../static', 'gui.html'))); @@ -42,7 +48,10 @@ export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Pr server.get('/init', async (_req, res) => { try { - await initializeCustomGui(testplane, pluginConfig); + if (toolAdapter.toolName === ToolName.Testplane) { + await toolAdapter.initGuiHandler(); + } + res.json(app.data); } catch (e: unknown) { const error = e as Error; @@ -73,7 +82,10 @@ export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Pr server.post('/run-custom-gui-action', async ({body: payload}, res) => { try { - await runCustomGuiAction(testplane, pluginConfig, payload); + if (toolAdapter.toolName === ToolName.Testplane) { + await toolAdapter.runCustomGuiAction(payload); + } + res.sendStatus(OK); } catch (e) { res.status(INTERNAL_SERVER_ERROR).send(`Error while running custom gui action: ${(e as Error).message}`); @@ -127,7 +139,7 @@ export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Pr server.post('/stop', (_req, res) => { try { // pass 0 to prevent terminating testplane process - testplane.halt(new Error('Tests were stopped by the user'), 0); + toolAdapter.halt(new Error('Tests were stopped by the user'), 0); res.sendStatus(OK); } catch (e) { res.status(INTERNAL_SERVER_ERROR).send(`Error while stopping tests: ${(e as Error).message}`); @@ -136,7 +148,7 @@ export const start = async ({paths, testplane, guiApi, configs}: ServerArgs): Pr await app.initialize(); - const {port, hostname} = options; + const {port, hostname} = args.cli.options; await BluebirdPromise.fromCallback((callback) => { const httpServer = server.listen(port, hostname, callback as () => void); httpServer.keepAliveTimeout = KEEP_ALIVE_TIMEOUT; diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index c8d1e9518..b21d002ce 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -1,19 +1,27 @@ -import path from 'path'; +import path from 'node:path'; +import os from 'node:os'; import {CommanderStatic} from '@gemini-testing/commander'; import chalk from 'chalk'; import fs from 'fs-extra'; -import type Testplane from 'testplane'; import type {TestCollection, Test as TestplaneTest, Config as TestplaneConfig} from 'testplane'; import _ from 'lodash'; import looksSame, {CoordBounds} from 'looks-same'; +import PQueue from 'p-queue'; +import type {Response} from 'express'; -import {createTestRunner} from './runner'; -import {subscribeOnToolEvents} from './report-subscriber'; import {GuiReportBuilder, GuiReportBuilderResult} from '../../report-builder/gui'; import {EventSource} from '../event-source'; -import {logger, getShortMD5, isUpdatedStatus} from '../../common-utils'; +import {TestplaneToolAdapter} from '../../adapters/tool/testplane'; +import {SqliteClient} from '../../sqlite-client'; +import {Cache} from '../../cache'; +import {ImagesInfoSaver} from '../../images-info-saver'; +import {SqliteImageStore} from '../../image-store'; import * as reporterHelper from '../../reporter-helpers'; +import {logger, getShortMD5, isUpdatedStatus} from '../../common-utils'; +import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils'; +import {formatTestResult, getExpectedCacheKey} from '../../server-utils'; +import {getTestsTreeFromDatabase} from '../../db-utils/server'; import { UPDATED, SKIPPED, @@ -23,36 +31,19 @@ import { LOCAL_DATABASE_NAME, PluginEvents } from '../../constants'; -import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils'; -import {getTestsTreeFromDatabase} from '../../db-utils/server'; -import {formatTestResult, getExpectedCacheKey} from '../../server-utils'; -import { + +import type {GuiCliOptions, ServerArgs} from '../index'; +import type {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui'; +import type {ReporterTestResult} from '../../adapters/test-result'; +import type {Tree, TreeImage} from '../../tests-tree-builder/base'; +import type {TestSpec} from '../../adapters/tool/types'; +import type { AssertViewResult, TestplaneTestResult, - HtmlReporterApi, ImageFile, ImageInfoDiff, ImageInfoUpdated, ImageInfoWithState, - ReporterConfig, TestSpecByPath + ReporterConfig, TestSpecByPath, RefImageFile } from '../../types'; -import {GuiCliOptions, GuiConfigs} from '../index'; -import {Tree, TreeImage} from '../../tests-tree-builder/base'; -import {TestSpec} from './runner/runner'; -import {Response} from 'express'; -import {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui'; -import {ReporterTestResult} from '../../test-adapter'; -import {SqliteClient} from '../../sqlite-client'; -import PQueue from 'p-queue'; -import os from 'os'; -import {Cache} from '../../cache'; -import {ImagesInfoSaver} from '../../images-info-saver'; -import {SqliteImageStore} from '../../image-store'; - -type ToolRunnerArgs = [paths: string[], testplane: Testplane & HtmlReporterApi, configs: GuiConfigs]; -type ReplModeOption = { - enabled: boolean; - beforeTest: boolean; - onFail: boolean; -} export type ToolRunnerTree = GuiReportBuilderResult & Pick; @@ -63,32 +54,33 @@ export interface UndoAcceptImagesResult { export class ToolRunner { private _testFiles: string[]; - private _testplane: Testplane & HtmlReporterApi; + private _toolAdapter: TestplaneToolAdapter; private _tree: ToolRunnerTree | null; protected _collection: TestCollection | null; private _globalOpts: CommanderStatic; private _guiOpts: GuiCliOptions; private _reportPath: string; - private _pluginConfig: ReporterConfig; + private _reporterConfig: ReporterConfig; private _eventSource: EventSource; protected _reportBuilder: GuiReportBuilder | null; private _tests: Record; private _expectedImagesCache: Cache<[TestSpecByPath, string | undefined], string>; - static create(this: new (...args: ToolRunnerArgs) => T, ...args: ToolRunnerArgs): T { - return new this(...args); + static create(this: new (args: ServerArgs) => T, args: ServerArgs): T { + return new this(args); } - constructor(...[paths, testplane, {program: globalOpts, pluginConfig, options: guiOpts}]: ToolRunnerArgs) { + constructor({paths, toolAdapter, cli}: ServerArgs) { this._testFiles = ([] as string[]).concat(paths); - this._testplane = testplane; + this._toolAdapter = toolAdapter; this._tree = null; this._collection = null; - this._globalOpts = globalOpts; - this._guiOpts = guiOpts; - this._reportPath = pluginConfig.path; - this._pluginConfig = pluginConfig; + this._globalOpts = cli.tool; + this._guiOpts = cli.options; + + this._reporterConfig = this._toolAdapter.reporterConfig; + this._reportPath = this._reporterConfig.path; this._eventSource = new EventSource(); this._reportBuilder = null; @@ -99,7 +91,7 @@ export class ToolRunner { } get config(): TestplaneConfig { - return this._testplane.config; + return this._toolAdapter.config; } get tree(): ToolRunnerTree | null { @@ -109,43 +101,30 @@ export class ToolRunner { async initialize(): Promise { await mergeDatabasesForReuse(this._reportPath); - const dbClient = await SqliteClient.create({htmlReporter: this._testplane.htmlReporter, reportPath: this._reportPath, reuse: true}); + const dbClient = await SqliteClient.create({htmlReporter: this._toolAdapter.htmlReporter, reportPath: this._reportPath, reuse: true}); const imageStore = new SqliteImageStore(dbClient); const imagesInfoSaver = new ImagesInfoSaver({ - imageFileSaver: this._testplane.htmlReporter.imagesSaver, + imageFileSaver: this._toolAdapter.htmlReporter.imagesSaver, expectedPathsCache: this._expectedImagesCache, imageStore, - reportPath: this._testplane.htmlReporter.config.path + reportPath: this._toolAdapter.htmlReporter.config.path }); - this._reportBuilder = GuiReportBuilder.create(this._testplane.htmlReporter, this._pluginConfig, {dbClient, imagesInfoSaver}); - this._subscribeOnEvents(); + this._reportBuilder = GuiReportBuilder.create(this._toolAdapter.htmlReporter, this._reporterConfig, {dbClient, imagesInfoSaver}); + this._toolAdapter.handleTestResults(this._reportBuilder, this._eventSource); this._collection = await this._readTests(); - this._testplane.htmlReporter.emit(PluginEvents.DATABASE_CREATED, dbClient.getRawConnection()); + this._toolAdapter.htmlReporter.emit(PluginEvents.DATABASE_CREATED, dbClient.getRawConnection()); await this._reportBuilder.saveStaticFiles(); - this._reportBuilder.setApiValues(this._testplane.htmlReporter.values); + this._reportBuilder.setApiValues(this._toolAdapter.htmlReporter.values); await this._handleRunnableCollection(); } async _readTests(): Promise { - const {grep, set: sets, browser: browsers} = this._globalOpts; - const replMode = this._getReplModeOption(); - - return this._testplane.readTests(this._testFiles, {grep, sets, browsers, replMode}); - } - - _getReplModeOption(): ReplModeOption { - const {repl = false, replBeforeTest = false, replOnFail = false} = this._globalOpts; - - return { - enabled: repl || replBeforeTest || replOnFail, - beforeTest: replBeforeTest, - onFail: replOnFail - }; + return this._toolAdapter.readTests(this._testFiles, this._globalOpts); } protected _ensureReportBuilder(): GuiReportBuilder { @@ -194,8 +173,12 @@ export class ToolRunner { return Promise.all(tests.map(async (test): Promise => { const updateResult = this._createTestplaneTestResult(test); - const currentResult = formatTestResult(updateResult, UPDATED, test.attempt); - const estimatedStatus = reportBuilder.getUpdatedReferenceTestStatus(currentResult); + const latestAttempt = reportBuilder.getLatestAttempt({ + fullName: updateResult.fullTitle(), + browserId: updateResult.browserId + }); + const latestResult = formatTestResult(updateResult, UPDATED, latestAttempt); + const estimatedStatus = reportBuilder.getUpdatedReferenceTestStatus(latestResult); const formattedResultWithoutAttempt = formatTestResult(updateResult, UPDATED); const formattedResult = reportBuilder.provideAttempt(formattedResultWithoutAttempt); @@ -300,11 +283,9 @@ export class ToolRunner { } async run(tests: TestSpec[] = []): Promise { - const {grep, set: sets, browser: browsers, devtools = false} = this._globalOpts; - const replMode = this._getReplModeOption(); + const testCollection = this._ensureTestCollection(); - return createTestRunner(this._ensureTestCollection(), tests) - .run((collection) => this._testplane.run(collection, {grep, sets, browsers, devtools, replMode})); + return this._toolAdapter.run(testCollection, tests, this._globalOpts); } protected async _handleRunnableCollection(): Promise { @@ -332,11 +313,7 @@ export class ToolRunner { } protected _isSilentlySkipped({silentSkip, parent}: TestplaneTest): boolean { - return silentSkip || parent && this._isSilentlySkipped(parent); - } - - protected _subscribeOnEvents(): void { - subscribeOnToolEvents(this._testplane, this._ensureReportBuilder(), this._eventSource); + return Boolean(silentSkip || parent && this._isSilentlySkipped(parent)); } protected _createTestplaneTestResult(updateData: TestRefUpdateData): TestplaneTestResult { @@ -351,8 +328,9 @@ export class ToolRunner { .filter(({stateName, actualImg}) => Boolean(stateName) && Boolean(actualImg)) .forEach((imageInfo) => { const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageFile}; - const path = this._testplane.config.browsers[browserId].getScreenshotPath(testplaneTest, stateName); - const refImg = {path, size: actualImg.size}; + const absoluteRefImgPath = this._toolAdapter.config.browsers[browserId].getScreenshotPath(testplaneTest, stateName); + const relativeRefImgPath = absoluteRefImgPath && path.relative(process.cwd(), absoluteRefImgPath); + const refImg: RefImageFile = {path: absoluteRefImgPath, relativePath: relativeRefImgPath, size: actualImg.size}; assertViewResults.push({stateName, refImg, currImg: actualImg, isUpdated: isUpdatedStatus(imageInfo.status)}); }); @@ -374,10 +352,7 @@ export class ToolRunner { protected _handleReferenceUpdate(testResult: ReporterTestResult, imageInfo: ImageInfoUpdated, state: string): void { this._expectedImagesCache.set([testResult, imageInfo.stateName], imageInfo.expectedImg.path); - this._testplane.emit( - this._testplane.events.UPDATE_REFERENCE, - {refImg: imageInfo.refImg, state} - ); + this._toolAdapter.updateReference({refImg: imageInfo.refImg, state}); } async _fillTestsTree(): Promise { @@ -397,7 +372,7 @@ export class ToolRunner { const dbPath = path.resolve(this._reportPath, LOCAL_DATABASE_NAME); if (await fs.pathExists(dbPath)) { - return getTestsTreeFromDatabase(ToolName.Testplane, dbPath, this._pluginConfig.baseHost); + return getTestsTreeFromDatabase(ToolName.Testplane, dbPath, this._reporterConfig.baseHost); } logger.warn(chalk.yellow(`Nothing to reuse in ${this._reportPath}: can not load data from ${DATABASE_URLS_JSON_NAME}`)); @@ -406,6 +381,6 @@ export class ToolRunner { } protected _resolveImgPath(imgPath: string): string { - return path.resolve(process.cwd(), this._pluginConfig.path, imgPath); + return path.resolve(process.cwd(), this._reporterConfig.path, imgPath); } } diff --git a/lib/images-info-saver.ts b/lib/images-info-saver.ts index ee77b203a..839cef296 100644 --- a/lib/images-info-saver.ts +++ b/lib/images-info-saver.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import PQueue from 'p-queue'; import {RegisterWorkers} from './workers/create-workers'; -import {ReporterTestResult} from './test-adapter'; +import {ReporterTestResult} from './adapters/test-result'; import { DiffOptions, ImageBase64, ImageBuffer, ImageFile, @@ -16,7 +16,7 @@ import { ImageInfoFull, ImageSize, TestSpecByPath } from './types'; -import {copyAndUpdate, removeBufferFromImagesInfo} from './test-adapter/utils'; +import {copyAndUpdate, removeBufferFromImagesInfo} from './adapters/test-result/utils'; import {cacheDiffImages} from './image-cache'; import {NEW_ISSUE_LINK, PluginEvents, TestStatus, UPDATED} from './constants'; import {createHash, getCurrentPath, getDiffPath, getReferencePath, getTempPath, makeDirFor} from './server-utils'; diff --git a/lib/merge-reports/index.js b/lib/merge-reports/index.js index 78d64c881..40b8c8be2 100644 --- a/lib/merge-reports/index.js +++ b/lib/merge-reports/index.js @@ -5,10 +5,11 @@ const _ = require('lodash'); const serverUtils = require('../server-utils'); const dbServerUtils = require('../db-utils/server'); -module.exports = async (pluginConfig, testplane, srcPaths, {destPath, headers}) => { +module.exports = async (toolAdapter, srcPaths, {destPath, headers}) => { validateOpts({srcPaths, destPath, headers}); let headersFromEnv; + const {htmlReporter, reporterConfig} = toolAdapter; try { headersFromEnv = JSON.parse(process.env.html_reporter_headers || '{}'); @@ -25,11 +26,11 @@ module.exports = async (pluginConfig, testplane, srcPaths, {destPath, headers}) const resolvedUrls = await tryResolveUrls(srcPaths, parsedHeaders); await Promise.all([ - serverUtils.saveStaticFilesToReportDir(testplane.htmlReporter, pluginConfig, destPath), + serverUtils.saveStaticFilesToReportDir(htmlReporter, reporterConfig, destPath), serverUtils.writeDatabaseUrlsFile(destPath, resolvedUrls) ]); - await testplane.htmlReporter.emitAsync(testplane.htmlReporter.events.REPORT_SAVED, {reportPath: destPath}); + await htmlReporter.emitAsync(htmlReporter.events.REPORT_SAVED, {reportPath: destPath}); }; function validateOpts({srcPaths, destPath, headers}) { diff --git a/lib/report-builder/gui.ts b/lib/report-builder/gui.ts index 9b2efe4a3..91e6ac19e 100644 --- a/lib/report-builder/gui.ts +++ b/lib/report-builder/gui.ts @@ -3,13 +3,13 @@ import {StaticReportBuilder, StaticReportBuilderOptions} from './static'; import {GuiTestsTreeBuilder, TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../tests-tree-builder/gui'; import {UPDATED, DB_COLUMNS, ToolName, TestStatus, TESTPLANE_TITLE_DELIMITER, SKIPPED, SUCCESS} from '../constants'; import {ConfigForStaticFile, getConfigForStaticFile} from '../server-utils'; -import {ReporterTestResult} from '../test-adapter'; +import {ReporterTestResult} from '../adapters/test-result'; import {Tree, TreeImage} from '../tests-tree-builder/base'; import {ImageInfoFull, ImageInfoWithState, ReporterConfig} from '../types'; import {determineStatus, isUpdatedStatus} from '../common-utils'; import {HtmlReporter, HtmlReporterValues} from '../plugin-api'; import {SkipItem} from '../tests-tree-builder/static'; -import {copyAndUpdate} from '../test-adapter/utils'; +import {copyAndUpdate} from '../adapters/test-result/utils'; interface UndoAcceptImageResult { updatedImage: TreeImage | undefined; diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts index 37f14f299..141d82cf1 100644 --- a/lib/report-builder/static.ts +++ b/lib/report-builder/static.ts @@ -11,13 +11,13 @@ import { PluginEvents, UNKNOWN_ATTEMPT, UPDATED } from '../constants'; import type {SqliteClient} from '../sqlite-client'; -import {ReporterTestResult} from '../test-adapter'; +import {ReporterTestResult} from '../adapters/test-result'; import {saveErrorDetails, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils'; import {ReporterConfig} from '../types'; import {HtmlReporter} from '../plugin-api'; import {getTestFromDb} from '../db-utils/server'; import {TestAttemptManager} from '../test-attempt-manager'; -import {copyAndUpdate} from '../test-adapter/utils'; +import {copyAndUpdate} from '../adapters/test-result/utils'; import {RegisterWorkers} from '../workers/create-workers'; import {ImagesInfoSaver} from '../images-info-saver'; @@ -75,6 +75,10 @@ export class StaticReportBuilder { this._workers = workers; } + getLatestAttempt(testInfo: {fullName: string, browserId: string}): number { + return this._testAttemptManager.getCurrentAttempt(testInfo); + } + /** If passed test result doesn't have attempt, this method registers new attempt and sets attempt number */ provideAttempt(testResultOriginal: ReporterTestResult): ReporterTestResult { let formattedResult = testResultOriginal; diff --git a/lib/reporter-helpers.ts b/lib/reporter-helpers.ts index c32df4b52..c1605c2f0 100644 --- a/lib/reporter-helpers.ts +++ b/lib/reporter-helpers.ts @@ -3,9 +3,9 @@ import tmp from 'tmp'; import _ from 'lodash'; import {getShortMD5, isImageInfoWithState} from './common-utils'; import * as utils from './server-utils'; -import {ReporterTestResult} from './test-adapter'; +import {ReporterTestResult} from './adapters/test-result'; import {getImagesInfoByStateName} from './server-utils'; -import {copyAndUpdate} from './test-adapter/utils'; +import {copyAndUpdate} from './adapters/test-result/utils'; import {ImageInfoFull, ImageInfoUpdated} from './types'; import {UPDATED} from './constants'; diff --git a/lib/server-utils.ts b/lib/server-utils.ts index 0ffbda713..da100250c 100644 --- a/lib/server-utils.ts +++ b/lib/server-utils.ts @@ -5,7 +5,6 @@ import url from 'url'; import chalk from 'chalk'; import {Router} from 'express'; import fs from 'fs-extra'; -import type Testplane from 'testplane'; import type {Test as TestplaneTest} from 'testplane'; import _ from 'lodash'; import tmp from 'tmp'; @@ -13,15 +12,14 @@ import tmp from 'tmp'; import {getShortMD5, logger, mkTestId} from './common-utils'; import {UPDATED, RUNNING, IDLE, SKIPPED, IMAGES_PATH, TestStatus, UNKNOWN_ATTEMPT} from './constants'; import type {HtmlReporter} from './plugin-api'; -import type {ReporterTestResult} from './test-adapter'; +import type {ReporterTestResult} from './adapters/test-result'; import { - CustomGuiItem, TestplaneTestResult, ImageInfoWithState, ReporterConfig, TestSpecByPath } from './types'; -import {TestplaneTestAdapter} from './test-adapter/testplane'; +import {TestplaneTestResultAdapter} from './adapters/test-result/testplane'; const DATA_FILE_NAME = 'data.js'; @@ -243,29 +241,6 @@ export function getDataForStaticFile(htmlReporter: HtmlReporter, pluginConfig: R }; } -export async function initializeCustomGui(testplane: Testplane, {customGui}: ReporterConfig): Promise { - await Promise.all( - _(customGui) - .flatMap(_.identity) - .map((ctx) => ctx.initialize?.({testplane, hermione: testplane, ctx})) - .value() - ); -} - -export interface CustomGuiActionPayload { - sectionName: string; - groupIndex: number; - controlIndex: number; -} - -export async function runCustomGuiAction(testplane: Testplane, {customGui}: ReporterConfig, payload: CustomGuiActionPayload): Promise { - const {sectionName, groupIndex, controlIndex} = payload; - const ctx = customGui[sectionName][groupIndex]; - const control = ctx.controls[controlIndex]; - - await ctx.action({testplane, hermione: testplane, control, ctx}); -} - export function getPluginClientScriptPath(pluginName: string): string | null { try { return require.resolve(`${pluginName}/plugin.js`); @@ -320,7 +295,7 @@ export const formatTestResult = ( status: TestStatus, attempt: number = UNKNOWN_ATTEMPT ): ReporterTestResult => { - return new TestplaneTestAdapter(rawResult, {attempt, status}); + return new TestplaneTestResultAdapter(rawResult, {attempt, status}); }; export const saveErrorDetails = async (testResult: ReporterTestResult, reportPath: string): Promise => { diff --git a/lib/sqlite-client.ts b/lib/sqlite-client.ts index 106b11057..44ffbc9f0 100644 --- a/lib/sqlite-client.ts +++ b/lib/sqlite-client.ts @@ -9,8 +9,8 @@ import {TestStatus, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_N import {createTablesQuery} from './db-utils/common'; import type {ImageInfoFull, TestError} from './types'; import {HtmlReporter} from './plugin-api'; -import {ReporterTestResult} from './test-adapter'; -import {DbTestResultTransformer} from './test-adapter/transformers/db'; +import {ReporterTestResult} from './adapters/test-result'; +import {DbTestResultTransformer} from './adapters/test-result/transformers/db'; const debug = makeDebug('html-reporter:sqlite-client'); diff --git a/lib/static/components/controls/run-button/index.jsx b/lib/static/components/controls/run-button/index.jsx index 81bc7b6dc..5384ccc23 100644 --- a/lib/static/components/controls/run-button/index.jsx +++ b/lib/static/components/controls/run-button/index.jsx @@ -20,7 +20,7 @@ const RunMode = Object.freeze({ }); const RunButton = ({actions, autoRun, isDisabled, isRunning, failedTests, checkedTests}) => { - const [mode, setMode] = useState(RunMode.ALL); + const [mode, setMode] = useLocalStorage('RunMode', RunMode.FAILED); const [showCheckboxes] = useLocalStorage('showCheckboxes', false); const btnClassName = classNames('btn', {'button_blink': isRunning}); diff --git a/lib/static/components/controls/test-name-filter-input.jsx b/lib/static/components/controls/test-name-filter-input.jsx index f11facaa4..18d65690a 100644 --- a/lib/static/components/controls/test-name-filter-input.jsx +++ b/lib/static/components/controls/test-name-filter-input.jsx @@ -25,7 +25,7 @@ const TestNameFilterInput = ({actions, testNameFilter: testNameFilterProp}) => { diff --git a/lib/static/components/section/body/meta-info/content.jsx b/lib/static/components/section/body/meta-info/content.jsx index d4f008b89..b0477d632 100644 --- a/lib/static/components/section/body/meta-info/content.jsx +++ b/lib/static/components/section/body/meta-info/content.jsx @@ -1,23 +1,19 @@ import path from 'path'; -import React, {Component, Fragment} from 'react'; -import {connect} from 'react-redux'; -import ClipboardButton from 'react-clipboard.js'; +import React, { Component, Fragment } from 'react'; +import { connect } from 'react-redux'; +import { ClipboardButton } from '@gravity-ui/uikit'; import PropTypes from 'prop-types'; -import {map, mapValues, isObject, omitBy, isEmpty} from 'lodash'; -import {isUrl, getUrlWithBase} from '../../../../../common-utils'; +import { map, mapValues, isObject, omitBy, isEmpty } from 'lodash'; +import { isUrl, getUrlWithBase } from '../../../../../common-utils'; -const mkLinkToUrl = (url, text = url) => { +const mkTextWithClipboardButton = (text, url) => { return - - {text} - - - + {url ? + {text || url} + : text} + ; -}; +} const serializeMetaValues = (metaInfo) => mapValues(metaInfo, (v) => isObject(v) ? JSON.stringify(v) : v); @@ -40,14 +36,17 @@ const resolveUrl = (baseUrl, value) => { const metaToElements = (metaInfo, metaInfoBaseUrls) => { return map(metaInfo, (value, key) => { if (isUrl(value)) { - value = mkLinkToUrl(value); + value = mkTextWithClipboardButton(value, value); } else if (metaInfoBaseUrls[key]) { const baseUrl = metaInfoBaseUrls[key]; const link = isUrl(baseUrl) ? resolveUrl(baseUrl, value) : path.join(baseUrl, value); - value = mkLinkToUrl(link, value); + value = mkTextWithClipboardButton(value, link); } else if (typeof value === 'boolean') { value = value.toString(); } + else if (typeof value === 'string') { + value = mkTextWithClipboardButton(value); + } return
{key}: @@ -74,17 +73,17 @@ class MetaInfoContent extends Component { }; getExtraMetaInfo = () => { - const {testName, apiValues: {extraItems, metaInfoExtenders}} = this.props; + const { testName, apiValues: { extraItems, metaInfoExtenders } } = this.props; return omitBy(mapValues(metaInfoExtenders, (extender) => { const stringifiedFn = extender.startsWith('return') ? extender : `return ${extender}`; - return new Function(stringifiedFn)()({testName}, extraItems); + return new Function(stringifiedFn)()({ testName }, extraItems); }), isEmpty); }; render() { - const {result, metaInfoBaseUrls, baseHost} = this.props; + const { result, metaInfoBaseUrls, baseHost } = this.props; const serializedMetaValues = serializeMetaValues(result.metaInfo); const extraMetaInfo = this.getExtraMetaInfo(); @@ -93,7 +92,7 @@ class MetaInfoContent extends Component { ...extraMetaInfo }; if (result.suiteUrl) { - formattedMetaInfo.url = mkLinkToUrl(getUrlWithBase(result.suiteUrl, baseHost), result.metaInfo.url); + formattedMetaInfo.url = mkTextWithClipboardButton(result.metaInfo.url, getUrlWithBase(result.suiteUrl, baseHost)); } return metaToElements(formattedMetaInfo, metaInfoBaseUrls); @@ -101,7 +100,7 @@ class MetaInfoContent extends Component { } export default connect( - ({tree, config: {metaInfoBaseUrls}, apiValues, view}, {resultId}) => { + ({ tree, config: { metaInfoBaseUrls }, apiValues, view }, { resultId }) => { const result = tree.results.byId[resultId]; const browser = tree.browsers.byId[result.parentId]; diff --git a/lib/static/components/section/title/browser.jsx b/lib/static/components/section/title/browser.jsx index 1810a0484..c61917a90 100644 --- a/lib/static/components/section/title/browser.jsx +++ b/lib/static/components/section/title/browser.jsx @@ -3,7 +3,7 @@ import ClipboardButton from 'react-clipboard.js'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {get} from 'lodash'; +import {get, escapeRegExp} from 'lodash'; import * as actions from '../../../modules/actions'; import {appendQuery} from '../../../modules/query-params'; import {ViewMode} from '../../../../constants/view-modes'; @@ -15,8 +15,8 @@ import Bullet from '../../bullet'; const BrowserTitle = (props) => { const getTestUrl = () => { return appendQuery(window.location.href, { - browser: props.browserName, - testNameFilter: props.testName, + browser: escapeRegExp(props.browserName), + testNameFilter: escapeRegExp(props.testName), strictMatchFilter: true, retryIndex: props.retryIndex, viewModes: ViewMode.ALL, diff --git a/lib/static/gui.jsx b/lib/static/gui.jsx index 2b310da92..41240faf0 100644 --- a/lib/static/gui.jsx +++ b/lib/static/gui.jsx @@ -3,12 +3,18 @@ import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import store from './modules/store'; import Gui from './components/gui'; +import {ThemeProvider} from '@gravity-ui/uikit'; + +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; const rootEl = document.getElementById('app'); ReactDOM.render( - - - , + + + + + , rootEl ); diff --git a/lib/static/index.jsx b/lib/static/index.jsx index 49429f695..c9bc4ecca 100644 --- a/lib/static/index.jsx +++ b/lib/static/index.jsx @@ -3,12 +3,18 @@ import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import store from './modules/store'; import Report from './components/report'; +import {ThemeProvider} from '@gravity-ui/uikit'; + +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; const rootEl = document.getElementById('app'); ReactDOM.render( - - - , + + + + + , rootEl ); diff --git a/lib/static/styles.css b/lib/static/styles.css index 66b016d58..fce08b877 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -530,7 +530,7 @@ details[open] > .details__summary:before { .meta-info__item { display: flex; word-wrap: break-word; - margin-bottom: 5px; + align-items: center; } .meta-info__item:last-child { @@ -546,6 +546,7 @@ details[open] > .details__summary:before { .meta-info__item-value { display: grid; grid-template-columns: repeat(2, auto); + align-items: center; } .custom-icon_view-local{ @@ -555,6 +556,18 @@ details[open] > .details__summary:before { text-overflow: ellipsis; } +.copy-button { + margin-left: 2px; +} + +.meta-info__item .copy-button { + opacity: 0; +} + +.meta-info__item:hover .copy-button { + opacity: 1; +} + .error { background: #f0f2f5; padding: 10px; diff --git a/lib/test-adapter/hermione.ts b/lib/test-adapter/hermione.ts deleted file mode 100644 index f8e1d70e2..000000000 --- a/lib/test-adapter/hermione.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - TestplaneTestAdapter as HermioneTestAdapter, - TestplaneTestAdapterOptions as HermioneTestAdapterOptions, - getStatus -} from './testplane'; diff --git a/lib/test-attempt-manager.ts b/lib/test-attempt-manager.ts index 2b65263e5..98ffcf839 100644 --- a/lib/test-attempt-manager.ts +++ b/lib/test-attempt-manager.ts @@ -1,4 +1,4 @@ -import {ReporterTestResult} from './test-adapter'; +import {ReporterTestResult} from './adapters/test-result'; import {IDLE, RUNNING, TestStatus} from './constants'; type TestSpec = Pick diff --git a/lib/tests-tree-builder/base.ts b/lib/tests-tree-builder/base.ts index 8da495b23..f45c45b76 100644 --- a/lib/tests-tree-builder/base.ts +++ b/lib/tests-tree-builder/base.ts @@ -1,9 +1,9 @@ import _ from 'lodash'; import {determineFinalStatus} from '../common-utils'; import {BrowserVersions, PWT_TITLE_DELIMITER, TESTPLANE_TITLE_DELIMITER, TestStatus, ToolName} from '../constants'; -import {ReporterTestResult} from '../test-adapter'; +import {ReporterTestResult} from '../adapters/test-result'; import {ErrorDetails, ImageInfoFull} from '../types'; -import {TreeTestResultTransformer} from '../test-adapter/transformers/tree'; +import {TreeTestResultTransformer} from '../adapters/test-result/transformers/tree'; import {DbTestResult} from '../sqlite-client'; export type BaseTreeTestResult = Omit & { diff --git a/lib/tests-tree-builder/static.ts b/lib/tests-tree-builder/static.ts index a45707829..501886680 100644 --- a/lib/tests-tree-builder/static.ts +++ b/lib/tests-tree-builder/static.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import {BaseTestsTreeBuilder, BaseTestsTreeBuilderOptions, Tree} from './base'; import {BrowserVersions, DB_COLUMN_INDEXES, TestStatus} from '../constants'; -import {ReporterTestResult} from '../test-adapter'; -import {SqliteTestAdapter} from '../test-adapter/sqlite'; +import {ReporterTestResult} from '../adapters/test-result'; +import {SqliteTestResultAdapter} from '../adapters/test-result/sqlite'; import {getTitleDelimiter} from '../common-utils'; import {RawSuitesRow} from '../types'; @@ -70,7 +70,7 @@ export class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { attemptsMap.set(browserId, attemptsMap.has(browserId) ? attemptsMap.get(browserId) as number + 1 : 0); const attempt = attemptsMap.get(browserId) as number; - const formattedResult = new SqliteTestAdapter(row, attempt, {titleDelimiter: getTitleDelimiter(this._toolName)}); + const formattedResult = new SqliteTestResultAdapter(row, attempt, {titleDelimiter: getTitleDelimiter(this._toolName)}); addBrowserVersion(browsers, formattedResult); diff --git a/lib/types.ts b/lib/types.ts index 0f01aa735..a62068095 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -41,6 +41,13 @@ export interface ImageFile { size: ImageSize; } +export interface RefImageFile extends ImageFile { + /** + * @note defined if testplane >= 8.13.0 + */ + relativePath?: string; +} + export interface ImageBuffer { buffer: Buffer; } @@ -59,7 +66,10 @@ export interface DiffOptions extends LooksSameOptions { export interface TestError { name: string; message: string; - snippet?: string; // defined if testplane >= 8.11.0 + /** + * @note defined if testplane >= 8.11.0 + */ + snippet?: string; stack?: string; stateName?: string; details?: ErrorDetails @@ -69,27 +79,37 @@ export interface TestError { export interface ImageInfoDiff { status: TestStatus.FAIL; stateName: string; - // Ref image is absent in pwt test results - refImg?: ImageFile; + /** + * @note Ref image is absent in pwt test results + */ + refImg?: RefImageFile; diffClusters?: CoordBounds[]; expectedImg: ImageFile; actualImg: ImageFile; diffImg?: ImageFile | ImageBuffer; diffOptions: DiffOptions; - differentPixels?: number; // defined if hermione >= 8.2.0 - diffRatio?: number; // defined if hermione >= 8.2.0 + /** + * @note defined if hermione >= 8.2.0 + */ + differentPixels?: number; + /** + * @note defined if hermione >= 8.2.0 + */ + diffRatio?: number; } interface AssertViewSuccess { stateName: string; - refImg: ImageFile; + refImg: RefImageFile; } export interface ImageInfoSuccess { status: TestStatus.SUCCESS; stateName: string; - // Ref image may be absent in pwt test results - refImg?: ImageFile; + /** + * @note Ref image is absent in pwt test results + */ + refImg?: RefImageFile; diffClusters?: CoordBounds[]; expectedImg: ImageFile; actualImg?: ImageFile; @@ -109,15 +129,17 @@ export interface ImageInfoNoRef { status: TestStatus.ERROR; error?: TestError; stateName: string; - // Ref image may be absent in pwt test results - refImg?: ImageFile; + /** + * @note Ref image is absent in pwt test results + */ + refImg?: RefImageFile; actualImg: ImageFile; } export interface ImageInfoUpdated { status: TestStatus.UPDATED; stateName: string; - refImg: ImageFile; + refImg: RefImageFile; actualImg: ImageFile; expectedImg: ImageFile; } diff --git a/package-lock.json b/package-lock.json index b8a1ec017..148e195a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "html-reporter", - "version": "9.19.0", + "version": "10.3.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "html-reporter", - "version": "9.18.1", + "version": "10.3.1", "license": "MIT", "workspaces": [ "test/func/fixtures/*", @@ -18,7 +18,7 @@ "@gemini-testing/sql.js": "^2.0.0", "ansi-html-community": "^0.0.8", "axios": "1.6.3", - "better-sqlite3": "^8.5.0", + "better-sqlite3": "^10.0.0", "bluebird": "^3.5.3", "body-parser": "^1.18.2", "chalk": "^4.1.2", @@ -46,6 +46,9 @@ "tmp": "^0.1.0", "worker-farm": "^1.7.0" }, + "bin": { + "html-reporter": "bin/html-reporter" + }, "devDependencies": { "@babel/core": "^7.22.5", "@babel/plugin-transform-runtime": "^7.22.5", @@ -53,7 +56,8 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@gemini-testing/commander": "^2.15.3", - "@playwright/test": "^1.37.1", + "@gravity-ui/uikit": "^6.20.0", + "@playwright/test": "^1.44.1", "@swc/core": "^1.3.64", "@types/better-sqlite3": "^7.6.4", "@types/bluebird": "^3.5.3", @@ -161,10 +165,10 @@ "webpack-merge": "^4.1.1" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "hermione": ">=6.0.0", + "hermione": ">=8.0.0", "testplane": "*" }, "peerDependenciesMeta": { @@ -2893,6 +2897,12 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/@bem-react/classname": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.6.0.tgz", + "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==", + "dev": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3121,6 +3131,85 @@ "resolved": "https://registry.npmjs.org/@gemini-testing/sql.js/-/sql.js-2.0.0.tgz", "integrity": "sha512-FoslR6S5cxObp0fNtiFkQ6TvGZ5sd7+KomY7Hm3sH51uAHxyzJSQGvpsaiw5dmO/HHYEBF/bJMXHVWEQGfot7A==" }, + "node_modules/@gravity-ui/i18n": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/i18n/-/i18n-1.5.1.tgz", + "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==", + "dev": true + }, + "node_modules/@gravity-ui/icons": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.10.0.tgz", + "integrity": "sha512-xS0G4+TM7cD2cCKS4wVc01c4lLe/OreKjm4sHwrOtJWH4EawaRbpkuwtgUDcUvY2EryIcI6lgV+8o714m6lcyQ==", + "dev": true, + "peerDependencies": { + "react": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@gravity-ui/uikit": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", + "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", + "dev": true, + "dependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/i18n": "^1.3.0", + "@gravity-ui/icons": "^2.8.1", + "@popperjs/core": "^2.11.8", + "blueimp-md5": "^2.19.0", + "focus-trap": "^7.5.4", + "lodash": "^4.17.21", + "rc-slider": "^10.5.0", + "react-beautiful-dnd": "^13.1.1", + "react-copy-to-clipboard": "^5.1.0", + "react-popper": "^2.3.0", + "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.21", + "react-window": "^1.8.10", + "tabbable": "^6.2.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@gravity-ui/uikit/node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dev": true, + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@gravity-ui/uikit/node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -3419,12 +3508,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", - "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "dev": true, "dependencies": { - "playwright": "1.38.1" + "playwright": "1.44.1" }, "bin": { "playwright": "cli.js" @@ -3433,6 +3522,16 @@ "node": ">=16" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@puppeteer/browsers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", @@ -7280,6 +7379,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/abort-controller": { @@ -7330,9 +7430,9 @@ } }, "node_modules/acorn-globals/node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -7342,10 +7442,13 @@ } }, "node_modules/acorn-globals/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } @@ -8504,13 +8607,13 @@ "dev": true }, "node_modules/better-sqlite3": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.5.0.tgz", - "integrity": "sha512-vbPcv/Hx5WYdyNg/NbcfyaBZyv9s/NVbxb7yCeC5Bq1pVocNxeL2tZmSu3Rlm4IEOTjYdGyzWQgyx0OSdORBzw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-10.0.0.tgz", + "integrity": "sha512-rOz0JY8bt9oMgrFssP7GnvA5R3yln73y/NizzWqy3WlFth8Ux8+g4r/N9fjX97nn4X1YX6MTER2doNpTu5pqiA==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", - "prebuild-install": "^7.1.0" + "prebuild-install": "^7.1.1" } }, "node_modules/big-integer": { @@ -8630,6 +8733,12 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, "node_modules/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -11464,6 +11573,15 @@ "node": ">=0.10.0" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dev": true, + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -11931,6 +12049,15 @@ "urix": "^0.1.0" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dev": true, + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -13341,6 +13468,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { "webidl-conversions": "^7.0.0" @@ -15427,6 +15555,15 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -19129,9 +19266,9 @@ } }, "node_modules/jsdom/node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -19164,37 +19301,25 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/jsdom/node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "node_modules/jsdom/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { - "node": ">=6" - } - }, - "node_modules/jsdom/node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "node": ">=10.0.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsdom/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/jsesc": { @@ -20036,6 +20161,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true + }, "node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -21330,9 +21461,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", "dev": true }, "node_modules/nyc": { @@ -22398,12 +22529,12 @@ } }, "node_modules/playwright": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", - "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", "dev": true, "dependencies": { - "playwright-core": "1.38.1" + "playwright-core": "1.44.1" }, "bin": { "playwright": "cli.js" @@ -22416,9 +22547,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", - "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -23408,9 +23539,9 @@ "dev": true }, "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "node_modules/public-encrypt": { @@ -23579,6 +23710,12 @@ "performance-now": "^2.1.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "dev": true + }, "node_modules/railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -23670,21 +23807,44 @@ "react-dom": ">=16.9.0" } }, - "node_modules/rc-resize-observer/node_modules/rc-util": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.15.0.tgz", - "integrity": "sha512-8RI8sjOCXD3FhD3dzQNBQetpGol6BBd3sHQ/8jSGk9NPT0CH3JGtBfPODnASyE7AdDpCFQMOmgcp9CBs3S/1hg==", + "node_modules/rc-slider": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", + "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "react-is": "^16.12.0", - "shallowequal": "^1.1.0" + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, + "node_modules/rc-util": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", + "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/react": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", @@ -23699,6 +23859,25 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-checkbox-tree": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz", @@ -23727,6 +23906,19 @@ "react": ">=15.5.0" } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dev": true, + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -23775,6 +23967,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "dev": true + }, "node_modules/react-hotkeys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", @@ -24086,6 +24284,33 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", + "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", + "dev": true, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-only-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", @@ -27078,6 +27303,12 @@ "acorn-node": "^1.2.0" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -27776,6 +28007,12 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, "node_modules/tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", @@ -27858,6 +28095,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "dev": true + }, "node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -27866,6 +28109,39 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -27879,9 +28155,9 @@ } }, "node_modules/tr46/node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -28016,9 +28292,9 @@ } }, "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -28757,6 +29033,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/userhome": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", @@ -33420,6 +33705,12 @@ } } }, + "@bem-react/classname": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.6.0.tgz", + "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==", + "dev": true + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -33588,6 +33879,67 @@ "resolved": "https://registry.npmjs.org/@gemini-testing/sql.js/-/sql.js-2.0.0.tgz", "integrity": "sha512-FoslR6S5cxObp0fNtiFkQ6TvGZ5sd7+KomY7Hm3sH51uAHxyzJSQGvpsaiw5dmO/HHYEBF/bJMXHVWEQGfot7A==" }, + "@gravity-ui/i18n": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/i18n/-/i18n-1.5.1.tgz", + "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==", + "dev": true + }, + "@gravity-ui/icons": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.10.0.tgz", + "integrity": "sha512-xS0G4+TM7cD2cCKS4wVc01c4lLe/OreKjm4sHwrOtJWH4EawaRbpkuwtgUDcUvY2EryIcI6lgV+8o714m6lcyQ==", + "dev": true, + "requires": {} + }, + "@gravity-ui/uikit": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", + "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", + "dev": true, + "requires": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/i18n": "^1.3.0", + "@gravity-ui/icons": "^2.8.1", + "@popperjs/core": "^2.11.8", + "blueimp-md5": "^2.19.0", + "focus-trap": "^7.5.4", + "lodash": "^4.17.21", + "rc-slider": "^10.5.0", + "react-beautiful-dnd": "^13.1.1", + "react-copy-to-clipboard": "^5.1.0", + "react-popper": "^2.3.0", + "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.21", + "react-window": "^1.8.10", + "tabbable": "^6.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dev": true, + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + } + } + }, "@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -33813,14 +34165,20 @@ "optional": true }, "@playwright/test": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", - "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "dev": true, "requires": { - "playwright": "1.38.1" + "playwright": "1.44.1" } }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true + }, "@puppeteer/browsers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", @@ -36785,16 +37143,19 @@ }, "dependencies": { "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true }, "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } } } }, @@ -37674,12 +38035,12 @@ "dev": true }, "better-sqlite3": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.5.0.tgz", - "integrity": "sha512-vbPcv/Hx5WYdyNg/NbcfyaBZyv9s/NVbxb7yCeC5Bq1pVocNxeL2tZmSu3Rlm4IEOTjYdGyzWQgyx0OSdORBzw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-10.0.0.tgz", + "integrity": "sha512-rOz0JY8bt9oMgrFssP7GnvA5R3yln73y/NizzWqy3WlFth8Ux8+g4r/N9fjX97nn4X1YX6MTER2doNpTu5pqiA==", "requires": { "bindings": "^1.5.0", - "prebuild-install": "^7.1.0" + "prebuild-install": "^7.1.1" } }, "big-integer": { @@ -37761,6 +38122,12 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -40050,6 +40417,15 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dev": true, + "requires": { + "toggle-selection": "^1.0.6" + } + }, "copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -40432,6 +40808,15 @@ } } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dev": true, + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -43092,6 +43477,15 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "requires": { + "tabbable": "^6.2.0" + } + }, "follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -45895,9 +46289,9 @@ }, "dependencies": { "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true }, "entities": { @@ -45915,29 +46309,12 @@ "entities": "^4.4.0" } }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true + "requires": {} } } }, @@ -46629,6 +47006,12 @@ "fs-monkey": "^1.0.4" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -47639,9 +48022,9 @@ "dev": true }, "nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", "dev": true }, "nyc": { @@ -48469,19 +48852,19 @@ } }, "playwright": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", - "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.38.1" + "playwright-core": "1.44.1" } }, "playwright-core": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", - "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", "dev": true }, "playwright-fixture-report": { @@ -49245,9 +49628,9 @@ "dev": true }, "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "public-encrypt": { @@ -49373,6 +49756,12 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "dev": true + }, "railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -49446,18 +49835,34 @@ "classnames": "^2.2.1", "rc-util": "^5.0.0", "resize-observer-polyfill": "^1.5.1" + } + }, + "rc-slider": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", + "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + } + }, + "rc-util": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", + "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" }, "dependencies": { - "rc-util": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.15.0.tgz", - "integrity": "sha512-8RI8sjOCXD3FhD3dzQNBQetpGol6BBd3sHQ/8jSGk9NPT0CH3JGtBfPODnASyE7AdDpCFQMOmgcp9CBs3S/1hg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "react-is": "^16.12.0", - "shallowequal": "^1.1.0" - } + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true } } }, @@ -49472,6 +49877,21 @@ "prop-types": "^15.6.2" } }, + "react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-checkbox-tree": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz", @@ -49494,6 +49914,16 @@ "prop-types": "^15.5.0" } }, + "react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dev": true, + "requires": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + } + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -49532,6 +49962,12 @@ } } }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "dev": true + }, "react-hotkeys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", @@ -49784,6 +50220,23 @@ "react-lifecycles-compat": "^3.0.4" } }, + "react-virtualized-auto-sizer": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", + "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", + "dev": true, + "requires": {} + }, + "react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, "read-only-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", @@ -52188,6 +52641,12 @@ "acorn-node": "^1.2.0" } }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -52736,6 +53195,12 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "dev": true }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", @@ -52802,11 +53267,43 @@ "is-number": "^7.0.0" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "dev": true + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } + } + }, "tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -52817,9 +53314,9 @@ }, "dependencies": { "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true } } @@ -52905,9 +53402,9 @@ } }, "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "tsutils": { "version": "3.21.0", @@ -53462,6 +53959,13 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "dev": true, + "requires": {} + }, "userhome": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", diff --git a/package.json b/package.json index c5e6b0f62..5c66a0dd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-reporter", - "version": "9.19.0", + "version": "10.3.2", "description": "Html-reporter and GUI for viewing and managing results of a tests run. Currently supports Testplane and Hermione.", "files": [ "build" @@ -17,10 +17,12 @@ "copy-static": "copyfiles 'lib/static/icons/*' .npmignore build", "check-types": "tsc --project tsconfig.spec.json", "coverage": "nyc npm run test-unit", - "e2e:build-browsers": "docker build -f test/func/docker/Dockerfile -t html-reporter-browsers:0.0.1 --network host test/func/docker", + "browsers:build:local": "PLATFORM=$([ $(node -e 'console.log(process.arch)') = 'arm64' ] && echo linux/arm64 || echo linux/amd64) npm run browsers:build:single-platform", + "browsers:build:single-platform": "docker build -f test/func/docker/Dockerfile --platform $PLATFORM -t yinfra/html-reporter-browsers test/func/docker --load", + "browsers:build-and-push": "docker buildx build -t yinfra/html-reporter-browsers --platform linux/amd64,linux/arm64 test/func/docker --push", + "browsers:launch": "docker run -it --rm --network=host $(which colima >/dev/null || echo --add-host=host.docker.internal:0.0.0.0) yinfra/html-reporter-browsers", "e2e:build-packages": "npm run --workspace=test/func/packages --if-present build", "e2e:generate-fixtures": "npm run --workspace=test/func/fixtures generate", - "e2e:launch-browsers": "docker run -it --rm --network=host --add-host=host.docker.internal:0.0.0.0 html-reporter-browsers:0.0.1", "e2e:test": "npm run --workspace=test/func/tests test", "e2e": "npm run e2e:build-packages && npm run e2e:generate-fixtures ; npm run e2e:test", "lint": "eslint .", @@ -48,7 +50,7 @@ "url": "https://github.com/gemini-testing/html-reporter/issues" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "keywords": [ "testplane", @@ -56,10 +58,13 @@ "plugin", "html-reporter" ], + "bin": { + "html-reporter": "./bin/html-reporter" + }, "homepage": "https://github.com/gemini-testing/html-reporter#readme", "license": "MIT", "peerDependencies": { - "hermione": ">=6.0.0", + "hermione": ">=8.0.0", "testplane": "*" }, "peerDependenciesMeta": { @@ -75,7 +80,7 @@ "@gemini-testing/sql.js": "^2.0.0", "ansi-html-community": "^0.0.8", "axios": "1.6.3", - "better-sqlite3": "^8.5.0", + "better-sqlite3": "^10.0.0", "bluebird": "^3.5.3", "body-parser": "^1.18.2", "chalk": "^4.1.2", @@ -110,7 +115,8 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@gemini-testing/commander": "^2.15.3", - "@playwright/test": "^1.37.1", + "@gravity-ui/uikit": "^6.20.0", + "@playwright/test": "^1.44.1", "@swc/core": "^1.3.64", "@types/better-sqlite3": "^7.6.4", "@types/bluebird": "^3.5.3", diff --git a/playwright.ts b/playwright.ts index 3efc83ab1..9f7c42e5a 100644 --- a/playwright.ts +++ b/playwright.ts @@ -12,7 +12,7 @@ import {ReporterConfig, TestSpecByPath} from './lib/types'; import {parseConfig} from './lib/config'; import {PluginEvents, ToolName, UNKNOWN_ATTEMPT} from './lib/constants'; import {RegisterWorkers} from './lib/workers/create-workers'; -import {PlaywrightTestAdapter} from './lib/test-adapter/playwright'; +import {PlaywrightTestResultAdapter} from './lib/adapters/test-result/playwright'; import {SqliteClient} from './lib/sqlite-client'; import {SqliteImageStore} from './lib/image-store'; import {ImagesInfoSaver} from './lib/images-info-saver'; @@ -31,7 +31,9 @@ class MyReporter implements Reporter { protected _initPromise: Promise; constructor(opts: Partial) { - this._config = parseConfig(_.omit(opts, ['configDir'])); + const reporterOpts = _.omitBy(opts, (_value, key) => key === 'configDir' || key.startsWith('_')); + + this._config = parseConfig(reporterOpts); this._htmlReporter = HtmlReporter.create(this._config, {toolName: ToolName.Playwright}); this._staticReportBuilder = null; this._workerFarm = workerFarm(require.resolve('./lib/workers/worker'), ['saveDiffTo']); @@ -68,7 +70,7 @@ class MyReporter implements Reporter { const staticReportBuilder = this._staticReportBuilder as StaticReportBuilder; - const formattedResult = new PlaywrightTestAdapter(test, result, UNKNOWN_ATTEMPT); + const formattedResult = new PlaywrightTestResultAdapter(test, result, UNKNOWN_ATTEMPT); await staticReportBuilder.addTestResult(formattedResult); }); diff --git a/test/func/common.testplane.conf.js b/test/func/common.testplane.conf.js index ee3e07390..0ffee03fa 100644 --- a/test/func/common.testplane.conf.js +++ b/test/func/common.testplane.conf.js @@ -13,7 +13,7 @@ module.exports.getCommonConfig = (projectDir) => ({ desiredCapabilities: { browserName: 'chrome', 'goog:chromeOptions': { - args: ['headless', 'no-sandbox', 'hide-scrollbars'], + args: ['headless', 'no-sandbox', 'hide-scrollbars', 'disable-dev-shm-usage'], binary: CHROME_BINARY_PATH } }, diff --git a/test/func/docker/Dockerfile b/test/func/docker/Dockerfile index e65354f82..412272c94 100644 --- a/test/func/docker/Dockerfile +++ b/test/func/docker/Dockerfile @@ -1,15 +1,28 @@ -FROM cimg/node:16.20-browsers +FROM node:18-slim -ENV CHROME_VERSION=116 +ARG DEBIAN_FRONTEND=noninteractive -USER circleci +ARG BUILDPLATFORM -COPY --chown=circleci browser-utils browser-utils -WORKDIR browser-utils +ENV CHROMIUM_VERSION="126.0.6478.126-1~deb12u1" +ENV ARM_CHROMEDRIVER_URL="https://github.com/electron/electron/releases/download/v31.1.0/chromedriver-v31.1.0-linux-arm64.zip" +ENV ARM_CHROMEDRIVER_RELATIVE_PATH="" +ENV AMD_CHROMEDRIVER_URL="https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.126/linux64/chromedriver-linux64.zip" +ENV AMD_CHROMEDRIVER_RELATIVE_PATH="/chromedriver-linux64" -RUN npm ci && npm run install-chromium +RUN if [ "$BUILDPLATFORM" = "linux/arm64" ]; then export RELATIVE_PATH="$ARM_CHROMEDRIVER_RELATIVE_PATH"; export URL="$ARM_CHROMEDRIVER_URL"; \ + else export RELATIVE_PATH="$AMD_CHROMEDRIVER_RELATIVE_PATH"; export URL="$AMD_CHROMEDRIVER_URL"; fi && \ + apt-get update -y && \ + apt-get install -y wget unzip git && \ + wget "$URL" -O "chromedriver.zip" && \ + unzip "chromedriver" && \ + if [ "$RELATIVE_PATH" != "" ]; then mv "$RELATIVE_PATH/chromedriver" "/chromedriver"; fi && \ + apt update -y && \ + apt install libnss3 -y && \ + rm chromedriver.zip && \ + (rm chromedriver.debug || true) && \ + apt-get install chromium="$CHROMIUM_VERSION" -y -RUN npm install selenium-standalone@9.1.1 -g && \ - selenium-standalone install --drivers.chrome.version=$CHROME_VERSION +LABEL com.circleci.preserve-entrypoint=true -ENTRYPOINT selenium-standalone start --drivers.chrome.version=$CHROME_VERSION +ENTRYPOINT ./chromedriver --port=4444 --whitelisted-ips="" diff --git a/test/func/fixtures/plugins/screens/eea1754/chrome/header.png b/test/func/fixtures/plugins/screens/eea1754/chrome/header.png index bf8ebcf91..236088d2e 100644 Binary files a/test/func/fixtures/plugins/screens/eea1754/chrome/header.png and b/test/func/fixtures/plugins/screens/eea1754/chrome/header.png differ diff --git a/test/func/tests/screens/c0db305/chrome/details summary.png b/test/func/tests/screens/c0db305/chrome/details summary.png index cbf9826f5..9b727d925 100644 Binary files a/test/func/tests/screens/c0db305/chrome/details summary.png and b/test/func/tests/screens/c0db305/chrome/details summary.png differ diff --git a/test/func/tests/utils.js b/test/func/tests/utils.js index 980516b61..f918d6591 100644 --- a/test/func/tests/utils.js +++ b/test/func/tests/utils.js @@ -34,7 +34,7 @@ const hideScreenshots = async (browser) => { const runGui = async (projectDir) => { return new Promise((resolve, reject) => { - const child = childProcess.spawn('npm', ['run', 'gui'], {cwd: projectDir}); + const child = childProcess.spawn('npm', ['run', 'gui', '--', '--no-open'], {cwd: projectDir}); let processKillTimeoutId = setTimeout(() => { treeKill(child.pid).then(() => { diff --git a/test/func/utils/constants.js b/test/func/utils/constants.js index a5c675abb..cbeb6b47e 100644 --- a/test/func/utils/constants.js +++ b/test/func/utils/constants.js @@ -1,6 +1,6 @@ module.exports = { - GRID_URL: 'http://localhost:4444/wd/hub/', - CHROME_BINARY_PATH: '/home/circleci/browsers/chrome-linux/chrome', + GRID_URL: 'http://127.0.0.1:4444/', + CHROME_BINARY_PATH: '/usr/bin/chromium', PORTS: { testplane: { server: 8083, diff --git a/test/unit/lib/test-adapter/playwright.ts b/test/unit/lib/adapters/test-result/playwright.ts similarity index 76% rename from test/unit/lib/test-adapter/playwright.ts rename to test/unit/lib/adapters/test-result/playwright.ts index fa1afac99..139d72880 100644 --- a/test/unit/lib/test-adapter/playwright.ts +++ b/test/unit/lib/adapters/test-result/playwright.ts @@ -7,14 +7,14 @@ import { ImageTitleEnding, PlaywrightAttachment, PwtTestStatus -} from 'lib/test-adapter/playwright'; +} from 'lib/adapters/test-result/playwright'; import {ErrorName} from 'lib/errors'; import {ERROR, FAIL, TestStatus, UNKNOWN_ATTEMPT} from 'lib/constants'; import {ImageInfoDiff, ImageInfoNoRef} from 'lib/types'; -describe('PlaywrightTestAdapter', () => { +describe('PlaywrightTestResultAdapter', () => { let sandbox: sinon.SinonSandbox; - let PlaywrightTestAdapter: typeof import('lib/test-adapter/playwright').PlaywrightTestAdapter; + let PlaywrightTestResultAdapter: typeof import('lib/adapters/test-result/playwright').PlaywrightTestResultAdapter; let imageSizeStub: sinon.SinonStub; const createAttachment = (path: string): PlaywrightAttachment => ({ @@ -46,9 +46,9 @@ describe('PlaywrightTestAdapter', () => { imageSizeStub = sinon.stub().returns({height: 100, width: 200}); - PlaywrightTestAdapter = proxyquire('lib/test-adapter/playwright', { + PlaywrightTestResultAdapter = proxyquire('lib/adapters/test-result/playwright', { 'image-size': imageSizeStub - }).PlaywrightTestAdapter; + }).PlaywrightTestResultAdapter; }); afterEach(() => { @@ -57,7 +57,7 @@ describe('PlaywrightTestAdapter', () => { describe('attempt', () => { it('should return suite attempt', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase({titlePath: sinon.stub().returns(['another-title'])}), mkTestResult(), 3); + const adapter = new PlaywrightTestResultAdapter(mkTestCase({titlePath: sinon.stub().returns(['another-title'])}), mkTestResult(), 3); assert.equal(adapter.attempt, 3); }); @@ -65,7 +65,7 @@ describe('PlaywrightTestAdapter', () => { describe('browserId', () => { it('should return browserId', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); assert.equal(adapter.browserId, 'some-browser'); }); @@ -73,7 +73,7 @@ describe('PlaywrightTestAdapter', () => { describe('error', () => { it('should return undefined if there are no errors', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors: []}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({errors: []}), UNKNOWN_ATTEMPT); const {error} = adapter; @@ -83,7 +83,7 @@ describe('PlaywrightTestAdapter', () => { it('should return an error with name NO_REF_IMAGE for snapshot missing errors', () => { const errorMessage = 'A snapshot doesn\'t exist: image-name.png.'; const errors = [{message: errorMessage}]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); const {error} = adapter; @@ -94,7 +94,7 @@ describe('PlaywrightTestAdapter', () => { it('should return an error with name IMAGE_DIFF for screenshot comparison failures', () => { const errorMessage = 'Screenshot comparison failed'; const errors = [{message: errorMessage}]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); const {error} = adapter; @@ -106,7 +106,7 @@ describe('PlaywrightTestAdapter', () => { const errorMessage = 'Some error occurred'; const errorStack = 'Error: Some error occurred at some-file.ts:10:15'; const errors = [{message: errorMessage, stack: errorStack}]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); const {error} = adapter; @@ -118,7 +118,7 @@ describe('PlaywrightTestAdapter', () => { {message: 'First error', stack: 'Error: First error at some-file.ts:5:10'}, {message: 'Second error', stack: 'Error: Second error at another-file.ts:15:20'} ]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({errors}), UNKNOWN_ATTEMPT); const expectedMessage = JSON.stringify(errors.map(err => err.message)); const expectedStack = JSON.stringify(errors.map(err => err.stack)); @@ -131,7 +131,7 @@ describe('PlaywrightTestAdapter', () => { describe('file', () => { it('should return file path', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); assert.strictEqual(adapter.file, 'test-file-path'); }); @@ -139,7 +139,7 @@ describe('PlaywrightTestAdapter', () => { describe('fullName', () => { it('should return fullName', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); assert.strictEqual(adapter.fullName, 'describe › test'); }); @@ -151,7 +151,7 @@ describe('PlaywrightTestAdapter', () => { {title: 'Step1', duration: 100}, {title: 'Step2', duration: 200} ]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({steps} as any), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({steps} as any), UNKNOWN_ATTEMPT); const expectedHistory = ['Step1 <- 100ms\n', 'Step2 <- 200ms\n']; assert.deepEqual(adapter.history, expectedHistory); @@ -160,7 +160,7 @@ describe('PlaywrightTestAdapter', () => { describe('id', () => { it('should return id', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), 0); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), 0); assert.strictEqual(adapter.id, 'describe test some-browser 0'); }); @@ -168,7 +168,7 @@ describe('PlaywrightTestAdapter', () => { describe('imageDir', () => { it('should return imageDir', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); assert.strictEqual(adapter.imageDir, '4050de5'); }); @@ -184,7 +184,7 @@ describe('PlaywrightTestAdapter', () => { {name: 'screenshot', path: 'test-results/test-name-1.png', contentType: 'image/png'} ]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({attachments, errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({attachments, errors}), UNKNOWN_ATTEMPT); assert.equal(adapter.imagesInfo.length, 2); assert.deepEqual(adapter.imagesInfo.find(info => (info as ImageInfoDiff).stateName === undefined), { @@ -209,7 +209,7 @@ describe('PlaywrightTestAdapter', () => { {name: 'screenshot', path: 'test-results/test-name-1.png', contentType: 'image/png'} ]; - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({attachments, errors}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({attachments, errors}), UNKNOWN_ATTEMPT); assert.equal(adapter.imagesInfo.length, 2); assert.deepEqual(adapter.imagesInfo.find(info => (info as ImageInfoNoRef).stateName === undefined), { @@ -231,31 +231,31 @@ describe('PlaywrightTestAdapter', () => { describe('status', () => { it('should return SUCCESS for PASSED PwtTestStatus', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.PASSED}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.PASSED}), UNKNOWN_ATTEMPT); assert.equal(adapter.status, TestStatus.SUCCESS); }); it('should return FAIL for FAILED PwtTestStatus', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.FAILED}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.FAILED}), UNKNOWN_ATTEMPT); assert.equal(adapter.status, TestStatus.FAIL); }); it('should return FAIL for TIMED_OUT PwtTestStatus', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.TIMED_OUT}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.TIMED_OUT}), UNKNOWN_ATTEMPT); assert.equal(adapter.status, TestStatus.FAIL); }); it('should return FAIL for INTERRUPTED PwtTestStatus', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.INTERRUPTED}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.INTERRUPTED}), UNKNOWN_ATTEMPT); assert.equal(adapter.status, TestStatus.FAIL); }); it('should return SKIPPED for any other PwtTestStatus', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.SKIPPED}), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult({status: PwtTestStatus.SKIPPED}), UNKNOWN_ATTEMPT); assert.equal(adapter.status, TestStatus.SKIPPED); }); @@ -263,7 +263,7 @@ describe('PlaywrightTestAdapter', () => { describe('testPath', () => { it('should return testPath', () => { - const adapter = new PlaywrightTestAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); + const adapter = new PlaywrightTestResultAdapter(mkTestCase(), mkTestResult(), UNKNOWN_ATTEMPT); assert.deepEqual(adapter.testPath, ['describe', 'test']); }); diff --git a/test/unit/lib/test-adapter/testplane.ts b/test/unit/lib/adapters/test-result/testplane.ts similarity index 74% rename from test/unit/lib/test-adapter/testplane.ts rename to test/unit/lib/adapters/test-result/testplane.ts index 3ac134533..2144558e0 100644 --- a/test/unit/lib/test-adapter/testplane.ts +++ b/test/unit/lib/adapters/test-result/testplane.ts @@ -6,17 +6,18 @@ import tmpOriginal from 'tmp'; import {TestStatus} from 'lib/constants/test-statuses'; import {ERROR_DETAILS_PATH} from 'lib/constants/paths'; -import {ReporterTestResult} from 'lib/test-adapter'; -import {TestplaneTestAdapter, TestplaneTestAdapterOptions} from 'lib/test-adapter/testplane'; +import {TestplaneTestResultAdapter, TestplaneTestResultAdapterOptions} from 'lib/adapters/test-result/testplane'; import {TestplaneTestResult} from 'lib/types'; import * as originalUtils from 'lib/server-utils'; import * as originalCommonUtils from 'lib/common-utils'; -import * as originalTestAdapterUtils from 'lib/test-adapter/utils'; +import * as originalTestAdapterUtils from 'lib/adapters/test-result/utils'; -describe('TestplaneTestAdapter', () => { +import type {ReporterTestResult} from 'lib/adapters/test-result'; + +describe('TestplaneTestResultAdapter', () => { const sandbox = sinon.sandbox.create(); - let TestplaneTestAdapter: new (testResult: TestplaneTestResult, options: TestplaneTestAdapterOptions) => ReporterTestResult; + let TestplaneTestResultAdapter: new (testResult: TestplaneTestResult, options: TestplaneTestResultAdapterOptions) => ReporterTestResult; let getCommandsHistory: sinon.SinonStub; let getSuitePath: sinon.SinonStub; let utils: sinon.SinonStubbedInstance; @@ -28,8 +29,8 @@ describe('TestplaneTestAdapter', () => { const mkTestplaneTestResultAdapter = ( testResult: TestplaneTestResult, {status = TestStatus.SUCCESS}: {status?: TestStatus} = {} - ): TestplaneTestAdapter => { - return new TestplaneTestAdapter(testResult, {status, attempt: 0}) as TestplaneTestAdapter; + ): TestplaneTestResultAdapter => { + return new TestplaneTestResultAdapter(testResult, {status, attempt: 0}) as TestplaneTestResultAdapter; }; const mkTestResult_ = (result: Partial): TestplaneTestResult => _.defaults(result, { @@ -51,20 +52,19 @@ describe('TestplaneTestAdapter', () => { const originalCommonUtils = proxyquire('lib/common-utils', {}); commonUtils = _.clone(originalCommonUtils); - const originalTestAdapterUtils = proxyquire('lib/test-adapter/utils', { - '../../server-utils': utils, - '../../common-utils': commonUtils + const originalTestAdapterUtils = proxyquire('lib/adapters/test-result/utils', { + '../../../common-utils': commonUtils }); testAdapterUtils = _.clone(originalTestAdapterUtils); - TestplaneTestAdapter = proxyquire('lib/test-adapter/testplane', { + TestplaneTestResultAdapter = proxyquire('lib/adapters/test-result/testplane', { tmp, 'fs-extra': fs, - '../plugin-utils': {getSuitePath}, - '../history-utils': {getCommandsHistory}, + '../../plugin-utils': {getSuitePath}, + '../../history-utils': {getCommandsHistory}, '../server-utils': utils, './utils': testAdapterUtils - }).TestplaneTestAdapter; + }).TestplaneTestResultAdapter; sandbox.stub(utils, 'getCurrentPath').returns(''); sandbox.stub(utils, 'getDiffPath').returns(''); sandbox.stub(utils, 'getReferencePath').returns(''); @@ -89,9 +89,9 @@ describe('TestplaneTestAdapter', () => { } as any }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.error, { + assert.deepEqual(TestplaneTestResultAdapter.error, { message: 'some-message', stack: 'some-stack', stateName: 'some-test', @@ -112,17 +112,17 @@ describe('TestplaneTestAdapter', () => { } as any }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.history, ['some-history']); + assert.deepEqual(TestplaneTestResultAdapter.history, ['some-history']); }); it('should return test state', () => { const testResult = mkTestResult_({title: 'some-test'}); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.state, {name: 'some-test'}); + assert.deepEqual(TestplaneTestResultAdapter.state, {name: 'some-test'}); }); describe('error details', () => { @@ -140,9 +140,9 @@ describe('TestplaneTestAdapter', () => { }); getDetailsFileName.returns('md5-bro-n-time'); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.errorDetails, { + assert.deepEqual(TestplaneTestResultAdapter.errorDetails, { title: 'some-title', data: {foo: 'bar'}, filePath: `${ERROR_DETAILS_PATH}/md5-bro-n-time` @@ -152,9 +152,9 @@ describe('TestplaneTestAdapter', () => { it('should have "error details" title if no title is given', () => { const testResult = mkTestResult_({err: {details: {}} as any}); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.propertyVal(testplaneTestAdapter.errorDetails, 'title', 'error details'); + assert.propertyVal(TestplaneTestResultAdapter.errorDetails, 'title', 'error details'); }); it('should be memoized', () => { @@ -164,10 +164,10 @@ describe('TestplaneTestAdapter', () => { details: {title: 'some-title', data: {foo: 'bar'}} } as any }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - const firstErrDetails = testplaneTestAdapter.errorDetails; - const secondErrDetails = testplaneTestAdapter.errorDetails; + const firstErrDetails = TestplaneTestResultAdapter.errorDetails; + const secondErrDetails = TestplaneTestResultAdapter.errorDetails; assert.calledOnce(extractErrorDetails); assert.deepEqual(firstErrDetails, secondErrDetails); @@ -189,29 +189,29 @@ describe('TestplaneTestAdapter', () => { details: {data: {foo: 'bar'}} } as any }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); // we need to get errorDetails to trigger getDetailsFileName to be called - testplaneTestAdapter.errorDetails; + TestplaneTestResultAdapter.errorDetails; - assert.calledWith(getDetailsFileName, 'abcdef', 'bro', testplaneTestAdapter.attempt); + assert.calledWith(getDetailsFileName, 'abcdef', 'bro', TestplaneTestResultAdapter.attempt); }); }); it('should return image dir', () => { const testResult = mkTestResult_({id: 'some-id'}); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.imageDir, 'some-id'); + assert.deepEqual(TestplaneTestResultAdapter.imageDir, 'some-id'); }); it('should return description', () => { const testResult = mkTestResult_({description: 'some-description'}); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.description, 'some-description'); + assert.deepEqual(TestplaneTestResultAdapter.description, 'some-description'); }); describe('timestamp', () => { @@ -219,9 +219,9 @@ describe('TestplaneTestAdapter', () => { const testResult = mkTestResult_({ timestamp: 100500 }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.strictEqual(testplaneTestAdapter.timestamp, 100500); + assert.strictEqual(TestplaneTestResultAdapter.timestamp, 100500); }); }); @@ -243,9 +243,9 @@ describe('TestplaneTestAdapter', () => { }] }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.imagesInfo, [ + assert.deepEqual(TestplaneTestResultAdapter.imagesInfo, [ { status: TestStatus.FAIL, stateName: 'some-state', @@ -273,9 +273,9 @@ describe('TestplaneTestAdapter', () => { }] }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.imagesInfo, [ + assert.deepEqual(TestplaneTestResultAdapter.imagesInfo, [ { status: TestStatus.ERROR, stateName: 'some-state', @@ -301,9 +301,9 @@ describe('TestplaneTestAdapter', () => { } as TestplaneTestResult['assertViewResults'][number]] }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.imagesInfo, [ + assert.deepEqual(TestplaneTestResultAdapter.imagesInfo, [ { status: TestStatus.UPDATED, stateName: 'some-state', @@ -323,9 +323,9 @@ describe('TestplaneTestAdapter', () => { }] }); - const testplaneTestAdapter = mkTestplaneTestResultAdapter(testResult); + const TestplaneTestResultAdapter = mkTestplaneTestResultAdapter(testResult); - assert.deepEqual(testplaneTestAdapter.imagesInfo, [ + assert.deepEqual(TestplaneTestResultAdapter.imagesInfo, [ { status: TestStatus.SUCCESS, stateName: 'some-state', diff --git a/test/unit/lib/adapters/tool/testplane/index.ts b/test/unit/lib/adapters/tool/testplane/index.ts new file mode 100644 index 000000000..a69dd1f3e --- /dev/null +++ b/test/unit/lib/adapters/tool/testplane/index.ts @@ -0,0 +1,433 @@ +import Testplane, {type TestCollection} from 'testplane'; +import proxyquire from 'proxyquire'; +import sinon, {SinonStub} from 'sinon'; +import P from 'bluebird'; +import type {CommanderStatic} from '@gemini-testing/commander'; + +import {HtmlReporter} from '../../../../../../lib/plugin-api'; +import {ToolName} from '../../../../../../lib/constants'; +import {GuiApi} from '../../../../../../lib/gui/api'; +import {GuiReportBuilder} from '../../../../../../lib/report-builder/gui'; +import {EventSource} from '../../../../../../lib/gui/event-source'; + +import {stubTool, stubConfig, stubTestCollection} from '../../../../utils'; +import type {ReporterConfig} from '../../../../../../lib/types'; +import type {TestSpec} from '../../../../../../lib/adapters/tool/types'; + +describe('lib/adapters/tool/testplane/index', () => { + const sandbox = sinon.sandbox.create(); + let TestplaneToolAdapter: typeof import('../../../../../../lib/adapters/tool/testplane').TestplaneToolAdapter; + let parseConfigStub: SinonStub; + let createTestRunnerStub: SinonStub; + let handleTestResultsStub: SinonStub; + + beforeEach(() => { + sandbox.stub(Testplane, 'create').returns(stubTool()); + sandbox.stub(HtmlReporter, 'create').returns({}); + sandbox.stub(GuiApi, 'create').returns({}); + + parseConfigStub = sandbox.stub(); + createTestRunnerStub = sandbox.stub(); + handleTestResultsStub = sandbox.stub(); + + TestplaneToolAdapter = proxyquire('../../../../../../lib/adapters/tool/testplane', { + '../../../config': {parseConfig: parseConfigStub}, + './runner': {createTestRunner: createTestRunnerStub}, + './test-results-handler': {handleTestResults: handleTestResultsStub} + }).TestplaneToolAdapter; + }); + + afterEach(() => { + sandbox.restore(); + + delete process.env['html_reporter_enabled']; + }); + + describe('constructor', () => { + describe('used from cli', () => { + it('should disable html reporter plugin in order to not generate static report', () => { + TestplaneToolAdapter.create({toolName: ToolName.Testplane}); + + assert.equal(process.env['html_reporter_enabled'], 'false'); + }); + + it('should create testplane instance with path to config file', () => { + const configPath = '/some/config/path'; + + TestplaneToolAdapter.create({toolName: ToolName.Testplane, configPath}); + + assert.calledOnceWith(Testplane.create as SinonStub, configPath); + }); + + [ToolName.Testplane, 'hermione'].forEach(toolName => { + it(`should parse reporter opts from "html-reporter/${toolName}" plugin in config`, () => { + const pluginOpts = { + enabled: true, + path: 'hermione-report' + }; + const config = stubConfig({plugins: { + [`html-reporter/${toolName}`]: pluginOpts + }}); + const testplane = stubTool(config); + (Testplane.create as SinonStub).returns(testplane); + + TestplaneToolAdapter.create({toolName: ToolName.Testplane}); + + assert.calledOnceWith(parseConfigStub, pluginOpts); + }); + }); + + it('should use default opts if html-reporter plugin is not found in config', () => { + const config = stubConfig({plugins: {}}); + const testplane = stubTool(config); + (Testplane.create as SinonStub).returns(testplane); + + TestplaneToolAdapter.create({toolName: ToolName.Testplane}); + + assert.calledOnceWith(parseConfigStub, {}); + }); + + it('should init htmlReporter instance with parsed reporter config', () => { + const reporterConfig = {path: 'some/path'} as ReporterConfig; + parseConfigStub.returns(reporterConfig); + + TestplaneToolAdapter.create({toolName: ToolName.Testplane}); + + assert.calledOnceWith(HtmlReporter.create as SinonStub, reporterConfig, {toolName: ToolName.Testplane}); + }); + + it('should set "htmlReporter field in testplane to use from other plugins', () => { + const testplane = stubTool(); + const htmlReporter = sinon.createStubInstance(HtmlReporter); + (Testplane.create as SinonStub).returns(testplane); + (HtmlReporter.create as SinonStub).returns(htmlReporter); + + TestplaneToolAdapter.create({toolName: ToolName.Testplane}); + + assert.equal(testplane.htmlReporter, htmlReporter); + }); + }); + + describe('used from plugin', () => { + it('should use passed testplane instance', () => { + const reporterConfig = {path: 'some/path'} as ReporterConfig; + + TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: stubTool(), reporterConfig}); + + assert.notCalled(Testplane.create as SinonStub); + }); + + it('should init htmlReporter instance with passed reporter config', () => { + const reporterConfig = {path: 'some/path'} as ReporterConfig; + + TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: stubTool(), reporterConfig}); + + assert.calledOnceWith(HtmlReporter.create as SinonStub, reporterConfig, {toolName: ToolName.Testplane}); + }); + + it('should set "htmlReporter field in testplane to use from other plugins', () => { + const testplane = stubTool(); + const htmlReporter = sinon.createStubInstance(HtmlReporter); + (HtmlReporter.create as SinonStub).returns(htmlReporter); + + TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + assert.equal(testplane.htmlReporter, htmlReporter); + }); + }); + }); + + describe('initGuiApi', () => { + it('should set "gui" field in testplane to use from other plugins', () => { + const testplane = stubTool(); + const gui = {}; + (GuiApi.create as SinonStub).returns({gui}); + + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + toolAdapter.initGuiApi(); + + assert.equal(testplane.gui, gui); + }); + }); + + describe('readTests', () => { + it('should correctly pass "paths" to the tests', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.readTests(['foo', 'bar'], {} as CommanderStatic); + + assert.calledOnceWith(testplane.readTests, ['foo', 'bar']); + }); + + it('should correctly pass cli options', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + const cliTool = {grep: 'foo', set: 'bar', browser: 'yabro'} as unknown as CommanderStatic; + + await toolAdapter.readTests([], cliTool); + + assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ + grep: cliTool.grep, + sets: cliTool.set, + browsers: cliTool.browser + })); + }); + + describe('"replMode" option', () => { + it('should be disabled by default', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.readTests([], {} as CommanderStatic); + + assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ + replMode: { + enabled: false, + beforeTest: false, + onFail: false + } + })); + }); + + it('should be enabled when specify "repl" flag', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.readTests([], {repl: true} as unknown as CommanderStatic); + + assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ + replMode: { + enabled: true, + beforeTest: false, + onFail: false + } + })); + }); + + it('should be enabled when specify "beforeTest" flag', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.readTests([], {replBeforeTest: true} as unknown as CommanderStatic); + + assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ + replMode: { + enabled: true, + beforeTest: true, + onFail: false + } + })); + }); + + it('should be enabled when specify "onFail" flag', async () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.readTests([], {replOnFail: true} as unknown as CommanderStatic); + + assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ + replMode: { + enabled: true, + beforeTest: false, + onFail: true + } + })); + }); + }); + }); + + describe('run', () => { + let collection: TestCollection; + let runner: {run: SinonStub}; + + const run_ = async (testplane: Testplane, collection: TestCollection, tests: TestSpec[], cliTool: CommanderStatic): Promise => { + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + + await toolAdapter.run(collection, tests, cliTool); + + const runHandler = runner.run.firstCall.args[0]; + await runHandler(collection); + }; + + beforeEach(() => { + collection = stubTestCollection() as TestCollection; + runner = {run: sandbox.stub().resolves()}; + + createTestRunnerStub.withArgs(collection, []).returns(runner); + }); + + it('should run testplane with passed opts', async () => { + const testplane = stubTool(); + const tests = [] as TestSpec[]; + const cliTool = {grep: /some-grep/, set: 'some-set', browser: 'yabro', devtools: true} as unknown as CommanderStatic; + + await run_(testplane, collection, tests, cliTool); + + assert.calledOnceWith(testplane.run, collection, sinon.match({ + grep: cliTool.grep, + sets: cliTool.set, + browsers: cliTool.browser, + devtools: cliTool.devtools + })); + }); + + describe('"replMode" option', () => { + it('should be disabled by default', async () => { + const testplane = stubTool(); + const tests = [] as TestSpec[]; + const cliTool = {} as unknown as CommanderStatic; + + await run_(testplane, collection, tests, cliTool); + + assert.calledOnceWith(testplane.run, collection, sinon.match({ + replMode: { + enabled: false, + beforeTest: false, + onFail: false + } + })); + }); + + it('should be enabled when specify "repl" flag', async () => { + const testplane = stubTool(); + const tests = [] as TestSpec[]; + const cliTool = {repl: true} as unknown as CommanderStatic; + + await run_(testplane, collection, tests, cliTool); + + assert.calledOnceWith(testplane.run, collection, sinon.match({ + replMode: { + enabled: true, + beforeTest: false, + onFail: false + } + })); + }); + + it('should be enabled when specify "beforeTest" flag', async () => { + const testplane = stubTool(); + const tests = [] as TestSpec[]; + const cliTool = {replBeforeTest: true} as unknown as CommanderStatic; + + await run_(testplane, collection, tests, cliTool); + + assert.calledOnceWith(testplane.run, collection, sinon.match({ + replMode: { + enabled: true, + beforeTest: true, + onFail: false + } + })); + }); + + it('should be enabled when specify "onFail" flag', async () => { + const testplane = stubTool(); + const tests = [] as TestSpec[]; + const cliTool = {replOnFail: true} as unknown as CommanderStatic; + + await run_(testplane, collection, tests, cliTool); + + assert.calledOnceWith(testplane.run, collection, sinon.match({ + replMode: { + enabled: true, + beforeTest: false, + onFail: true + } + })); + }); + }); + }); + + describe('updateReference', () => { + it('should emit "UPDATE_REFERENCE" event with passed options', () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + const onUpdateReference = sinon.spy(); + const updateOpts = { + refImg: {path: '/ref/path', size: {height: 100, width: 200}}, + state: 'plain' + }; + + testplane.on(testplane.events.UPDATE_REFERENCE, onUpdateReference); + toolAdapter.updateReference(updateOpts); + + assert.calledOnceWith(onUpdateReference, updateOpts); + }); + }); + + describe('handleTestResults', () => { + it('should call test results handler with correct args', () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + const reportBuilder = {} as GuiReportBuilder; + const eventSource = {} as EventSource; + + toolAdapter.handleTestResults(reportBuilder, eventSource); + + assert.calledOnceWith(handleTestResultsStub, testplane, reportBuilder, eventSource); + }); + }); + + describe('halt', () => { + it('should halt testplane', () => { + const testplane = stubTool(); + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: {} as ReporterConfig}); + const error = new Error('o.O'); + const timeout = 100500; + + toolAdapter.halt(error, timeout); + + assert.calledOnceWith(testplane.halt, error, timeout); + }); + }); + + describe('initGuiHandler', () => { + it('should initialize each group of controls if initialize-function is available', async () => { + const initializeSpy1 = sinon.spy(); + const initializeSpy2 = sinon.spy(); + + const initialize1 = sinon.stub().callsFake(() => P.delay(5).then(initializeSpy1)); + const initialize2 = sinon.stub().callsFake(() => P.delay(10).then(initializeSpy2)); + + const ctx1 = {initialize: initialize1}; + const ctx2 = {initialize: initialize2}; + + const reporterConfig = { + customGui: {'section-1': [ctx1], 'section-2': [ctx2]} + } as unknown as ReporterConfig; + const testplane = stubTool(); + + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig}); + + await toolAdapter.initGuiHandler(); + + assert.calledOnceWith(initialize1, {testplane, hermione: testplane, ctx: ctx1}); + assert.calledOnceWith(initialize2, {testplane, hermione: testplane, ctx: ctx2}); + assert.callOrder(initializeSpy1, initializeSpy2); + }); + }); + + describe('runCustomGuiAction', () => { + it('should run action for specified controls', async () => { + const actionSpy = sinon.spy(); + const action = sinon.stub().callsFake(() => P.delay(10).then(actionSpy)); + + const control = {}; + const ctx = {controls: [control], action}; + const reporterConfig = {customGui: {'section': [ctx]}} as unknown as ReporterConfig; + const testplane = stubTool(); + + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig}); + + await toolAdapter.runCustomGuiAction({ + sectionName: 'section', + groupIndex: 0, + controlIndex: 0 + }); + + assert.calledOnceWith(action, {testplane, hermione: testplane, ctx, control}); + assert.calledOnce(actionSpy); + }); + }); +}); diff --git a/test/unit/lib/gui/tool-runner/report-subsciber.js b/test/unit/lib/adapters/tool/testplane/test-results-handler.js similarity index 86% rename from test/unit/lib/gui/tool-runner/report-subsciber.js rename to test/unit/lib/adapters/tool/testplane/test-results-handler.js index caf3fc665..f68ef9b6a 100644 --- a/test/unit/lib/gui/tool-runner/report-subsciber.js +++ b/test/unit/lib/adapters/tool/testplane/test-results-handler.js @@ -3,14 +3,14 @@ const {EventEmitter} = require('events'); const Promise = require('bluebird'); const _ = require('lodash'); -const {subscribeOnToolEvents} = require('lib/gui/tool-runner/report-subscriber'); +const {handleTestResults} = require('lib/adapters/tool/testplane/test-results-handler'); const {GuiReportBuilder} = require('lib/report-builder/gui'); const {ClientEvents} = require('lib/gui/constants'); const {stubTool, stubConfig} = require('test/unit/utils'); -const {TestplaneTestAdapter} = require('lib/test-adapter/testplane'); +const {TestplaneTestResultAdapter} = require('lib/adapters/test-result/testplane'); const {UNKNOWN_ATTEMPT} = require('lib/constants'); -describe('lib/gui/tool-runner/testplane/report-subscriber', () => { +describe('lib/adapters/tool/testplane/test-results-handler', () => { const sandbox = sinon.createSandbox(); let reportBuilder; let client; @@ -37,7 +37,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { reportBuilder.addTestResult.callsFake(_.identity); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); - sandbox.stub(TestplaneTestAdapter.prototype, 'id').value('some-id'); + sandbox.stub(TestplaneTestResultAdapter.prototype, 'id').value('some-id'); client = new EventEmitter(); sandbox.spy(client, 'emit'); @@ -49,7 +49,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { it('should emit "END" event for client', () => { const testplane = mkTestplane_(); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); return testplane.emitAsync(testplane.events.RUNNER_END) .then(() => assert.calledOnceWith(client.emit, ClientEvents.END)); @@ -62,7 +62,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { reportBuilder.addTestResult.callsFake(() => Promise.delay(100).then(mediator).then(() => ({id: 'some-id'}))); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); testplane.emit(testplane.events.TEST_FAIL, testResult); await testplane.emitAsync(testplane.events.RUNNER_END); @@ -78,7 +78,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { reportBuilder.addTestResult.resolves({id: 'some-id'}); reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); testplane.emit(testplane.events.TEST_BEGIN, testResult); await testplane.emitAsync(testplane.events.RUNNER_END); @@ -91,7 +91,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { const testplane = mkTestplane_(); const testResult = mkTestplaneTestResult(); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); await testplane.emitAsync(testplane.events.TEST_PENDING, testResult); await testplane.emitAsync(testplane.events.RUNNER_END); @@ -108,7 +108,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); await testplane.emitAsync(testplane.events.TEST_PENDING, testResult); await testplane.emitAsync(testplane.events.RUNNER_END); @@ -123,7 +123,7 @@ describe('lib/gui/tool-runner/testplane/report-subscriber', () => { reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); - subscribeOnToolEvents(testplane, reportBuilder, client); + handleTestResults(testplane, reportBuilder, client); testplane.emit(testplane.events.TEST_FAIL, testResult); await testplane.emitAsync(testplane.events.RUNNER_END); diff --git a/test/unit/lib/cli-commands/remove-unused-screens/index.js b/test/unit/lib/cli/commands/remove-unused-screens/index.js similarity index 84% rename from test/unit/lib/cli-commands/remove-unused-screens/index.js rename to test/unit/lib/cli/commands/remove-unused-screens/index.js index fc904d9a5..d20ef9b93 100644 --- a/test/unit/lib/cli-commands/remove-unused-screens/index.js +++ b/test/unit/lib/cli/commands/remove-unused-screens/index.js @@ -1,15 +1,14 @@ 'use strict'; const path = require('path'); -const _ = require('lodash'); const fs = require('fs-extra'); const proxyquire = require('proxyquire'); const chalk = require('chalk'); const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('lib/constants/database'); -const {stubTool} = require('test/unit/utils'); const {logger} = require('lib/common-utils'); +const {stubToolAdapter, stubReporterConfig} = require('test/unit/utils'); -describe('lib/cli-commands/remove-unused-screens', () => { +describe('lib/cli/commands/remove-unused-screens', () => { const sandbox = sinon.sandbox.create(); let actionPromise, removeUnusedScreens, getTestsFromFs, findScreens, askQuestion; let identifyOutdatedScreens, identifyUnusedScreens, removeScreens, filesizeMock; @@ -25,17 +24,13 @@ describe('lib/cli-commands/remove-unused-screens', () => { }) }); - const mkPluginCfg_ = (opts) => _.defaults(opts, {path: 'default-path'}); + const mkToolAdapter_ = ({htmlReporter, reporterConfig} = {}) => { + const toolAdapter = stubToolAdapter({reporterConfig, htmlReporter}); - const mkTestplane_ = (htmlReporter) => { - const testplane = stubTool(); + toolAdapter.htmlReporter.downloadDatabases = sandbox.stub().resolves(['/default-path/sqlite.db']); + toolAdapter.htmlReporter.mergeDatabases = sandbox.stub().resolves(); - testplane.htmlReporter = htmlReporter || { - downloadDatabases: sandbox.stub().resolves(['/default-path/sqlite.db']), - mergeDatabases: sandbox.stub().resolves() - }; - - return testplane; + return toolAdapter; }; const mkTestsTreeFromFs_ = (opts = {}) => { @@ -44,10 +39,9 @@ describe('lib/cli-commands/remove-unused-screens', () => { const removeUnusedScreens_ = async ({ program = mkProgram_({pattern: ['default/pattern'], skipQuestions: false}), - pluginConfig = mkPluginCfg_(), - testplane = mkTestplane_() + toolAdapter = mkToolAdapter_() } = {}) => { - removeUnusedScreens(program, pluginConfig, testplane); + removeUnusedScreens(program, toolAdapter); await actionPromise; }; @@ -71,7 +65,7 @@ describe('lib/cli-commands/remove-unused-screens', () => { filesizeMock = sandbox.stub().returns('12345'); - removeUnusedScreens = proxyquire('lib/cli-commands/remove-unused-screens', { + removeUnusedScreens = proxyquire('lib/cli/commands/remove-unused-screens', { ora: () => ({start: sandbox.stub(), succeed: sandbox.stub()}), filesize: filesizeMock, './utils': {getTestsFromFs, findScreens, askQuestion, identifyOutdatedScreens, identifyUnusedScreens, removeScreens} @@ -101,11 +95,11 @@ describe('lib/cli-commands/remove-unused-screens', () => { }); it('should get tests tree from fs', async () => { - const testplane = mkTestplane_(); + const toolAdapter = mkToolAdapter_(); - await removeUnusedScreens_({testplane}); + await removeUnusedScreens_({toolAdapter}); - assert.calledOnceWith(getTestsFromFs, testplane); + assert.calledOnceWith(getTestsFromFs, toolAdapter); }); it('should inform user about the number of tests read', async () => { @@ -346,49 +340,49 @@ describe('lib/cli-commands/remove-unused-screens', () => { }); it('should not handle unused images if user say "no"', async () => { - const testplane = mkTestplane_(); + const toolAdapter = mkToolAdapter_(); const program = mkProgram_({pattern: ['some/pattern'], skipQuestions: false}); askQuestion.withArgs(sinon.match({name: 'identifyUnused'}), program.options).resolves(false); - await removeUnusedScreens_({testplane, program}); + await removeUnusedScreens_({toolAdapter, program}); - assert.notCalled(testplane.htmlReporter.downloadDatabases); + assert.notCalled(toolAdapter.htmlReporter.downloadDatabases); }); describe('if user say "yes"', () => { - let testplane, program, pluginConfig; + let toolAdapter, program, reporterConfig; beforeEach(() => { - testplane = mkTestplane_(); + reporterConfig = stubReporterConfig({path: './testplane-report'}); + toolAdapter = mkToolAdapter_({reporterConfig}); program = mkProgram_({pattern: ['some/pattern'], skipQuestions: false}); - pluginConfig = mkPluginCfg_({path: './testplane-report'}); askQuestion.withArgs(sinon.match({name: 'identifyUnused'}), program.options).resolves(true); }); it('should inform user if report with the result of test run is missing on fs', async () => { - fs.pathExists.withArgs(pluginConfig.path).resolves(false); + fs.pathExists.withArgs(reporterConfig.path).resolves(false); - await removeUnusedScreens_({program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); - assert.calledWith(logger.error, sinon.match(`Can't find html-report in "${pluginConfig.path}" folder`)); + assert.calledWith(logger.error, sinon.match(`Can't find html-report in "${reporterConfig.path}" folder`)); assert.calledOnceWith(process.exit, 1); }); describe('download databases', () => { it('should download from main databaseUrls.json', async () => { - const mainDatabaseUrls = path.resolve(pluginConfig.path, DATABASE_URLS_JSON_NAME); + const mainDatabaseUrls = path.resolve(reporterConfig.path, DATABASE_URLS_JSON_NAME); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); - assert.calledOnceWith(testplane.htmlReporter.downloadDatabases, [mainDatabaseUrls], {pluginConfig}); + assert.calledOnceWith(toolAdapter.htmlReporter.downloadDatabases, [mainDatabaseUrls], {pluginConfig: reporterConfig}); }); it(`should inform user if databases were not loaded from "${DATABASE_URLS_JSON_NAME}"`, async () => { - const mainDatabaseUrls = path.resolve(pluginConfig.path, DATABASE_URLS_JSON_NAME); - testplane.htmlReporter.downloadDatabases.resolves([]); + const mainDatabaseUrls = path.resolve(reporterConfig.path, DATABASE_URLS_JSON_NAME); + toolAdapter.htmlReporter.downloadDatabases.resolves([]); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); assert.calledWith(logger.error, sinon.match(`Databases were not loaded from "${mainDatabaseUrls}" file`)); assert.calledOnceWith(process.exit, 1); @@ -397,32 +391,32 @@ describe('lib/cli-commands/remove-unused-screens', () => { describe('merge databases', () => { it('should not merge databases if downloaded only main database', async () => { - const mainDatabasePath = path.resolve(pluginConfig.path, LOCAL_DATABASE_NAME); - testplane.htmlReporter.downloadDatabases.resolves([mainDatabasePath]); + const mainDatabasePath = path.resolve(reporterConfig.path, LOCAL_DATABASE_NAME); + toolAdapter.htmlReporter.downloadDatabases.resolves([mainDatabasePath]); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); - assert.notCalled(testplane.htmlReporter.mergeDatabases); + assert.notCalled(toolAdapter.htmlReporter.mergeDatabases); }); it('should merge source databases to main', async () => { - const srcDb1 = path.resolve(pluginConfig.path, 'sqlite_1.db'); - const srcDb2 = path.resolve(pluginConfig.path, 'sqlite_2.db'); - const mainDatabasePath = path.resolve(pluginConfig.path, LOCAL_DATABASE_NAME); - testplane.htmlReporter.downloadDatabases.resolves([mainDatabasePath, srcDb1, srcDb2]); + const srcDb1 = path.resolve(reporterConfig.path, 'sqlite_1.db'); + const srcDb2 = path.resolve(reporterConfig.path, 'sqlite_2.db'); + const mainDatabasePath = path.resolve(reporterConfig.path, LOCAL_DATABASE_NAME); + toolAdapter.htmlReporter.downloadDatabases.resolves([mainDatabasePath, srcDb1, srcDb2]); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); - assert.calledOnceWith(testplane.htmlReporter.mergeDatabases, [srcDb1, srcDb2], pluginConfig.path); + assert.calledOnceWith(toolAdapter.htmlReporter.mergeDatabases, [srcDb1, srcDb2], reporterConfig.path); }); it('should infrom user about how much databases were merged', async () => { - const srcDb1 = path.resolve(pluginConfig.path, 'sqlite_1.db'); - const srcDb2 = path.resolve(pluginConfig.path, 'sqlite_2.db'); - const mainDatabasePath = path.resolve(pluginConfig.path, LOCAL_DATABASE_NAME); - testplane.htmlReporter.downloadDatabases.resolves([mainDatabasePath, srcDb1, srcDb2]); + const srcDb1 = path.resolve(reporterConfig.path, 'sqlite_1.db'); + const srcDb2 = path.resolve(reporterConfig.path, 'sqlite_2.db'); + const mainDatabasePath = path.resolve(reporterConfig.path, LOCAL_DATABASE_NAME); + toolAdapter.htmlReporter.downloadDatabases.resolves([mainDatabasePath, srcDb1, srcDb2]); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); assert.calledWith( logger.log, @@ -436,11 +430,11 @@ describe('lib/cli-commands/remove-unused-screens', () => { byId: {a: {}, b: {}, c: {}} }); getTestsFromFs.resolves(testsTreeFromFs); - const mergedDbPath = path.resolve(pluginConfig.path, LOCAL_DATABASE_NAME); + const mergedDbPath = path.resolve(reporterConfig.path, LOCAL_DATABASE_NAME); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); - assert.calledOnceWith(identifyUnusedScreens, testsTreeFromFs, {testplane, mergedDbPath}); + assert.calledOnceWith(identifyUnusedScreens, testsTreeFromFs, {toolAdapter, mergedDbPath}); }); it('should not throw if unused screen does not exist on fs', async () => { @@ -451,7 +445,7 @@ describe('lib/cli-commands/remove-unused-screens', () => { accessError.code = 'ENOENT'; fs.access.withArgs('/root/broId/unusedTestId/2.png').rejects(accessError); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); assert.calledOnceWith(logger.warn, sinon.match('Screen by path: "/root/broId/unusedTestId/2.png" is not found in your file system')); assert.calledWith(logger.log, `Found ${chalk.green('0')} unused reference images out of ${chalk.bold('2')}`); @@ -461,7 +455,7 @@ describe('lib/cli-commands/remove-unused-screens', () => { findScreens.resolves(['/root/usedTestId/img.png', '/root/unusedTestId/img.png']); identifyUnusedScreens.returns(['/root/unusedTestId/img.png']); - await removeUnusedScreens_({testplane, program, pluginConfig}); + await removeUnusedScreens_({toolAdapter, program}); assert.calledWith(logger.log, `Found ${chalk.red('1')} unused reference images out of ${chalk.bold('2')}`); }); diff --git a/test/unit/lib/cli-commands/remove-unused-screens/utils.js b/test/unit/lib/cli/commands/remove-unused-screens/utils.js similarity index 86% rename from test/unit/lib/cli-commands/remove-unused-screens/utils.js rename to test/unit/lib/cli/commands/remove-unused-screens/utils.js index 830a499d9..3dc922349 100644 --- a/test/unit/lib/cli-commands/remove-unused-screens/utils.js +++ b/test/unit/lib/cli/commands/remove-unused-screens/utils.js @@ -6,9 +6,9 @@ const proxyquire = require('proxyquire'); const inquirer = require('inquirer'); const {SUCCESS, ERROR} = require('lib/constants/test-statuses'); -const {stubTool, stubConfig, mkState} = require('test/unit/utils'); +const {stubToolAdapter, stubConfig, mkState} = require('test/unit/utils'); -describe('lib/cli-commands/remove-unused-screens/utils', () => { +describe('lib/cli/commands/remove-unused-screens/utils', () => { const sandbox = sinon.sandbox.create(); let utils, fgMock; @@ -36,14 +36,15 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { }; }; - const mkTestplane_ = (testCollection = mkTestCollection_(), config = stubConfig(), htmlReporter) => { - const testplane = stubTool(config); - testplane.readTests.resolves(testCollection); - testplane.htmlReporter = htmlReporter || { + const mkToolAdapter_ = (testCollection = mkTestCollection_(), config = stubConfig(), htmlReporter) => { + const toolAdapter = stubToolAdapter({config}); + + toolAdapter.readTests.resolves(testCollection); + toolAdapter.htmlReporter = htmlReporter || { getTestsTreeFromDatabase: sandbox.stub().returns(mkTestsTreeFromDb_()) }; - return testplane; + return toolAdapter; }; beforeEach(() => { @@ -52,7 +53,7 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { fgMock = sandbox.stub().resolves([]); - utils = proxyquire('lib/cli-commands/remove-unused-screens/utils', { + utils = proxyquire('lib/cli/commands/remove-unused-screens/utils', { 'fast-glob': fgMock }); }); @@ -66,12 +67,12 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { getScreenshotPath = sandbox.stub().returns('/default/path/*.png'); }); - it('should read all testplane tests silently', async () => { - const testplane = mkTestplane_(); + it('should read all tests silently', async () => { + const toolAdapter = mkToolAdapter_(); - await utils.getTestsFromFs(testplane); + await utils.getTestsFromFs(toolAdapter); - assert.calledOnceWith(testplane.readTests, [], {silent: true}); + assert.calledOnceWith(toolAdapter.readTests, [], {silent: true}); }); it('should add test tests tree with its screen info', async () => { @@ -82,9 +83,9 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { fgMock.withArgs('/ref/path/*.png').resolves(['/ref/path/1.png', '/ref/path/2.png']); - const testplane = mkTestplane_(testCollection, config); + const toolAdapter = mkToolAdapter_(testCollection, config); - const tests = await utils.getTestsFromFs(testplane); + const tests = await utils.getTestsFromFs(toolAdapter); assert.lengthOf(Object.keys(tests.byId), 1); assert.deepEqual( @@ -106,9 +107,9 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { .withArgs(test2, '*').returns('/ref/path-2/*.png'); const config = stubConfig({browsers: {bro1: {getScreenshotPath}, bro2: {getScreenshotPath}}}); - const testplane = mkTestplane_(testCollection, config); + const toolAdapter = mkToolAdapter_(testCollection, config); - const tests = await utils.getTestsFromFs(testplane); + const tests = await utils.getTestsFromFs(toolAdapter); assert.deepEqual(tests.screenPatterns, ['/ref/path-1/*.png', '/ref/path-2/*.png']); }); @@ -122,9 +123,9 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { bro1: {getScreenshotPath}, bro2: {getScreenshotPath}, bro3: {getScreenshotPath}, bro4: {getScreenshotPath} }}); - const testplane = mkTestplane_(testCollection, config); + const toolAdapter = mkToolAdapter_(testCollection, config); - const tests = await utils.getTestsFromFs(testplane); + const tests = await utils.getTestsFromFs(toolAdapter); assert.equal(tests.count, 2); }); @@ -136,9 +137,9 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { const config = stubConfig({browsers: { bro1: {getScreenshotPath}, bro2: {getScreenshotPath}, bro3: {getScreenshotPath} }}); - const testplane = mkTestplane_(testCollection, config); + const toolAdapter = mkToolAdapter_(testCollection, config); - const tests = await utils.getTestsFromFs(testplane); + const tests = await utils.getTestsFromFs(toolAdapter); assert.deepEqual(tests.browserIds, new Set(['bro1', 'bro2', 'bro3'])); }); @@ -261,8 +262,8 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { const imagesById = mkImage({id: 'img-1', stateName: 'a'}); const dbTree = mkTestsTreeFromDb_({browsersById, resultsById, imagesById}); - const testplane = mkTestplane_(); - testplane.htmlReporter.getTestsTreeFromDatabase.withArgs('/report/sqlite.db').returns(dbTree); + const toolAdapter = mkToolAdapter_(); + toolAdapter.htmlReporter.getTestsTreeFromDatabase.withArgs('/report/sqlite.db').returns(dbTree); const fsTestsTree = mkTestsTreeFromFs_({ byId: { @@ -272,7 +273,7 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { const unusedScreens = utils.identifyUnusedScreens( fsTestsTree, - {testplane, mergedDbPath: '/report/sqlite.db'} + {toolAdapter, mergedDbPath: '/report/sqlite.db'} ); assert.deepEqual(unusedScreens, ['/test1/b.png']); @@ -284,8 +285,8 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { const imagesById = mkImage({id: 'img-1', stateName: 'a'}); const dbTree = mkTestsTreeFromDb_({browsersById, resultsById, imagesById}); - const testplane = mkTestplane_(); - testplane.htmlReporter.getTestsTreeFromDatabase.withArgs('/report/sqlite.db').returns(dbTree); + const toolAdapter = mkToolAdapter_(); + toolAdapter.htmlReporter.getTestsTreeFromDatabase.withArgs('/report/sqlite.db').returns(dbTree); const fsTestsTree = mkTestsTreeFromFs_({ byId: { @@ -295,7 +296,7 @@ describe('lib/cli-commands/remove-unused-screens/utils', () => { const unusedScreens = utils.identifyUnusedScreens( fsTestsTree, - {testplane, mergedDbPath: '/report/sqlite.db'} + {toolAdapter, mergedDbPath: '/report/sqlite.db'} ); assert.isEmpty(unusedScreens); diff --git a/test/unit/lib/gui/api/index.js b/test/unit/lib/gui/api/index.js index 87e5ee07a..252e7f180 100644 --- a/test/unit/lib/gui/api/index.js +++ b/test/unit/lib/gui/api/index.js @@ -2,34 +2,28 @@ const EventEmitter2 = require('eventemitter2'); const {GuiEvents} = require('lib/gui/constants/gui-events'); -const {Api} = require('lib/gui/api'); -const {stubTool} = require('../../../utils'); +const {GuiApi} = require('lib/gui/api'); -describe('lig/gui/api', () => { +describe('lib/gui/api', () => { describe('constructor', () => { - it('should extend tool with gui api', () => { - const tool = stubTool(); + it('should init gui api', () => { + const api = GuiApi.create(); - Api.create(tool); - - assert.instanceOf(tool.gui, EventEmitter2); + assert.instanceOf(api.gui, EventEmitter2); }); it('should add events to gui api', () => { - const tool = stubTool(); - - Api.create(tool); + const api = GuiApi.create(); - assert.deepEqual(tool.gui.events, GuiEvents); + assert.deepEqual(api.gui.events, GuiEvents); }); }); describe('initServer', () => { it('should emit "SERVER_INIT" event through gui api', () => { - const tool = stubTool(); - const api = Api.create(tool); + const api = GuiApi.create(); const onServerInit = sinon.spy().named('onServerInit'); - tool.gui.on(GuiEvents.SERVER_INIT, onServerInit); + api.gui.on(GuiEvents.SERVER_INIT, onServerInit); api.initServer({foo: 'bar'}); @@ -39,10 +33,9 @@ describe('lig/gui/api', () => { describe('serverReady', () => { it('should emit "SERVER_READY" event through gui api', () => { - const tool = stubTool(); - const api = Api.create(tool); + const api = GuiApi.create(); const onServerReady = sinon.spy().named('onServerReady'); - tool.gui.on(GuiEvents.SERVER_READY, onServerReady); + api.gui.on(GuiEvents.SERVER_READY, onServerReady); api.serverReady({url: 'http://my.server'}); diff --git a/test/unit/lib/gui/server.js b/test/unit/lib/gui/server.js index 7050f1f6e..6bbec02b8 100644 --- a/test/unit/lib/gui/server.js +++ b/test/unit/lib/gui/server.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); const {App} = require('lib/gui/app'); -const {stubTool} = require('../../utils'); +const {stubToolAdapter} = require('../../utils'); describe('lib/gui/server', () => { const sandbox = sinon.createSandbox(); @@ -23,19 +23,12 @@ describe('lib/gui/server', () => { static: sandbox.stub() }); - const mkApi_ = () => ({ - initServer: sandbox.stub(), - serverReady: sandbox.stub() - }); - const startServer = (opts = {}) => { opts = _.defaults(opts, { paths: [], - tool: stubTool(), - guiApi: mkApi_(), - configs: { - options: {hostname: 'localhost', port: '4444'}, - pluginConfig: {path: 'default-path'} + toolAdapter: stubToolAdapter(), + cli: { + options: {hostname: 'localhost', port: '4444'} } }); @@ -71,32 +64,36 @@ describe('lib/gui/server', () => { }); it('should init server from api', async () => { - const guiApi = mkApi_(); + const toolAdapter = stubToolAdapter(); + const {guiApi} = toolAdapter; - await startServer({guiApi}); + await startServer({toolAdapter}); assert.calledOnceWith(guiApi.initServer, expressStub); assert.calledOnceWith(guiApi.serverReady, {url: 'http://localhost:4444'}); }); it('should init server only after body is parsed', async () => { - const guiApi = mkApi_(); + const toolAdapter = stubToolAdapter(); + const {guiApi} = toolAdapter; - await startServer({guiApi}); + await startServer({toolAdapter}); assert.callOrder(bodyParserStub.json, guiApi.initServer, guiApi.serverReady); }); it('should init server before any static middleware starts', async () => { - const guiApi = mkApi_(); + const toolAdapter = stubToolAdapter(); + const {guiApi} = toolAdapter; - await startServer({guiApi}); + await startServer({toolAdapter}); assert.callOrder(guiApi.initServer, staticMiddleware, guiApi.serverReady); }); it('should properly complete app working', async () => { sandbox.stub(process, 'kill'); + sandbox.stub(process, 'exit'); await startServer(); @@ -106,28 +103,24 @@ describe('lib/gui/server', () => { }); it('should correctly set json replacer', async () => { - const guiApi = mkApi_(); + const toolAdapter = stubToolAdapter(); - await startServer({guiApi}); + await startServer({toolAdapter}); assert.calledOnceWith(expressStub.set, 'json replacer', sinon.match.func); }); it('should try to attach plugins middleware on startup', async () => { - const pluginConfig = { + const reporterConfig = { path: 'test-path', plugins: [ {name: 'test-plugin', component: 'TestComponent'} ] }; + const toolAdapter = stubToolAdapter({reporterConfig}); - await startServer({ - configs: { - options: {hostname: 'localhost', port: '4444'}, - pluginConfig - } - }); + await startServer({toolAdapter}); - assert.calledOnceWith(initPluginRoutesStub, RouterStub, pluginConfig); + assert.calledOnceWith(initPluginRoutesStub, RouterStub, reporterConfig); }); }); diff --git a/test/unit/lib/gui/tool-runner/index.js b/test/unit/lib/gui/tool-runner/index.js index 63b2ccbe8..9488cc353 100644 --- a/test/unit/lib/gui/tool-runner/index.js +++ b/test/unit/lib/gui/tool-runner/index.js @@ -7,7 +7,7 @@ const proxyquire = require('proxyquire'); const {GuiReportBuilder} = require('lib/report-builder/gui'); const {LOCAL_DATABASE_NAME} = require('lib/constants/database'); const {logger} = require('lib/common-utils'); -const {stubTool, stubConfig, mkImagesInfo, mkState, mkSuite} = require('test/unit/utils'); +const {stubToolAdapter, stubConfig, stubReporterConfig, mkImagesInfo, mkState, mkSuite} = require('test/unit/utils'); const {SqliteClient} = require('lib/sqlite-client'); const {PluginEvents, TestStatus, UPDATED} = require('lib/constants'); const {Cache} = require('lib/cache'); @@ -16,12 +16,10 @@ describe('lib/gui/tool-runner/index', () => { const sandbox = sinon.createSandbox(); let reportBuilder; let ToolGuiReporter; - let subscribeOnToolEvents; - let testplane; + let toolAdapter; let getTestsTreeFromDatabase; let looksSame; let toolRunnerUtils; - let createTestRunner; let getReferencePath; let reporterHelpers; @@ -40,37 +38,21 @@ describe('lib/gui/tool-runner/index', () => { })); }; - const mkToolCliOpts_ = (globalCliOpts = {name: () => 'testplane'}, guiCliOpts = {}) => { - return {program: globalCliOpts, options: guiCliOpts}; - }; - const mkPluginConfig_ = (config = {}) => { - const pluginConfig = _.defaults(config, {path: 'default-path'}); - return {pluginConfig}; - }; - - const mkTestplane_ = (config, testsTree) => { - const testplane = stubTool(config, {UPDATE_REFERENCE: 'updateReference'}); - sandbox.stub(testplane, 'emit'); - testplane.readTests.resolves(mkTestCollection_(testsTree)); - - return testplane; - }; - - const initGuiReporter = (testplane, opts = {}) => { + const initGuiReporter = (opts = {}) => { opts = _.defaults(opts, { + toolAdapter: stubToolAdapter(), paths: [], - configs: {} + cli: { + tool: {}, + options: {} + } }); - const configs = _.defaults(opts.configs, mkToolCliOpts_(), mkPluginConfig_()); - - return ToolGuiReporter.create(opts.paths, testplane, configs); + return ToolGuiReporter.create(opts); }; beforeEach(() => { - testplane = stubTool(); - - createTestRunner = sandbox.stub(); + toolAdapter = stubToolAdapter(); toolRunnerUtils = { findTestResult: sandbox.stub(), @@ -81,7 +63,6 @@ describe('lib/gui/tool-runner/index', () => { reportBuilder.addTestResult.callsFake(_.identity); reportBuilder.provideAttempt.callsFake(_.identity); - subscribeOnToolEvents = sandbox.stub().named('reportSubscriber').resolves(); looksSame = sandbox.stub().named('looksSame').resolves({equal: true}); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); @@ -99,7 +80,7 @@ describe('lib/gui/tool-runner/index', () => { fileExists: sandbox.stub(), deleteFile: sandbox.stub() }, - './test-adapter/utils': { + './adapters/test-result/utils': { copyAndUpdate: sandbox.stub().callsFake(_.assign) } }); @@ -107,8 +88,6 @@ describe('lib/gui/tool-runner/index', () => { ToolGuiReporter = proxyquire(`lib/gui/tool-runner`, { 'looks-same': looksSame, - './runner': {createTestRunner}, - './report-subscriber': {subscribeOnToolEvents}, './utils': toolRunnerUtils, '../../sqlite-client': {SqliteClient: {create: () => sandbox.createStubInstance(SqliteClient)}}, '../../db-utils/server': {getTestsTreeFromDatabase}, @@ -116,6 +95,8 @@ describe('lib/gui/tool-runner/index', () => { }).ToolRunner; sandbox.stub(logger, 'warn'); + + sandbox.stub(process, 'cwd').returns('/ref/cwd'); }); afterEach(() => sandbox.restore()); @@ -123,9 +104,9 @@ describe('lib/gui/tool-runner/index', () => { describe('initialize', () => { it('should set values added through api', () => { const htmlReporter = {emit: sandbox.stub(), values: {foo: 'bar'}, config: {}, imagesSaver: {}}; - testplane = stubTool(stubConfig(), {}, {}, htmlReporter); + toolAdapter = stubToolAdapter({htmlReporter}); - const gui = initGuiReporter(testplane); + const gui = initGuiReporter({toolAdapter}); return gui.initialize() .then(() => assert.calledWith(reportBuilder.setApiValues, {foo: 'bar'})); @@ -133,109 +114,34 @@ describe('lib/gui/tool-runner/index', () => { describe('correctly pass options to "readTests" method', () => { it('should pass "paths" option', () => { - const gui = initGuiReporter(testplane, {paths: ['foo', 'bar']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo', 'bar']}); return gui.initialize() - .then(() => assert.calledOnceWith(testplane.readTests, ['foo', 'bar'])); + .then(() => assert.calledOnceWith(toolAdapter.readTests, ['foo', 'bar'])); }); - it('should pass "grep", "sets" and "browsers" options', () => { - const grep = 'foo'; - const set = 'bar'; - const browser = 'yabro'; + it('should pass cli options', () => { + const cliTool = {grep: 'foo', set: 'bar', browser: 'yabro'}; - const gui = initGuiReporter(testplane, { - configs: { - program: {grep, set, browser} + const gui = initGuiReporter({ + toolAdapter, + cli: { + tool: cliTool, + options: {} } }); return gui.initialize() .then(() => { - assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({grep, sets: set, browsers: browser})); - }); - }); - - describe('"replMode" option', () => { - it('should be disabled by default', async () => { - const gui = initGuiReporter(testplane, { - configs: { - program: {} - } - }); - - await gui.initialize(); - - assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ - replMode: { - enabled: false, - beforeTest: false, - onFail: false - } - })); - }); - - it('should be enabled when specify "repl" flag', async () => { - const gui = initGuiReporter(testplane, { - configs: { - program: {repl: true} - } - }); - - await gui.initialize(); - - assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ - replMode: { - enabled: true, - beforeTest: false, - onFail: false - } - })); - }); - - it('should be enabled when specify "beforeTest" flag', async () => { - const gui = initGuiReporter(testplane, { - configs: { - program: {replBeforeTest: true} - } - }); - - await gui.initialize(); - - assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ - replMode: { - enabled: true, - beforeTest: true, - onFail: false - } - })); - }); - - it('should be enabled when specify "onFail" flag', async () => { - const gui = initGuiReporter(testplane, { - configs: { - program: {replOnFail: true} - } + assert.calledOnceWith(toolAdapter.readTests, sinon.match.any, cliTool); }); - - await gui.initialize(); - - assert.calledOnceWith(testplane.readTests, sinon.match.any, sinon.match({ - replMode: { - enabled: true, - beforeTest: false, - onFail: true - } - })); - }); }); }); it('should not add disabled test to report', () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_({disabled: true})})); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_({disabled: true})})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() .then(() => { @@ -244,10 +150,9 @@ describe('lib/gui/tool-runner/index', () => { }); it('should not add silently skipped test to report', () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_({silentSkip: true})})); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_({silentSkip: true})})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() .then(() => { @@ -256,12 +161,10 @@ describe('lib/gui/tool-runner/index', () => { }); it('should not add test from silently skipped suite to report', () => { - const testplane = stubTool(); const silentlySkippedSuite = mkSuite({silentSkip: true}); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_({parent: silentlySkippedSuite})})); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_({parent: silentlySkippedSuite})})); - - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() .then(() => { @@ -270,132 +173,169 @@ describe('lib/gui/tool-runner/index', () => { }); it('should add skipped test to report', () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_({pending: true})})); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_({pending: true})})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() .then(() => assert.calledOnce(reportBuilder.addTestResult)); }); it('should add idle test to report', () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_()})); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_()})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() .then(() => assert.calledOnce(reportBuilder.addTestResult)); }); - it('should subscribe on events before read tests', () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_()})); + it('should handle test results before read tests', () => { + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_()})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); return gui.initialize() - .then(() => assert.callOrder(subscribeOnToolEvents, testplane.readTests)); + .then(() => assert.callOrder(toolAdapter.handleTestResults, toolAdapter.readTests)); }); it('should initialize report builder after read tests for the correct order of events', async () => { - const testplane = stubTool(); - testplane.readTests.resolves(mkTestCollection_({bro: stubTest_()})); - const gui = initGuiReporter(testplane, {paths: ['foo']}); + toolAdapter.readTests.resolves(mkTestCollection_({bro: stubTest_()})); + const gui = initGuiReporter({toolAdapter, paths: ['foo']}); await gui.initialize(); - assert.callOrder(testplane.readTests, testplane.htmlReporter.emit); - assert.calledOnceWith(testplane.htmlReporter.emit, PluginEvents.DATABASE_CREATED, sinon.match.any); + assert.callOrder(toolAdapter.readTests, toolAdapter.htmlReporter.emit); + assert.calledOnceWith(toolAdapter.htmlReporter.emit, PluginEvents.DATABASE_CREATED, sinon.match.any); }); }); describe('updateReferenceImage', () => { - describe('should emit "UPDATE_REFERENCE" event', () => { - it('should emit "UPDATE_REFERENCE" event with state and reference data', async () => { - const testRefUpdateData = [{ - id: 'some-id', - fullTitle: () => 'some-title', - browserId: 'yabro', - suite: {path: ['suite1']}, - state: {}, - metaInfo: {}, - imagesInfo: [{ + it('should update reference for one image', async () => { + const testRefUpdateData = [{ + id: 'some-id', + fullTitle: () => 'some-title', + browserId: 'yabro', + suite: {path: ['suite1']}, + state: {}, + metaInfo: {}, + imagesInfo: [{ + status: UPDATED, + stateName: 'plain1', + actualImg: { + size: {height: 100, width: 200} + } + }] + }]; + + const getScreenshotPath = sandbox.stub().returns('/ref/path1'); + const config = stubConfig({ + browsers: {yabro: {getScreenshotPath}} + }); + + const testCollection = mkTestCollection_({'some-title.yabro': testRefUpdateData[0]}); + const toolAdapter = stubToolAdapter({config, testCollection}); + + const gui = initGuiReporter({toolAdapter}); + await gui.initialize(); + + await gui.updateReferenceImage(testRefUpdateData); + + assert.calledOnceWith(toolAdapter.updateReference, { + refImg: {path: '/ref/path1', relativePath: '../path1', size: {height: 100, width: 200}}, + state: 'plain1' + }); + }); + + it('should update reference for each image', async () => { + const tests = [{ + id: 'some-id', + fullTitle: () => 'some-title', + browserId: 'yabro', + suite: {path: ['suite1']}, + state: {}, + metaInfo: {}, + imagesInfo: [ + { status: UPDATED, stateName: 'plain1', actualImg: { size: {height: 100, width: 200} } - }] - }]; - - const getScreenshotPath = sandbox.stub().returns('/ref/path1'); - const config = stubConfig({ - browsers: {yabro: {getScreenshotPath}} - }); - const testplane = mkTestplane_(config, {'some-title.yabro': testRefUpdateData[0]}); - const gui = initGuiReporter(testplane, {pluginConfig: {path: 'report-path'}}); - await gui.initialize(); + }, + { + status: UPDATED, + stateName: 'plain2', + actualImg: { + size: {height: 200, width: 300} + } + } + ] + }]; - await gui.updateReferenceImage(testRefUpdateData); + const getScreenshotPath = sandbox.stub() + .onFirstCall().returns('/ref/path1') + .onSecondCall().returns('/ref/path2'); - assert.calledOnceWith(testplane.emit, 'updateReference', { - refImg: {path: '/ref/path1', size: {height: 100, width: 200}}, - state: 'plain1' - }); + const config = stubConfig({ + browsers: {yabro: {getScreenshotPath}} }); - it('for each image info', async () => { - const tests = [{ - id: 'some-id', - fullTitle: () => 'some-title', - browserId: 'yabro', - suite: {path: ['suite1']}, - state: {}, - metaInfo: {}, - imagesInfo: [ - { - status: UPDATED, - stateName: 'plain1', - actualImg: { - size: {height: 100, width: 200} - } - }, - { - status: UPDATED, - stateName: 'plain2', - actualImg: { - size: {height: 200, width: 300} - } - } - ] - }]; + const testCollection = mkTestCollection_({'some-title.yabro': tests[0]}); + const toolAdapter = stubToolAdapter({config, testCollection}); - const getScreenshotPath = sandbox.stub() - .onFirstCall().returns('/ref/path1') - .onSecondCall().returns('/ref/path2'); + const gui = initGuiReporter({toolAdapter}); + await gui.initialize(); - const config = stubConfig({ - browsers: {yabro: {getScreenshotPath}} - }); + await gui.updateReferenceImage(tests); - const testplane = mkTestplane_(config, {'some-title.yabro': tests[0]}); - const gui = initGuiReporter(testplane); - await gui.initialize(); + assert.calledTwice(toolAdapter.updateReference); + assert.calledWith(toolAdapter.updateReference.firstCall, { + refImg: {path: '/ref/path1', relativePath: '../path1', size: {height: 100, width: 200}}, + state: 'plain1' + }); + assert.calledWith(toolAdapter.updateReference.secondCall, { + refImg: {path: '/ref/path2', relativePath: '../path2', size: {height: 200, width: 300}}, + state: 'plain2' + }); + }); - await gui.updateReferenceImage(tests); + it('should determine status based on the latest result', async () => { + const testRefUpdateData = [{ + id: 'some-id', + fullTitle: () => 'some-title', + browserId: 'yabro', + suite: {path: ['suite1']}, + state: {}, + metaInfo: {}, + imagesInfo: [{ + status: UPDATED, + stateName: 'plain1', + actualImg: { + size: {height: 100, width: 200} + } + }] + }]; - assert.calledTwice(testplane.emit); - assert.calledWith(testplane.emit.firstCall, 'updateReference', { - refImg: {path: '/ref/path1', size: {height: 100, width: 200}}, - state: 'plain1' - }); - assert.calledWith(testplane.emit.secondCall, 'updateReference', { - refImg: {path: '/ref/path2', size: {height: 200, width: 300}}, - state: 'plain2' - }); + const getScreenshotPath = sandbox.stub().returns('/ref/path1'); + const config = stubConfig({ + browsers: {yabro: {getScreenshotPath}} }); + + const testCollection = mkTestCollection_({'some-title.yabro': testRefUpdateData[0]}); + const toolAdapter = stubToolAdapter({config, testCollection}); + + reportBuilder.getLatestAttempt.withArgs({fullName: 'some-title', browserId: 'yabro'}).returns(100500); + reportBuilder.getUpdatedReferenceTestStatus.withArgs(sinon.match({attempt: 100500})).returns(TestStatus.UPDATED); + + const gui = initGuiReporter({toolAdapter}); + await gui.initialize(); + + reportBuilder.addTestResult.reset(); + + await gui.updateReferenceImage(testRefUpdateData); + + assert.calledOnceWith(reportBuilder.addTestResult, sinon.match.any, {status: TestStatus.UPDATED}); }); }); @@ -429,8 +369,11 @@ describe('lib/gui/tool-runner/index', () => { const config = stubConfig({ browsers: {yabro: {getScreenshotPath}} }); - const testplane = mkTestplane_(config, {'some-title.yabro': tests[0]}); - const gui = initGuiReporter(testplane); + + const testCollection = mkTestCollection_({'some-title.yabro': tests[0]}); + const toolAdapter = stubToolAdapter({config, testCollection}); + + const gui = initGuiReporter({toolAdapter}); await gui.initialize(); return {gui, tests}; @@ -477,8 +420,11 @@ describe('lib/gui/tool-runner/index', () => { let compareOpts; beforeEach(() => { - testplane = stubTool(stubConfig({tolerance: 100500, antialiasingTolerance: 500100})); - testplane.readTests.resolves(mkTestCollection_()); + toolAdapter = stubToolAdapter({ + config: stubConfig({tolerance: 100500, antialiasingTolerance: 500100}), + reporterConfig: stubReporterConfig({path: 'report_path'}) + }); + toolAdapter.readTests.resolves(mkTestCollection_()); compareOpts = { tolerance: 100500, @@ -491,7 +437,7 @@ describe('lib/gui/tool-runner/index', () => { }); it('should stop comparison on first diff in reference images', async () => { - const gui = initGuiReporter(testplane, {configs: mkPluginConfig_({path: 'report_path'})}); + const gui = initGuiReporter({toolAdapter}); const refImagesInfo = mkImagesInfo({expectedImg: {path: 'ref-path-1'}}); const comparedImagesInfo = [mkImagesInfo({expectedImg: {path: 'ref-path-2'}})]; @@ -513,7 +459,7 @@ describe('lib/gui/tool-runner/index', () => { }); it('should stop comparison on diff in actual images', async () => { - const gui = initGuiReporter(testplane, {configs: mkPluginConfig_({path: 'report_path'})}); + const gui = initGuiReporter({toolAdapter}); const refImagesInfo = mkImagesInfo({actualImg: {path: 'act-path-1'}}); const comparedImagesInfo = [mkImagesInfo({actualImg: {path: 'act-path-2'}})]; @@ -536,7 +482,7 @@ describe('lib/gui/tool-runner/index', () => { }); it('should compare each diff cluster', async () => { - const gui = initGuiReporter(testplane, {configs: mkPluginConfig_({path: 'report_path'})}); + const gui = initGuiReporter({toolAdapter}); const refImagesInfo = mkImagesInfo({ diffClusters: [ {left: 0, top: 0, right: 5, bottom: 5}, @@ -559,7 +505,7 @@ describe('lib/gui/tool-runner/index', () => { }); it('should return all found image ids with equal diffs', async () => { - const gui = initGuiReporter(testplane); + const gui = initGuiReporter({toolAdapter}); const refImagesInfo = {...mkImagesInfo(), id: 'selected-img-1'}; const comparedImagesInfo = [ {...mkImagesInfo(), id: 'compared-img-2'}, @@ -576,89 +522,24 @@ describe('lib/gui/tool-runner/index', () => { }); describe('run', () => { - let runner; - let collection; - - beforeEach(() => { - runner = {run: sandbox.stub().resolves()}; - collection = mkTestCollection_(); - createTestRunner.withArgs(collection).returns(runner); - testplane.readTests.resolves(collection); - }); - - describe('should run testplane with', () => { - const run_ = async (globalCliOpts = {}) => { - const configs = {...mkPluginConfig_(), ...mkToolCliOpts_(globalCliOpts)}; - const gui = ToolGuiReporter.create([], testplane, configs); - - await gui.initialize(); - await gui.run(); - - const runHandler = runner.run.firstCall.args[0]; - runHandler(collection); - }; - - it('passed opts', async () => { - await run_({grep: /some-grep/, set: 'some-set', browser: 'yabro', devtools: true}); + it('should run tool with passed opts', async () => { + const cliTool = {grep: /some-grep/, set: 'some-set', browser: 'yabro', devtools: true}; + const collection = mkTestCollection_(); + toolAdapter.readTests.resolves(collection); - assert.calledOnceWith(testplane.run, collection, sinon.match({grep: /some-grep/, sets: 'some-set', browsers: 'yabro', devtools: true})); - }); - - describe('"replMode" option', () => { - it('should be disabled by default', async () => { - await run_(); - - assert.calledOnceWith(testplane.run, collection, sinon.match({ - replMode: { - enabled: false, - beforeTest: false, - onFail: false - } - })); - }); - - it('should be enabled when specify "repl" flag', async () => { - await run_({repl: true}); - - assert.calledOnceWith(testplane.run, collection, sinon.match({ - replMode: { - enabled: true, - beforeTest: false, - onFail: false - } - })); - }); + const gui = initGuiReporter({toolAdapter, cli: {tool: cliTool, options: {}}}); + const tests = []; - it('should be enabled when specify "beforeTest" flag', async () => { - await run_({replBeforeTest: true}); - - assert.calledOnceWith(testplane.run, collection, sinon.match({ - replMode: { - enabled: true, - beforeTest: true, - onFail: false - } - })); - }); - - it('should be enabled when specify "onFail" flag', async () => { - await run_({replOnFail: true}); + await gui.initialize(); + await gui.run(tests); - assert.calledOnceWith(testplane.run, collection, sinon.match({ - replMode: { - enabled: true, - beforeTest: false, - onFail: true - } - })); - }); - }); + assert.calledOnceWith(toolAdapter.run, collection, tests, cliTool); }); }); - describe('finalize testplane', () => { + describe('finalize tool', () => { it('should call reportBuilder.finalize', async () => { - const gui = initGuiReporter(testplane); + const gui = initGuiReporter({toolAdapter}); await gui.initialize(); gui.finalize(); @@ -670,9 +551,11 @@ describe('lib/gui/tool-runner/index', () => { describe('reuse tests tree from database', () => { let gui; let dbPath; + let toolAdapter; beforeEach(() => { - gui = initGuiReporter(testplane, {configs: mkPluginConfig_({path: 'report_path'})}); + toolAdapter = stubToolAdapter({reporterConfig: stubReporterConfig({path: 'report_path'})}); + gui = initGuiReporter({toolAdapter}); dbPath = path.resolve('report_path', LOCAL_DATABASE_NAME); sandbox.stub(fs, 'pathExists').withArgs(dbPath).resolves(false); @@ -716,8 +599,7 @@ describe('lib/gui/tool-runner/index', () => { it('"autoRun" from gui options', async () => { const guiOpts = {autoRun: true}; - const configs = {...mkPluginConfig_(), ...mkToolCliOpts_({}, guiOpts)}; - const gui = ToolGuiReporter.create([], testplane, configs); + const gui = initGuiReporter({toolAdapter, cli: {options: guiOpts}}); await gui.initialize(); diff --git a/test/unit/lib/images-info-saver.ts b/test/unit/lib/images-info-saver.ts index 69708dda7..d28fdc81d 100644 --- a/test/unit/lib/images-info-saver.ts +++ b/test/unit/lib/images-info-saver.ts @@ -1,7 +1,7 @@ import * as fsOriginal from 'fs-extra'; import {ImagesInfoSaver as ImagesInfoSaverOriginal} from 'lib/images-info-saver'; import {Writable} from 'type-fest'; -import {ReporterTestResult} from 'lib/test-adapter'; +import {ReporterTestResult} from 'lib/adapters/test-result'; import { ImageBase64, ImageBuffer, diff --git a/test/unit/lib/merge-reports/index.js b/test/unit/lib/merge-reports/index.js index b9d26c807..2c61ca32d 100644 --- a/test/unit/lib/merge-reports/index.js +++ b/test/unit/lib/merge-reports/index.js @@ -2,17 +2,17 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); -const {stubTool, stubConfig} = require('../../utils'); +const {stubToolAdapter} = require('../../utils'); const originalServerUtils = require('lib/server-utils'); describe('lib/merge-reports', () => { const sandbox = sinon.sandbox.create(); let htmlReporter, serverUtils, mergeReports, axiosStub; - const execMergeReports_ = async ({pluginConfig = stubConfig(), testplane = stubTool(stubConfig()), paths = [], opts = {}}) => { + const execMergeReports_ = async ({toolAdapter = stubToolAdapter(), paths = [], opts = {}}) => { opts = _.defaults(opts, {destination: 'default-dest-report/path'}); - await mergeReports(pluginConfig, testplane, paths, opts); + await mergeReports(toolAdapter, paths, opts); }; beforeEach(() => { @@ -71,11 +71,10 @@ describe('lib/merge-reports', () => { }); describe('should send headers to request json urls', () => { - let pluginConfig, testplane, paths, destPath; + let toolAdapter, paths, destPath; beforeEach(() => { - pluginConfig = stubConfig(); - testplane = stubTool(pluginConfig, {}, {}, htmlReporter); + toolAdapter = stubToolAdapter({htmlReporter}); paths = ['src-report/path-1.json']; destPath = 'dest-report/path'; }); @@ -87,7 +86,7 @@ describe('lib/merge-reports', () => { it('from environment variable', async () => { process.env['html_reporter_headers'] = '{"foo":"bar","baz":"qux"}'; - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers: []}}); assert.calledOnceWith( axiosStub.get, @@ -103,7 +102,7 @@ describe('lib/merge-reports', () => { it('from cli option', async () => { const headers = ['foo=bar', 'baz=qux']; - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers}}); assert.calledOnceWith( axiosStub.get, @@ -121,7 +120,7 @@ describe('lib/merge-reports', () => { const headers = ['foo=123', 'abc=def']; axiosStub.get.withArgs('src-report/path-1.json').resolves({data: {jsonUrls: ['src-report/path-2.json'], dbUrls: []}}); - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers}}); assert.calledTwice(axiosStub.get); assert.calledWith( @@ -146,20 +145,18 @@ describe('lib/merge-reports', () => { }); it('should merge reports', async () => { - const pluginConfig = stubConfig(); - const testplane = stubTool(pluginConfig, {}, {}, htmlReporter); + const toolAdapter = stubToolAdapter({htmlReporter}); const paths = ['src-report/path-1.json', 'src-report/path-2.db']; const destPath = 'dest-report/path'; - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers: []}}); - assert.calledOnceWith(serverUtils.saveStaticFilesToReportDir, testplane.htmlReporter, pluginConfig, destPath); + assert.calledOnceWith(serverUtils.saveStaticFilesToReportDir, toolAdapter.htmlReporter, toolAdapter.reporterConfig, destPath); assert.calledOnceWith(serverUtils.writeDatabaseUrlsFile, destPath, paths); }); it('should resolve json urls while merging reports', async () => { - const pluginConfig = stubConfig(); - const testplane = stubTool(pluginConfig, {}, {}, htmlReporter); + const toolAdapter = stubToolAdapter({htmlReporter}); const paths = ['src-report/path-1.json']; const destPath = 'dest-report/path'; @@ -168,44 +165,42 @@ describe('lib/merge-reports', () => { axiosStub.get.withArgs('src-report/path-3.json').resolves({data: {jsonUrls: ['src-report/path-4.json'], dbUrls: ['path-3.db']}}); axiosStub.get.withArgs('src-report/path-4.json').resolves({data: {jsonUrls: [], dbUrls: ['path-4.db']}}); - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers: []}}); assert.calledOnceWith(serverUtils.writeDatabaseUrlsFile, destPath, ['path-1.db', 'path-2.db', 'path-3.db', 'path-4.db']); }); it('should normalize urls while merging reports', async () => { - const pluginConfig = stubConfig(); - const testplane = stubTool(pluginConfig, {}, {}, htmlReporter); + const toolAdapter = stubToolAdapter({htmlReporter}); const paths = ['src-report/path-1.json']; const destPath = 'dest-report/path'; axiosStub.get.withArgs('src-report/path-1.json').resolves({data: {jsonUrls: ['https://foo.bar/path-2.json']}}); axiosStub.get.withArgs('https://foo.bar/path-2.json').resolves({data: {jsonUrls: [], dbUrls: ['sqlite.db']}}); - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers: []}}); assert.calledOnceWith(serverUtils.writeDatabaseUrlsFile, destPath, ['https://foo.bar/sqlite.db']); }); it('should fallback to json url while merging reports', async () => { - const pluginConfig = stubConfig(); - const testplane = stubTool(pluginConfig, {}, {}, htmlReporter); + const toolAdapter = stubToolAdapter({htmlReporter}); const paths = ['src-report/path-1.json']; const destPath = 'dest-report/path'; axiosStub.get.rejects(); - await execMergeReports_({pluginConfig, testplane, paths, opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths, opts: {destPath, headers: []}}); assert.calledOnceWith(serverUtils.writeDatabaseUrlsFile, destPath, ['src-report/path-1.json']); }); it('should emit REPORT_SAVED event', async () => { - const testplane = stubTool({}, {}, {}, htmlReporter); + const toolAdapter = stubToolAdapter({htmlReporter}); const destPath = 'dest-report/path'; - await execMergeReports_({pluginConfig: {}, testplane, paths: [''], opts: {destPath, headers: []}}); + await execMergeReports_({toolAdapter, paths: [''], opts: {destPath, headers: []}}); - assert.calledOnceWith(testplane.htmlReporter.emitAsync, 'reportSaved', {reportPath: destPath}); + assert.calledOnceWith(toolAdapter.htmlReporter.emitAsync, 'reportSaved', {reportPath: destPath}); }); }); diff --git a/test/unit/lib/report-builder/gui.js b/test/unit/lib/report-builder/gui.js index cbd0204b4..1ad578f0f 100644 --- a/test/unit/lib/report-builder/gui.js +++ b/test/unit/lib/report-builder/gui.js @@ -4,7 +4,7 @@ const fs = require('fs-extra'); const _ = require('lodash'); const proxyquire = require('proxyquire'); const serverUtils = require('lib/server-utils'); -const {TestplaneTestAdapter} = require('lib/test-adapter/testplane'); +const {TestplaneTestResultAdapter} = require('lib/adapters/test-result/testplane'); const {SqliteClient} = require('lib/sqlite-client'); const {GuiTestsTreeBuilder} = require('lib/tests-tree-builder/gui'); const {HtmlReporter} = require('lib/plugin-api'); @@ -33,7 +33,7 @@ describe('GuiReportBuilder', () => { htmlReporter }; - TestplaneTestAdapter.create = (obj) => obj; + TestplaneTestResultAdapter.create = (obj) => obj; dbClient = await SqliteClient.create({htmlReporter, reportPath: TEST_REPORT_PATH}); imagesInfoSaver = sinon.createStubInstance(ImagesInfoSaver); @@ -100,7 +100,7 @@ describe('GuiReportBuilder', () => { }).StaticReportBuilder }, '../server-utils': {hasImage, deleteFile}, - '../test-adapter/utils': {copyAndUpdate} + '../adapters/test-result/utils': {copyAndUpdate} }).GuiReportBuilder; sandbox.stub(GuiTestsTreeBuilder, 'create').returns(Object.create(GuiTestsTreeBuilder.prototype)); diff --git a/test/unit/lib/server-utils.js b/test/unit/lib/server-utils.js index 41c77f982..89116dd5f 100644 --- a/test/unit/lib/server-utils.js +++ b/test/unit/lib/server-utils.js @@ -1,8 +1,6 @@ 'use strict'; const path = require('path'); - -const Promise = require('bluebird'); const _ = require('lodash'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); @@ -242,51 +240,6 @@ describe('server-utils', () => { }); }); - describe('initializeCustomGui', () => { - it('should initialize each group of controls if initialize-function is available', async () => { - const initializeSpy1 = sinon.spy().named('initialize-1'); - const initializeSpy2 = sinon.spy().named('initialize-2'); - - const initialize1 = sinon.stub().callsFake(() => Promise.delay(5).then(initializeSpy1)); - const initialize2 = sinon.stub().callsFake(() => Promise.delay(10).then(initializeSpy2)); - - const ctx1 = {initialize: initialize1}; - const ctx2 = {initialize: initialize2}; - - const pluginConfig = { - customGui: {'section-1': [ctx1], 'section-2': [ctx2]} - }; - const testplane = {}; - - await utils.initializeCustomGui(testplane, pluginConfig); - - assert.calledOnceWith(initialize1, {testplane, hermione: testplane, ctx: ctx1}); - assert.calledOnceWith(initialize2, {testplane, hermione: testplane, ctx: ctx2}); - - assert.callOrder(initializeSpy1, initializeSpy2); - }); - }); - - describe('runCustomGuiAction', () => { - it('should run action for specified controls', async () => { - const actionSpy = sinon.spy().named('action'); - const action = sinon.stub().callsFake(() => Promise.delay(10).then(actionSpy)); - const control = {}; - const ctx = {controls: [control], action}; - const pluginConfig = {customGui: {'section': [ctx]}}; - const testplane = {}; - - await utils.runCustomGuiAction(testplane, pluginConfig, { - sectionName: 'section', - groupIndex: 0, - controlIndex: 0 - }); - - assert.calledOnceWith(action, {testplane, hermione: testplane, ctx, control}); - assert.calledOnce(actionSpy); - }); - }); - describe('forEachPlugin', () => { it('should call the callback for each plugin only once', () => { const plugins = [ diff --git a/test/unit/lib/static/components/controls/run-button.jsx b/test/unit/lib/static/components/controls/run-button.jsx index 4557cde22..d296925a1 100644 --- a/test/unit/lib/static/components/controls/run-button.jsx +++ b/test/unit/lib/static/components/controls/run-button.jsx @@ -5,10 +5,12 @@ import {mkConnectedComponent, mkState} from '../utils'; describe('', () => { const sandbox = sinon.sandbox.create(); - let RunButton, useLocalStorageStub, actionsStub, selectorsStub; + let RunButton, useLocalStorageStub, actionsStub, selectorsStub, writeValueStub; beforeEach(() => { + writeValueStub = sandbox.stub(); useLocalStorageStub = sandbox.stub().returns([true]); + useLocalStorageStub.withArgs('RunMode', 'Failed').returns(['All', writeValueStub]); actionsStub = { runAllTests: sandbox.stub().returns({type: 'some-type'}), runFailedTests: sandbox.stub().returns({type: 'some-type'}), @@ -69,6 +71,7 @@ describe('', () => { }); it('should call "runFailedTests" action on "Run failed tests" click', () => { + useLocalStorageStub.withArgs('RunMode', 'Failed').returns(['Failed', () => {}]); const failedTests = [{testName: 'suite test', browserName: 'yabro'}]; const state = mkState({initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false}}); selectorsStub.getFailedTests.withArgs(state).returns(failedTests); @@ -81,6 +84,7 @@ describe('', () => { }); it('should call "retrySuite" action on "Run checked tests" click', () => { + useLocalStorageStub.withArgs('RunMode', 'Failed').returns(['Checked', () => {}]); const checkedTests = [{testName: 'suite test', browserName: 'yabro'}]; const state = mkState({initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false}}); selectorsStub.getCheckedTests.withArgs(state).returns(checkedTests); @@ -110,33 +114,46 @@ describe('', () => { assert.equal(component.find('button').text(), 'Run all tests'); }); - it('should be "Run checked tests" if there are checked tests', () => { + it('should switch to "Run checked tests" if there are checked tests', () => { selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); const component = mkConnectedComponent(, { initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} }); - assert.equal(component.find('button').text(), 'Run checked tests'); + assert.calledWith(writeValueStub, 'Checked'); + }); + }); + + describe('localStorage', () => { + it('should save "Run all tests" if picked', () => { + selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + selectorsStub.getFailedTests.returns([{testName: 'testName', browserName: 'browserName'}]); + const component = mkConnectedComponent(, { + initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} + }); + + component.find({children: 'All'}).simulate('click'); + assert.calledWith(writeValueStub, 'All'); }); - it('should be "Run failed tests" if picked', () => { + it('should save "Run failed tests" if picked', () => { selectorsStub.getFailedTests.returns([{testName: 'testName', browserName: 'browserName'}]); const component = mkConnectedComponent(, { initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} }); component.find({children: 'Failed'}).simulate('click'); - assert.equal(component.find('button').text(), 'Run failed tests'); + assert.calledOnceWith(writeValueStub, 'Failed'); }); - it('should be "Run checked tests" if picked', () => { + it('should save "Run checked tests" if picked', () => { selectorsStub.getCheckedTests.returns([{testName: 'testName', browserName: 'browserName'}]); const component = mkConnectedComponent(, { initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} }); component.find({children: 'Checked'}).simulate('click'); - assert.equal(component.find('button').text(), 'Run checked tests'); + assert.calledWith(writeValueStub, 'Checked'); }); }); diff --git a/test/unit/lib/static/components/suites.jsx b/test/unit/lib/static/components/suites.jsx index 1b371642d..a3bde3e13 100644 --- a/test/unit/lib/static/components/suites.jsx +++ b/test/unit/lib/static/components/suites.jsx @@ -41,6 +41,9 @@ describe('', () => { }); it('should render few section common components', () => { + if (!global.SVGElement) { + global.SVGElement = HTMLElement; // Without this line test throws an error "ReferenceError: SVGElement is not defined" + } getVisibleRootSuiteIds.returns(['suite-id-1', 'suite-id-2']); const component = mkSuitesComponent(); diff --git a/test/unit/lib/static/components/utils.jsx b/test/unit/lib/static/components/utils.jsx index a89aa61aa..a9cdd1990 100644 --- a/test/unit/lib/static/components/utils.jsx +++ b/test/unit/lib/static/components/utils.jsx @@ -3,6 +3,7 @@ import _ from 'lodash'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import defaultState from 'lib/static/modules/default-state'; +import { ThemeProvider } from '@gravity-ui/uikit'; exports.mkState = ({initialState} = {}) => { return _.defaultsDeep(initialState, defaultState); @@ -17,7 +18,7 @@ exports.mkStore = ({initialState, state} = {}) => { exports.mkConnectedComponent = (Component, state) => { const store = exports.mkStore(state); - return mount({Component}); + return mount({Component}); }; exports.mkImg_ = (opts = {}) => { diff --git a/test/unit/testplane.js b/test/unit/testplane.js index 2781e6bbb..5dd31ab86 100644 --- a/test/unit/testplane.js +++ b/test/unit/testplane.js @@ -19,7 +19,7 @@ describe('lib/testplane', () => { const sandbox = sinon.createSandbox(); let testplane; let cacheExpectedPaths = new Map(), cacheAllImages = new Map(), cacheDiffImages = new Map(); - let fs, originalUtils, utils, SqliteClient, ImagesInfoSaver, TestAdapter, StaticReportBuilder, HtmlReporter, runHtmlReporter; + let fs, originalUtils, utils, SqliteClient, ImagesInfoSaver, TestResultAdapter, StaticReportBuilder, HtmlReporter, runHtmlReporter; let program; @@ -115,15 +115,15 @@ describe('lib/testplane', () => { './server-utils': utils }).ImagesInfoSaver; - TestAdapter = proxyquire('lib/test-adapter', { + TestResultAdapter = proxyquire('lib/adapters/test-result', { 'fs-extra': fs, './server-utils': utils - }).TestAdapter; + }).TestResultAdapter; StaticReportBuilder = proxyquire('lib/report-builder/static', { 'fs-extra': fs, '../server-utils': utils, - '../test-adapter': {TestAdapter}, + '../adapters/test-result': {TestResultAdapter}, '../images-info-saver': {ImagesInfoSaver} }).StaticReportBuilder; diff --git a/test/unit/utils.js b/test/unit/utils.js index b77989de7..1aec99eb0 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.js @@ -14,6 +14,10 @@ function stubConfig(config = {}) { return Object.assign(config, browserConfigs); } +function stubReporterConfig(opts = {}) { + return _.defaults(opts, {path: 'default-path'}); +} + const stubTestCollection = (testsTree = {}) => { return { eachTest: (cb) => { @@ -31,6 +35,7 @@ function stubTool(config = stubConfig(), events = {}, errors = {}, htmlReporter) tool.run = sinon.stub().resolves(false); tool.readTests = sinon.stub().resolves(stubTestCollection()); + tool.halt = sinon.stub(); tool.htmlReporter = htmlReporter || sinon.createStubInstance(HtmlReporter); _.defaultsDeep(tool.htmlReporter, { emitAsync: sinon.stub(), @@ -46,6 +51,29 @@ function stubTool(config = stubConfig(), events = {}, errors = {}, htmlReporter) return tool; } +function stubToolAdapter({ + config = stubConfig(), reporterConfig = stubReporterConfig(), testCollection = stubTestCollection(), htmlReporter +} = {}) { + const toolAdapter = { + config, + reporterConfig, + htmlReporter: htmlReporter || sinon.createStubInstance(HtmlReporter), + run: sinon.stub().resolves(false), + readTests: sinon.stub().resolves(testCollection), + updateReference: sinon.stub(), + handleTestResults: sinon.stub(), + guiApi: { + initServer: sinon.stub(), + serverReady: sinon.stub() + } + }; + + sinon.stub(toolAdapter.htmlReporter, 'imagesSaver').value({saveImg: sinon.stub()}); + sinon.stub(toolAdapter.htmlReporter, 'config').value({}); + + return toolAdapter; +} + function mkSuite(opts = {}) { return _.defaults(opts, { name: _.last(opts.suitePath) || 'default-suite', @@ -153,8 +181,10 @@ class ImageDiffError extends Error { module.exports = { stubConfig, + stubReporterConfig, stubTestCollection, stubTool, + stubToolAdapter, mkSuite, mkState, mkBrowserResult, diff --git a/testplane.ts b/testplane.ts index 070eb23cc..c4d8bc762 100644 --- a/testplane.ts +++ b/testplane.ts @@ -6,19 +6,19 @@ import _ from 'lodash'; import PQueue from 'p-queue'; import {CommanderStatic} from '@gemini-testing/commander'; -import {cliCommands} from './lib/cli-commands'; +import {TestplaneToolAdapter} from './lib/adapters/tool/testplane'; +import {commands as cliCommands} from './lib/cli'; import {parseConfig} from './lib/config'; import {ToolName} from './lib/constants'; -import {HtmlReporter} from './lib/plugin-api'; import {StaticReportBuilder} from './lib/report-builder/static'; import {formatTestResult, logPathToHtmlReport, logError, getExpectedCacheKey} from './lib/server-utils'; import {SqliteClient} from './lib/sqlite-client'; -import {HtmlReporterApi, ReporterOptions, TestSpecByPath} from './lib/types'; +import {ReporterOptions, TestSpecByPath} from './lib/types'; import {createWorkers, CreateWorkersRunner} from './lib/workers/create-workers'; import {SqliteImageStore} from './lib/image-store'; import {Cache} from './lib/cache'; import {ImagesInfoSaver} from './lib/images-info-saver'; -import {getStatus} from './lib/test-adapter/testplane'; +import {getStatus} from './lib/adapters/test-result/testplane'; export default (testplane: Testplane, opts: Partial): void => { if (testplane.isWorker()) { @@ -31,9 +31,8 @@ export default (testplane: Testplane, opts: Partial): void => { return; } - const htmlReporter = HtmlReporter.create(config, {toolName: ToolName.Testplane}); - - (testplane as Testplane & HtmlReporterApi).htmlReporter = htmlReporter; + const toolAdapter = TestplaneToolAdapter.create({toolName: ToolName.Testplane, tool: testplane, reporterConfig: config}); + const {htmlReporter} = toolAdapter; let isCliCommandLaunched = false; let handlingTestResults: Promise; @@ -54,7 +53,7 @@ export default (testplane: Testplane, opts: Partial): void => { testplane.on(testplane.events.CLI, (commander: CommanderStatic) => { _.values(cliCommands).forEach((command: string) => { // eslint-disable-next-line @typescript-eslint/no-var-requires - require(path.resolve(__dirname, 'lib/cli-commands', command))(commander, config, testplane); + require(path.resolve(__dirname, 'lib/cli/commands', command))(commander, toolAdapter); commander.prependListener(`command:${command}`, () => { isCliCommandLaunched = true;