diff --git a/hermione.js b/hermione.js deleted file mode 100644 index f01cfd5d8..000000000 --- a/hermione.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -const os = require('os'); -const PQueue = require('p-queue'); -const {PluginAdapter} = require('./lib/plugin-adapter'); -const {createWorkers} = require('./lib/workers/create-workers'); -const {FAIL, SUCCESS} = require('./lib/constants'); -const {hasDiff} = require('./lib/common-utils'); -const {formatTestResult} = require('./lib/server-utils'); - -let workers; - -module.exports = (hermione, opts) => { - if (hermione.isWorker()) { - return; - } - const plugin = PluginAdapter.create(hermione, opts); - - if (!plugin.isEnabled()) { - return; - } - - plugin - .addApi() - .addCliCommands() - .init(prepare); - - hermione.on(hermione.events.RUNNER_START, (runner) => { - workers = createWorkers(runner); - }); -}; - -async function prepare(hermione, reportBuilder, pluginConfig) { - const {path: reportPath} = pluginConfig; - const {imageHandler} = reportBuilder; - - const failHandler = async (testResult) => { - const formattedResult = formatTestResult(testResult, FAIL, reportBuilder); - const actions = [imageHandler.saveTestImages(formattedResult, workers)]; - - if (formattedResult.errorDetails) { - actions.push(formattedResult.saveErrorDetails(reportPath)); - } - - await Promise.all(actions); - - return formattedResult; - }; - - const addFail = (formattedResult) => { - return hasDiff(formattedResult.assertViewResults) - ? reportBuilder.addFail(formattedResult) - : reportBuilder.addError(formattedResult); - }; - - return new Promise((resolve, reject) => { - const queue = new PQueue({concurrency: os.cpus().length}); - const promises = []; - - hermione.on(hermione.events.TEST_PASS, testResult => { - promises.push(queue.add(async () => { - const formattedResult = formatTestResult(testResult, SUCCESS, reportBuilder); - await imageHandler.saveTestImages(formattedResult, workers); - - return reportBuilder.addSuccess(formattedResult); - }).catch(reject)); - }); - - hermione.on(hermione.events.RETRY, testResult => { - promises.push(queue.add(() => failHandler(testResult).then(addFail)).catch(reject)); - }); - - hermione.on(hermione.events.TEST_FAIL, testResult => { - promises.push(queue.add(() => failHandler(testResult).then(addFail)).catch(reject)); - }); - - hermione.on(hermione.events.TEST_PENDING, testResult => { - promises.push(queue.add(() => failHandler(testResult).then((testResult) => reportBuilder.addSkipped(testResult)).catch(reject))); - }); - - hermione.on(hermione.events.RUNNER_END, () => { - return Promise.all(promises).then(resolve, reject); - }); - }); -} diff --git a/hermione.ts b/hermione.ts new file mode 100644 index 000000000..385c3a700 --- /dev/null +++ b/hermione.ts @@ -0,0 +1,132 @@ +import os from 'os'; +import path from 'path'; +import Hermione, {TestResult as HermioneTestResult} from 'hermione'; +import _ from 'lodash'; +import PQueue from 'p-queue'; +import {CommanderStatic} from '@gemini-testing/commander'; + +import {cliCommands} from './lib/cli-commands'; +import {hasFailedImages} from './lib/common-utils'; +import {parseConfig} from './lib/config'; +import {SKIPPED, SUCCESS, TestStatus, ToolName, UNKNOWN_ATTEMPT} from './lib/constants'; +import {HtmlReporter} from './lib/plugin-api'; +import {StaticReportBuilder} from './lib/report-builder/static'; +import {formatTestResult, logPathToHtmlReport, logError} from './lib/server-utils'; +import {SqliteClient} from './lib/sqlite-client'; +import {HtmlReporterApi, ImageInfoFull, ReporterOptions} from './lib/types'; +import {createWorkers, CreateWorkersRunner} from './lib/workers/create-workers'; + +export = (hermione: Hermione, opts: Partial): void => { + if (hermione.isWorker() || !opts.enabled) { + return; + } + + const config = parseConfig(opts); + + const htmlReporter = HtmlReporter.create(config, {toolName: ToolName.Hermione}); + + (hermione as Hermione & HtmlReporterApi).htmlReporter = htmlReporter; + + let isCliCommandLaunched = false; + let handlingTestResults: Promise; + let staticReportBuilder: StaticReportBuilder; + + const withMiddleware = unknown>(fn: T): + (...args: Parameters) => ReturnType | undefined => { + return (...args: unknown[]) => { + // If any CLI command was launched, e.g. merge-reports, we need to interrupt regular flow + if (isCliCommandLaunched) { + return; + } + + return fn.call(undefined, ...args) as ReturnType; + }; + }; + + hermione.on(hermione.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, hermione); + + commander.prependListener(`command:${command}`, () => { + isCliCommandLaunched = true; + }); + }); + }); + + hermione.on(hermione.events.INIT, withMiddleware(async () => { + const dbClient = await SqliteClient.create({htmlReporter, reportPath: config.path}); + staticReportBuilder = StaticReportBuilder.create(htmlReporter, config, {dbClient}); + + handlingTestResults = Promise.all([ + staticReportBuilder.saveStaticFiles(), + handleTestResults(hermione, staticReportBuilder) + ]).then(async () => { + await staticReportBuilder.finalize(); + }).then(async () => { + await htmlReporter.emitAsync(htmlReporter.events.REPORT_SAVED, {reportPath: config.path}); + }); + + htmlReporter.emit(htmlReporter.events.DATABASE_CREATED, dbClient.getRawConnection()); + })); + + hermione.on(hermione.events.RUNNER_START, withMiddleware((runner) => { + staticReportBuilder.registerWorkers(createWorkers(runner as unknown as CreateWorkersRunner)); + })); + + hermione.on(hermione.events.RUNNER_END, withMiddleware(async () => { + try { + await handlingTestResults; + + logPathToHtmlReport(config); + } catch (e: unknown) { + logError(e as Error); + } + })); +}; + +async function handleTestResults(hermione: Hermione, reportBuilder: StaticReportBuilder): Promise { + return new Promise((resolve, reject) => { + const queue = new PQueue({concurrency: os.cpus().length}); + const promises: Promise[] = []; + + hermione.on(hermione.events.TEST_PASS, testResult => { + promises.push(queue.add(async () => { + const formattedResult = formatTestResult(testResult, SUCCESS, UNKNOWN_ATTEMPT, reportBuilder); + await reportBuilder.addSuccess(formattedResult); + }).catch(reject)); + }); + + hermione.on(hermione.events.RETRY, testResult => { + promises.push(queue.add(async () => { + const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR; + + const formattedResult = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder); + + await reportBuilder.addFail(formattedResult); + }).catch(reject)); + }); + + hermione.on(hermione.events.TEST_FAIL, testResult => { + promises.push(queue.add(async () => { + const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR; + + const formattedResult = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder); + + await reportBuilder.addFail(formattedResult); + }).catch(reject)); + }); + + hermione.on(hermione.events.TEST_PENDING, testResult => { + promises.push(queue.add(async () => { + const formattedResult = formatTestResult(testResult as HermioneTestResult, SKIPPED, UNKNOWN_ATTEMPT, reportBuilder); + + await reportBuilder.addSkipped(formattedResult); + }).catch(reject)); + }); + + hermione.on(hermione.events.RUNNER_END, () => { + return Promise.all(promises).then(() => resolve(), reject); + }); + }); +} diff --git a/lib/common-utils.ts b/lib/common-utils.ts index adc2f6251..9fdd4628b 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -5,8 +5,9 @@ import axios, {AxiosRequestConfig} from 'axios'; import {SUCCESS, FAIL, ERROR, SKIPPED, UPDATED, IDLE, RUNNING, QUEUED, TestStatus} from './constants'; import {UNCHECKED, INDETERMINATE, CHECKED} from './constants/checked-statuses'; -import {ImageData, ImageBase64, ImageInfoFull, TestError, ImageInfoError} from './types'; +import {ImageData, ImageBase64, ImageInfoFull, TestError, ImageInfoFail} from './types'; import {ErrorName, ImageDiffError, NoRefImageError} from './errors'; +import {ReporterTestResult} from './test-adapter'; export const getShortMD5 = (str: string): string => { return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7); }; @@ -29,7 +30,7 @@ export const isErrorStatus = (status: TestStatus): boolean => status === ERROR; export const isSkippedStatus = (status: TestStatus): boolean => status === SKIPPED; export const isUpdatedStatus = (status: TestStatus): boolean => status === UPDATED; -export const determineStatus = (statuses: TestStatus[]): TestStatus | null => { +export const determineFinalStatus = (statuses: TestStatus[]): TestStatus | null => { if (!statuses.length) { return SUCCESS; } @@ -103,17 +104,17 @@ export const hasNoRefImageErrors = ({assertViewResults = []}: {assertViewResults return assertViewResults.some((assertViewResult) => isNoRefImageError(assertViewResult)); }; -const hasFailedImages = (result: {imagesInfo?: ImageInfoFull[]}): boolean => { - const {imagesInfo = []} = result; - +export const hasFailedImages = (imagesInfo: ImageInfoFull[] = []): boolean => { return imagesInfo.some((imageInfo: ImageInfoFull) => { - return !isAssertViewError((imageInfo as ImageInfoError).error) && - (isErrorStatus(imageInfo.status) || isFailStatus(imageInfo.status)); + return (imageInfo as ImageInfoFail).stateName && + (isErrorStatus(imageInfo.status) || isFailStatus(imageInfo.status) || isNoRefImageError(imageInfo) || isImageDiffError(imageInfo)); }); }; -export const hasResultFails = (testResult: {status: TestStatus, imagesInfo?: ImageInfoFull[]}): boolean => { - return hasFailedImages(testResult) || isErrorStatus(testResult.status) || isFailStatus(testResult.status); +export const hasUnrelatedToScreenshotsErrors = (error: TestError): boolean => { + return !isNoRefImageError(error) && + !isImageDiffError(error) && + !isAssertViewError(error); }; export const getError = (error?: TestError): undefined | Pick => { @@ -128,6 +129,28 @@ export const hasDiff = (assertViewResults: {name?: string}[]): boolean => { return assertViewResults.some((result) => isImageDiffError(result as {name?: string})); }; +/* This method tries to determine true status of testResult by using fields like error, imagesInfo */ +export const determineStatus = (testResult: Pick): TestStatus => { + if ( + !hasFailedImages(testResult.imagesInfo) && + !isSkippedStatus(testResult.status) && + (!testResult.error || !hasUnrelatedToScreenshotsErrors(testResult.error)) + ) { + return SUCCESS; + } + + const imageErrors = (testResult.imagesInfo ?? []).map(imagesInfo => (imagesInfo as {error: {name?: string}}).error ?? {}); + if (hasDiff(imageErrors) || hasNoRefImageErrors({assertViewResults: imageErrors})) { + return FAIL; + } + + if (!isEmpty(testResult.error)) { + return ERROR; + } + + return testResult.status; +}; + export const isBase64Image = (image: ImageData | ImageBase64 | null | undefined): image is ImageBase64 => { return Boolean((image as ImageBase64 | undefined)?.base64); }; diff --git a/lib/constants/tests.ts b/lib/constants/tests.ts index 072614418..808755def 100644 --- a/lib/constants/tests.ts +++ b/lib/constants/tests.ts @@ -1 +1,5 @@ +export const HERMIONE_TITLE_DELIMITER = ' '; + export const PWT_TITLE_DELIMITER = ' › '; + +export const UNKNOWN_ATTEMPT = -1; diff --git a/lib/db-utils/server.ts b/lib/db-utils/server.ts index fbdb54560..a93b65382 100644 --- a/lib/db-utils/server.ts +++ b/lib/db-utils/server.ts @@ -14,7 +14,7 @@ import {DbLoadResult, HandleDatabasesOptions} from './common'; import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types'; import {Tree} from '../tests-tree-builder/base'; import {ReporterTestResult} from '../test-adapter'; -import {SqliteAdapter} from '../sqlite-adapter'; +import {SqliteClient} from '../sqlite-client'; export * from './common'; @@ -121,8 +121,8 @@ async function rewriteDatabaseUrls(dbPaths: string[], mainDatabaseUrls: string, }); } -export const getTestFromDb = (sqliteAdapter: SqliteAdapter, testResult: ReporterTestResult): T | undefined => { - return sqliteAdapter.query({ +export const getTestFromDb = (dbClient: SqliteClient, testResult: ReporterTestResult): T | undefined => { + return dbClient.query({ select: '*', where: `${DB_COLUMNS.SUITE_PATH} = ? AND ${DB_COLUMNS.NAME} = ? AND ${DB_COLUMNS.STATUS} = ?`, orderBy: DB_COLUMNS.TIMESTAMP, diff --git a/lib/errors/db-not-initialized-error.ts b/lib/errors/db-not-initialized-error.ts deleted file mode 100644 index a27a42c1a..000000000 --- a/lib/errors/db-not-initialized-error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class DbNotInitializedError extends Error { - constructor() { - super('Database must be initialized before use'); - } -} diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index 6769e3e65..6117dbcc0 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -12,9 +12,18 @@ import {createTestRunner} from './runner'; import {subscribeOnToolEvents} from './report-subscriber'; import {GuiReportBuilder, GuiReportBuilderResult} from '../../report-builder/gui'; import {EventSource} from '../event-source'; -import {logger, getShortMD5} from '../../common-utils'; +import {logger, getShortMD5, isUpdatedStatus} from '../../common-utils'; import * as reporterHelper from '../../reporter-helpers'; -import {UPDATED, SKIPPED, IDLE, TestStatus, ToolName, DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} from '../../constants'; +import { + UPDATED, + SKIPPED, + IDLE, + TestStatus, + ToolName, + DATABASE_URLS_JSON_NAME, + LOCAL_DATABASE_NAME, + PluginEvents, UNKNOWN_ATTEMPT +} from '../../constants'; import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils'; import {getTestsTreeFromDatabase} from '../../db-utils/server'; import {formatTestResult} from '../../server-utils'; @@ -33,6 +42,9 @@ import {Response} from 'express'; import {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui'; import {ReporterTestResult} from '../../test-adapter'; import {ImagesInfoFormatter} from '../../image-handler'; +import {SqliteClient} from '../../sqlite-client'; +import PQueue from 'p-queue'; +import os from 'os'; type ToolRunnerArgs = [paths: string[], hermione: Hermione & HtmlReporterApi, configs: GuiConfigs]; @@ -44,7 +56,7 @@ interface HermioneTestExtended extends HermioneTest { imagesInfo: Pick[]; } -type HermioneTestPlain = Pick; +type HermioneTestPlain = Pick; export interface UndoAcceptImagesResult { updatedImages: TreeImage[]; @@ -52,8 +64,13 @@ export interface UndoAcceptImagesResult { } // TODO: get rid of this function. It allows to format raw test, but is type-unsafe. -const formatTestResultUnsafe = (test: HermioneTest | HermioneTestExtended | HermioneTestPlain, status: TestStatus, {imageHandler}: {imageHandler: ImagesInfoFormatter}): ReporterTestResult => { - return formatTestResult(test as HermioneTestResult, status, {imageHandler}); +const formatTestResultUnsafe = ( + test: HermioneTest | HermioneTestExtended | HermioneTestPlain, + status: TestStatus, + attempt: number, + {imageHandler}: {imageHandler: ImagesInfoFormatter} +): ReporterTestResult => { + return formatTestResult(test as HermioneTestResult, status, attempt, {imageHandler}); }; export class ToolRunner { @@ -101,12 +118,14 @@ export class ToolRunner { async initialize(): Promise { await mergeDatabasesForReuse(this._reportPath); - this._reportBuilder = GuiReportBuilder.create(this._hermione.htmlReporter, this._pluginConfig, {reuse: true}); + const dbClient = await SqliteClient.create({htmlReporter: this._hermione.htmlReporter, reportPath: this._reportPath, reuse: true}); + + this._reportBuilder = GuiReportBuilder.create(this._hermione.htmlReporter, this._pluginConfig, {dbClient}); this._subscribeOnEvents(); this._collection = await this._readTests(); - await this._reportBuilder.init(); + this._hermione.htmlReporter.emit(PluginEvents.DATABASE_CREATED, dbClient.getRawConnection()); await this._reportBuilder.saveStaticFiles(); this._reportBuilder.setApiValues(this._hermione.htmlReporter.values); @@ -165,12 +184,12 @@ export class ToolRunner { return Promise.all(tests.map(async (test): Promise => { const updateResult = this._prepareTestResult(test); - const formattedResult = formatTestResultUnsafe(updateResult, UPDATED, reportBuilder); - const failResultId = formattedResult.id; - const updateAttempt = reportBuilder.getUpdatedAttempt(formattedResult); - formattedResult.attempt = updateAttempt; - updateResult.attempt = updateAttempt; + const formattedResultWithoutAttempt = formatTestResultUnsafe(updateResult, UPDATED, UNKNOWN_ATTEMPT, reportBuilder); + + const formattedResult = await reportBuilder.addUpdated(formattedResultWithoutAttempt); + + updateResult.attempt = formattedResult.attempt; await Promise.all(updateResult.imagesInfo.map(async (imageInfo) => { const {stateName} = imageInfo; @@ -181,26 +200,22 @@ export class ToolRunner { this._emitUpdateReference(result, stateName); })); - reportBuilder.addUpdated(formatTestResultUnsafe(updateResult, UPDATED, reportBuilder), failResultId); - return reportBuilder.getTestBranch(formattedResult.id); })); } async undoAcceptImages(tests: TestRefUpdateData[]): Promise { - const updatedImages: TreeImage[] = [], removedResults: string[] = []; + const updatedImages: TreeImage[] = [], removedResultIds: string[] = []; const reportBuilder = this._ensureReportBuilder(); await Promise.all(tests.map(async (test) => { const updateResult = this._prepareTestResult(test); - const formattedResult = formatTestResultUnsafe(updateResult, UPDATED, reportBuilder); - - formattedResult.attempt = updateResult.attempt; + const formattedResultWithoutAttempt = formatTestResultUnsafe(updateResult, UPDATED, UNKNOWN_ATTEMPT, reportBuilder); await Promise.all(updateResult.imagesInfo.map(async (imageInfo) => { const {stateName} = imageInfo; - const undoResultData = reportBuilder.undoAcceptImage(formattedResult, stateName); + const undoResultData = reportBuilder.undoAcceptImage(formattedResultWithoutAttempt, stateName); if (undoResultData === null) { return; } @@ -210,18 +225,19 @@ export class ToolRunner { removedResult, previousExpectedPath, shouldRemoveReference, - shouldRevertReference + shouldRevertReference, + newResult } = undoResultData; updatedImage && updatedImages.push(updatedImage); - removedResult && removedResults.push(removedResult); + removedResult && removedResultIds.push(removedResult.id); if (shouldRemoveReference) { - await reporterHelper.removeReferenceImage(formattedResult, stateName); + await reporterHelper.removeReferenceImage(newResult, stateName); } if (shouldRevertReference && removedResult) { - await reporterHelper.revertReferenceImage(removedResult, formattedResult, stateName); + await reporterHelper.revertReferenceImage(removedResult, newResult, stateName); } if (previousExpectedPath && (updateResult as HermioneTest).fullTitle) { @@ -233,7 +249,7 @@ export class ToolRunner { })); })); - return {updatedImages, removedResults}; + return {updatedImages, removedResults: removedResultIds}; } async findEqualDiffs(images: TestEqualDiffsData[]): Promise { @@ -288,6 +304,7 @@ export class ToolRunner { protected async _handleRunnableCollection(): Promise { const reportBuilder = this._ensureReportBuilder(); + const queue = new PQueue({concurrency: os.cpus().length}); this._ensureTestCollection().eachTest((test, browserId) => { if (test.disabled || this._isSilentlySkipped(test)) { @@ -298,11 +315,14 @@ export class ToolRunner { const testId = formatId(test.id.toString(), browserId); this._tests[testId] = _.extend(test, {browserId}); - test.pending - ? reportBuilder.addSkipped(formatTestResultUnsafe(test, SKIPPED, reportBuilder)) - : reportBuilder.addIdle(formatTestResultUnsafe(test, IDLE, reportBuilder)); + if (test.pending) { + queue.add(async () => reportBuilder.addSkipped(formatTestResultUnsafe(test, SKIPPED, UNKNOWN_ATTEMPT, reportBuilder))); + } else { + queue.add(async () => reportBuilder.addIdle(formatTestResultUnsafe(test, IDLE, UNKNOWN_ATTEMPT, reportBuilder))); + } }); + await queue.onIdle(); await this._fillTestsTree(); } @@ -311,7 +331,7 @@ export class ToolRunner { } protected _subscribeOnEvents(): void { - subscribeOnToolEvents(this._hermione, this._ensureReportBuilder(), this._eventSource, this._reportPath); + subscribeOnToolEvents(this._hermione, this._ensureReportBuilder(), this._eventSource); } protected _prepareTestResult(test: TestRefUpdateData): HermioneTestExtended | HermioneTestPlain { @@ -329,12 +349,19 @@ export class ToolRunner { const path = this._hermione.config.browsers[browserId].getScreenshotPath(rawTest, stateName); const refImg = {path, size: actualImg.size}; - assertViewResults.push({stateName, refImg, currImg: actualImg}); + assertViewResults.push({stateName, refImg, currImg: actualImg, isUpdated: isUpdatedStatus(imageInfo.status)}); return _.extend(imageInfo, {expectedImg: refImg}); }); - const res = _.merge({}, rawTest, {assertViewResults, imagesInfo, sessionId, attempt, meta: {url}, updated: true}); + const res = _.merge({}, rawTest, { + assertViewResults, + err: test.error, + imagesInfo, + sessionId, + attempt, + meta: {url} + }); // _.merge can't fully clone test object since hermione@7+ // TODO: use separate object to represent test results. Do not extend test object with test results @@ -356,7 +383,7 @@ export class ToolRunner { const {autoRun} = this._guiOpts; const testsTree = await this._loadDataFromDatabase(); - if (!_.isEmpty(testsTree)) { + if (testsTree && !_.isEmpty(testsTree)) { reportBuilder.reuseTestsTree(testsTree); } diff --git a/lib/gui/tool-runner/report-subscriber.ts b/lib/gui/tool-runner/report-subscriber.ts index 504659967..a307aaa1c 100644 --- a/lib/gui/tool-runner/report-subscriber.ts +++ b/lib/gui/tool-runner/report-subscriber.ts @@ -3,36 +3,19 @@ import PQueue from 'p-queue'; import Hermione from 'hermione'; import {ClientEvents} from '../constants'; import {getSuitePath} from '../../plugin-utils'; -import {createWorkers} from '../../workers/create-workers'; +import {createWorkers, CreateWorkersRunner} from '../../workers/create-workers'; import {logError, formatTestResult} from '../../server-utils'; -import {hasDiff} from '../../common-utils'; -import {TestStatus, RUNNING, SUCCESS, SKIPPED} from '../../constants'; +import {hasFailedImages} from '../../common-utils'; +import {TestStatus, RUNNING, SUCCESS, SKIPPED, UNKNOWN_ATTEMPT} from '../../constants'; import {GuiReportBuilder} from '../../report-builder/gui'; import {EventSource} from '../event-source'; -import {HermioneTestResult} from '../../types'; -import {HermioneTestAdapter, ReporterTestResult} from '../../test-adapter'; -import {ImageDiffError} from '../../errors'; +import {HermioneTestResult, ImageInfoFull} from '../../types'; -let workers: ReturnType; - -type CreateWorkersRunner = Parameters[0]; - -export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiReportBuilder, client: EventSource, reportPath: string): void => { +export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiReportBuilder, client: EventSource): void => { const queue = new PQueue({concurrency: os.cpus().length}); - const {imageHandler} = reportBuilder; - - async function failHandler(formattedResult: ReporterTestResult): Promise { - const actions: Promise[] = [imageHandler.saveTestImages(formattedResult, workers)]; - - if (formattedResult.errorDetails) { - actions.push((formattedResult as HermioneTestAdapter).saveErrorDetails(reportPath)); - } - - await Promise.all(actions); - } hermione.on(hermione.events.RUNNER_START, (runner) => { - workers = createWorkers(runner as unknown as CreateWorkersRunner); + reportBuilder.registerWorkers(createWorkers(runner as unknown as CreateWorkersRunner)); }); hermione.on(hermione.events.SUITE_BEGIN, (suite) => { @@ -47,22 +30,21 @@ export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiRepo }); hermione.on(hermione.events.TEST_BEGIN, (data) => { - const formattedResult = formatTestResult(data as HermioneTestResult, RUNNING, reportBuilder); - formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); + queue.add(async () => { + const formattedResultWithoutAttempt = formatTestResult(data as HermioneTestResult, RUNNING, UNKNOWN_ATTEMPT, reportBuilder); - reportBuilder.addRunning(formattedResult); - const testBranch = reportBuilder.getTestBranch(formattedResult.id); + const formattedResult = await reportBuilder.addRunning(formattedResultWithoutAttempt); + const testBranch = reportBuilder.getTestBranch(formattedResult.id); - return client.emit(ClientEvents.BEGIN_STATE, testBranch); + return client.emit(ClientEvents.BEGIN_STATE, testBranch); + }); }); hermione.on(hermione.events.TEST_PASS, (testResult) => { queue.add(async () => { - const formattedResult = formatTestResult(testResult, SUCCESS, reportBuilder); - formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); + const formattedResultWithoutAttempt = formatTestResult(testResult, SUCCESS, UNKNOWN_ATTEMPT, reportBuilder); - await imageHandler.saveTestImages(formattedResult, workers); - reportBuilder.addSuccess(formattedResult); + const formattedResult = await reportBuilder.addSuccess(formattedResultWithoutAttempt); const testBranch = reportBuilder.getTestBranch(formattedResult.id); client.emit(ClientEvents.TEST_RESULT, testBranch); @@ -71,12 +53,11 @@ export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiRepo hermione.on(hermione.events.RETRY, (testResult) => { queue.add(async () => { - const status = hasDiff(testResult.assertViewResults as ImageDiffError[]) ? TestStatus.FAIL : TestStatus.ERROR; - const formattedResult = formatTestResult(testResult, status, reportBuilder); - formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); + const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR; - await failHandler(formattedResult); - reportBuilder.addRetry(formattedResult); + const formattedResultWithoutAttempt = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder); + + const formattedResult = await reportBuilder.addRetry(formattedResultWithoutAttempt); const testBranch = reportBuilder.getTestBranch(formattedResult.id); client.emit(ClientEvents.TEST_RESULT, testBranch); @@ -85,14 +66,13 @@ export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiRepo hermione.on(hermione.events.TEST_FAIL, (testResult) => { queue.add(async () => { - const status = hasDiff(testResult.assertViewResults as ImageDiffError[]) ? TestStatus.FAIL : TestStatus.ERROR; - const formattedResult = formatTestResult(testResult, status, reportBuilder); - formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); + const status = hasFailedImages(testResult.assertViewResults as ImageInfoFull[]) ? TestStatus.FAIL : TestStatus.ERROR; + + const formattedResultWithoutAttempt = formatTestResult(testResult, status, UNKNOWN_ATTEMPT, reportBuilder); - await failHandler(formattedResult); - status === TestStatus.FAIL - ? reportBuilder.addFail(formattedResult) - : reportBuilder.addError(formattedResult); + const formattedResult = status === TestStatus.FAIL + ? await reportBuilder.addFail(formattedResultWithoutAttempt) + : await reportBuilder.addError(formattedResultWithoutAttempt); const testBranch = reportBuilder.getTestBranch(formattedResult.id); client.emit(ClientEvents.TEST_RESULT, testBranch); @@ -101,11 +81,9 @@ export const subscribeOnToolEvents = (hermione: Hermione, reportBuilder: GuiRepo hermione.on(hermione.events.TEST_PENDING, async (testResult) => { queue.add(async () => { - const formattedResult = formatTestResult(testResult as HermioneTestResult, SKIPPED, reportBuilder); - formattedResult.attempt = reportBuilder.getCurrAttempt(formattedResult); + const formattedResultWithoutAttempt = formatTestResult(testResult as HermioneTestResult, SKIPPED, UNKNOWN_ATTEMPT, reportBuilder); - await failHandler(formattedResult); - reportBuilder.addSkipped(formattedResult); + const formattedResult = await reportBuilder.addSkipped(formattedResultWithoutAttempt); const testBranch = reportBuilder.getTestBranch(formattedResult.id); client.emit(ClientEvents.TEST_RESULT, testBranch); diff --git a/lib/image-handler.ts b/lib/image-handler.ts index 791d07a40..c53badaf9 100644 --- a/lib/image-handler.ts +++ b/lib/image-handler.ts @@ -17,7 +17,7 @@ import { ImagesSaver, ImageInfoPageSuccess } from './types'; -import {ERROR, FAIL, PluginEvents, SUCCESS, TestStatus, UPDATED} from './constants'; +import {ERROR, FAIL, PluginEvents, SUCCESS, TestStatus, UNKNOWN_ATTEMPT, UPDATED} from './constants'; import { getError, getShortMD5, @@ -142,7 +142,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { const imagesInfo: ImageInfoFull[] = testResult.assertViewResults?.map((assertResult): ImageInfoFull => { let status: TestStatus, error: {message: string; stack?: string;} | undefined; - if (testResult.isUpdated === true) { + if (assertResult.isUpdated === true) { status = UPDATED; } else if (isImageDiffError(assertResult)) { status = FAIL; @@ -258,33 +258,38 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { private _getExpectedPath(testResult: ReporterTestResultPlain, stateName?: string): {path: string, reused: boolean} { const key = this._getExpectedKey(testResult, stateName); + let result: {path: string; reused: boolean}; if (testResult.status === UPDATED) { const expectedPath = utils.getReferencePath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName}); - cacheExpectedPaths.set(key, expectedPath); - - return {path: expectedPath, reused: false}; - } - if (cacheExpectedPaths.has(key)) { - return {path: cacheExpectedPaths.get(key) as string, reused: true}; - } - - const imageInfo = this._imageStore.getLastImageInfoFromDb(testResult, stateName); + result = {path: expectedPath, reused: false}; + } else if (cacheExpectedPaths.has(key)) { + result = {path: cacheExpectedPaths.get(key) as string, reused: true}; + } else { + const imageInfo = this._imageStore.getLastImageInfoFromDb(testResult, stateName); - if (imageInfo && (imageInfo as ImageInfoFail).expectedImg) { - const expectedPath = (imageInfo as ImageInfoFail).expectedImg.path; + if (imageInfo && (imageInfo as ImageInfoFail).expectedImg) { + const expectedPath = (imageInfo as ImageInfoFail).expectedImg.path; - cacheExpectedPaths.set(key, expectedPath); - - return {path: expectedPath, reused: true}; + result = {path: expectedPath, reused: true}; + } else { + const expectedPath = utils.getReferencePath({ + attempt: testResult.attempt, + browserId: testResult.browserId, + imageDir: testResult.imageDir, + stateName + }); + + result = {path: expectedPath, reused: false}; + } } - const expectedPath = utils.getReferencePath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName}); - - cacheExpectedPaths.set(key, expectedPath); + if (testResult.attempt !== UNKNOWN_ATTEMPT) { + cacheExpectedPaths.set(key, result.path); + } - return {path: expectedPath, reused: false}; + return result; } private _getImgFromStorage(imgPath: string): string { diff --git a/lib/image-store.ts b/lib/image-store.ts index bdbb6db79..5970f0ea3 100644 --- a/lib/image-store.ts +++ b/lib/image-store.ts @@ -1,5 +1,5 @@ import {DB_COLUMNS} from './constants'; -import {SqliteAdapter} from './sqlite-adapter'; +import {SqliteClient} from './sqlite-client'; import {ImageInfo, ImageInfoFull, LabeledSuitesRow} from './types'; import {ReporterTestResultPlain} from './image-handler'; @@ -8,10 +8,10 @@ export interface ImageStore { } export class SqliteImageStore implements ImageStore { - private _sqliteAdapter: SqliteAdapter; + private _dbClient: SqliteClient; - constructor(sqliteAdapter: SqliteAdapter) { - this._sqliteAdapter = sqliteAdapter; + constructor(dbClient: SqliteClient) { + this._dbClient = dbClient; } getLastImageInfoFromDb(testResult: ReporterTestResultPlain, stateName?: string): ImageInfo | undefined { @@ -19,7 +19,7 @@ export class SqliteImageStore implements ImageStore { const suitePath = testResult.testPath; const suitePathString = JSON.stringify(suitePath); - const imagesInfoResult = this._sqliteAdapter.query | undefined>({ + const imagesInfoResult = this._dbClient.query | undefined>({ select: DB_COLUMNS.IMAGES_INFO, where: `${DB_COLUMNS.SUITE_PATH} = ? AND ${DB_COLUMNS.NAME} = ?`, orderBy: DB_COLUMNS.TIMESTAMP, diff --git a/lib/plugin-adapter.ts b/lib/plugin-adapter.ts deleted file mode 100644 index 4c08c3e08..000000000 --- a/lib/plugin-adapter.ts +++ /dev/null @@ -1,83 +0,0 @@ -import _ from 'lodash'; -import type Hermione from 'hermione'; -import {CommanderStatic} from '@gemini-testing/commander'; - -import {parseConfig} from './config'; -import {StaticReportBuilder} from './report-builder/static'; -import * as utils from './server-utils'; -import {cliCommands} from './cli-commands'; -import {HtmlReporter} from './plugin-api'; -import {HtmlReporterApi, ReporterConfig, ReporterOptions} from './types'; -import {ToolName} from './constants'; - -type PrepareFn = (hermione: Hermione & HtmlReporterApi, reportBuilder: StaticReportBuilder, config: ReporterConfig) => Promise; - -export class PluginAdapter { - protected _hermione: Hermione & HtmlReporterApi; - protected _config: ReporterConfig; - - static create( - this: new (hermione: Hermione & HtmlReporterApi, opts: Partial) => T, - hermione: Hermione & HtmlReporterApi, - opts: Partial - ): T { - return new this(hermione, opts); - } - - constructor(hermione: Hermione & HtmlReporterApi, opts: Partial) { - this._hermione = hermione; - this._config = parseConfig(opts); - } - - isEnabled(): boolean { - return this._config.enabled; - } - - addApi(): this { - this._hermione.htmlReporter = HtmlReporter.create(this._config, {toolName: ToolName.Hermione}); - return this; - } - - addCliCommands(): this { - _.values(cliCommands).forEach((command: string) => { - this._hermione.on(this._hermione.events.CLI, (commander: CommanderStatic) => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require(`./cli-commands/${command}`)(commander, this._config, this._hermione); - commander.prependListener(`command:${command}`, () => this._run = _.noop as typeof this._run); - }); - }); - - return this; - } - - init(prepareData: PrepareFn): this { - this._hermione.on(this._hermione.events.INIT, () => this._run(prepareData)); - return this; - } - - protected async _createStaticReportBuilder(prepareData: PrepareFn): Promise { - const staticReportBuilder = StaticReportBuilder.create(this._hermione.htmlReporter, this._config); - - await staticReportBuilder.init(); - - return Promise - .all([ - staticReportBuilder.saveStaticFiles(), - prepareData(this._hermione, staticReportBuilder, this._config) - ]) - .then(() => staticReportBuilder.finalize()) - .then(async () => { - const htmlReporter = this._hermione.htmlReporter; - - await htmlReporter.emitAsync(htmlReporter.events.REPORT_SAVED, {reportPath: this._config.path}); - }); - } - - protected async _run(prepareData: PrepareFn): Promise { - const generateReport = this._createStaticReportBuilder(prepareData); - - this._hermione.on(this._hermione.events.RUNNER_END, () => - generateReport.then(() => utils.logPathToHtmlReport(this._config)).catch(utils.logError) - ); - } -} diff --git a/lib/report-builder/gui.ts b/lib/report-builder/gui.ts index e7b42ef22..074406f41 100644 --- a/lib/report-builder/gui.ts +++ b/lib/report-builder/gui.ts @@ -2,23 +2,25 @@ import * as _ from 'lodash'; import {StaticReportBuilder} from './static'; import {GuiTestsTreeBuilder, TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../tests-tree-builder/gui'; import { - IDLE, RUNNING, SKIPPED, FAIL, SUCCESS, UPDATED, TestStatus, DB_COLUMNS, ToolName, ERROR + IDLE, RUNNING, UPDATED, TestStatus, DB_COLUMNS, ToolName, HERMIONE_TITLE_DELIMITER } from '../constants'; import {ConfigForStaticFile, getConfigForStaticFile} from '../server-utils'; import {ReporterTestResult} from '../test-adapter'; -import {PreparedTestResult} from '../sqlite-adapter'; -import {Tree, TreeImage, TreeResult} from '../tests-tree-builder/base'; +import {PreparedTestResult} from '../sqlite-client'; +import {Tree, TreeImage} from '../tests-tree-builder/base'; import {ImageInfoWithState, ReporterConfig} from '../types'; -import {hasDiff, hasNoRefImageErrors, hasResultFails, isSkippedStatus, isUpdatedStatus} from '../common-utils'; +import {isUpdatedStatus} from '../common-utils'; import {HtmlReporterValues} from '../plugin-api'; import {SkipItem} from '../tests-tree-builder/static'; +import {copyAndUpdate} from '../test-adapter/utils'; interface UndoAcceptImageResult { updatedImage: TreeImage | undefined; - removedResult: string | undefined; + removedResult: ReporterTestResult | undefined; previousExpectedPath: string | null; shouldRemoveReference: boolean; shouldRevertReference: boolean; + newResult: ReporterTestResult; } export interface GuiReportBuilderResult { @@ -41,16 +43,16 @@ export class GuiReportBuilder extends StaticReportBuilder { this._skips = []; } - addIdle(result: ReporterTestResult): ReporterTestResult { + async addIdle(result: ReporterTestResult): Promise { return this._addTestResult(result, {status: IDLE}); } - addRunning(result: ReporterTestResult): ReporterTestResult { + async addRunning(result: ReporterTestResult): Promise { return this._addTestResult(result, {status: RUNNING}); } - addSkipped(result: ReporterTestResult): ReporterTestResult { - const formattedResult = super.addSkipped(result); + override async addSkipped(result: ReporterTestResult): Promise { + const formattedResult = await super.addSkipped(result); const { fullName: suite, skipReason: comment, @@ -62,8 +64,8 @@ export class GuiReportBuilder extends StaticReportBuilder { return formattedResult; } - addUpdated(result: ReporterTestResult, failResultId: string): ReporterTestResult { - return this._addTestResult(result, {status: UPDATED}, {failResultId}); + async addUpdated(result: ReporterTestResult): Promise { + return this._addTestResult(result, {status: UPDATED}); } setApiValues(values: HtmlReporterValues): this { @@ -73,6 +75,14 @@ export class GuiReportBuilder extends StaticReportBuilder { reuseTestsTree(tree: Tree): void { this._testsTree.reuseTestsTree(tree); + + // Fill test attempt manager with data from db + for (const [, testResult] of Object.entries(tree.results.byId)) { + this._testAttemptManager.registerAttempt({ + fullName: testResult.suitePath.join(HERMIONE_TITLE_DELIMITER), + browserId: testResult.name + }, testResult.status, testResult.attempt); + } } getResult(): GuiReportBuilderResult { @@ -102,31 +112,14 @@ export class GuiReportBuilder extends StaticReportBuilder { return this._testsTree.getImageDataToFindEqualDiffs(imageIds); } - getCurrAttempt(formattedResult: ReporterTestResult): number { - const lastResult = this._testsTree.getLastResult(formattedResult); - this._checkResult(lastResult, formattedResult); - - const {status, attempt} = lastResult; - - return [IDLE, RUNNING, SKIPPED].includes(status) ? attempt : attempt + 1; - } - - getUpdatedAttempt(formattedResult: ReporterTestResult): number { - const lastResult = this._testsTree.getLastResult(formattedResult); - this._checkResult(lastResult, formattedResult); - - const {attempt} = lastResult; + undoAcceptImage(testResultWithoutAttempt: ReporterTestResult, stateName: string): UndoAcceptImageResult | null { + const attempt = this._testAttemptManager.getCurrentAttempt(testResultWithoutAttempt); + const imagesInfoFormatter = this.imageHandler; + const testResult = copyAndUpdate(testResultWithoutAttempt, {attempt}, {imagesInfoFormatter}); - const imagesInfo = this._testsTree.getImagesInfo(formattedResult.id); - const isUpdated = imagesInfo.some((image) => image.status === UPDATED); - - return isUpdated ? attempt : attempt + 1; - } - - undoAcceptImage(formattedResult: ReporterTestResult, stateName: string): UndoAcceptImageResult | null { - const resultId = formattedResult.id; - const suitePath = formattedResult.testPath; - const browserName = formattedResult.browserId; + const resultId = testResult.id; + const suitePath = testResult.testPath; + const browserName = testResult.browserId; const resultData = this._testsTree.getResultDataToUnacceptImage(resultId, stateName); if (!resultData || !isUpdatedStatus(resultData.status)) { @@ -146,17 +139,19 @@ export class GuiReportBuilder extends StaticReportBuilder { const shouldRemoveReference = _.isNull(previousImageRefImgSize); const shouldRevertReference = !shouldRemoveReference; - let updatedImage, removedResult; + let updatedImage: TreeImage | undefined, removedResult: ReporterTestResult | undefined; if (shouldRemoveResult) { this._testsTree.removeTestResult(resultId); - formattedResult.attempt = formattedResult.attempt - 1; + this._testAttemptManager.removeAttempt(testResult); - removedResult = resultId; + removedResult = testResult; } else { updatedImage = this._testsTree.updateImageInfo(imageId, previousImage); } + const newResult = copyAndUpdate(testResult, {attempt: this._testAttemptManager.getCurrentAttempt(testResult)}, {imagesInfoFormatter}); + this._deleteTestResultFromDb({where: [ `${DB_COLUMNS.SUITE_PATH} = ?`, `${DB_COLUMNS.NAME} = ?`, @@ -165,11 +160,11 @@ export class GuiReportBuilder extends StaticReportBuilder { `json_extract(${DB_COLUMNS.IMAGES_INFO}, '$[0].stateName') = ?` ].join(' AND ')}, JSON.stringify(suitePath), browserName, status, timestamp.toString(), stateName); - return {updatedImage, removedResult, previousExpectedPath, shouldRemoveReference, shouldRevertReference}; + return {updatedImage, removedResult, previousExpectedPath, shouldRemoveReference, shouldRevertReference, newResult}; } - protected override _addTestResult(formattedResult: ReporterTestResult, props: {status: TestStatus} & Partial, opts: {failResultId?: string} = {}): ReporterTestResult { - super._addTestResult(formattedResult, props); + protected override async _addTestResult(formattedResultOriginal: ReporterTestResult, props: {status: TestStatus} & Partial): Promise { + const formattedResult = await super._addTestResult(formattedResultOriginal, props); const testResult = this._createTestResult(formattedResult, { ...props, @@ -177,56 +172,24 @@ export class GuiReportBuilder extends StaticReportBuilder { attempt: formattedResult.attempt }); - this._extendTestWithImagePaths(testResult, formattedResult, opts); - - if (![IDLE, RUNNING].includes(testResult.status)) { - this._updateTestResultStatus(testResult, formattedResult); - } + this._extendTestWithImagePaths(testResult, formattedResult); this._testsTree.addTestResult(testResult, formattedResult); return formattedResult; } - protected _checkResult(result: TreeResult | undefined, formattedResult: ReporterTestResult): asserts result is TreeResult { - if (!result) { - const filteredTestTreeResults = _.pickBy( - this._testsTree.tree.results.byId, - (_result, resultId) => resultId.startsWith(formattedResult.fullName)); - - throw new Error('Failed to get last result for test:\n' + - `fullName = ${formattedResult.fullName}; browserId = ${formattedResult.browserId}\n` + - `Related testsTree results: ${JSON.stringify(filteredTestTreeResults)}\n`); - } - } - - private _updateTestResultStatus(testResult: PreparedTestResult, formattedResult: ReporterTestResult): void { - if (!hasResultFails(testResult) && !isSkippedStatus(testResult.status)) { - testResult.status = SUCCESS; - return; - } - - const imageErrors = testResult.imagesInfo.map(imagesInfo => (imagesInfo as {error: {name?: string}}).error ?? {}); - if (hasDiff(imageErrors) || hasNoRefImageErrors({assertViewResults: imageErrors})) { - testResult.status = FAIL; - return; - } - - if (!_.isEmpty(formattedResult.error)) { - testResult.status = ERROR; - return; - } - } - - private _extendTestWithImagePaths(testResult: PreparedTestResult, formattedResult: ReporterTestResult, opts: {failResultId?: string} = {}): void { + private _extendTestWithImagePaths(testResult: PreparedTestResult, formattedResult: ReporterTestResult): void { const newImagesInfo = formattedResult.imagesInfo; + const imagesInfoFormatter = this._imageHandler; if (testResult.status !== UPDATED) { _.set(testResult, 'imagesInfo', newImagesInfo); return; } - const failImagesInfo = opts.failResultId ? this._testsTree.getImagesInfo(opts.failResultId) : []; + const failResultId = copyAndUpdate(formattedResult, {attempt: formattedResult.attempt - 1}, {imagesInfoFormatter}).id; + const failImagesInfo = this._testsTree.getImagesInfo(failResultId); if (failImagesInfo.length) { testResult.imagesInfo = _.clone(failImagesInfo); diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts index 5ce2e56c6..ee7bb8743 100644 --- a/lib/report-builder/static.ts +++ b/lib/report-builder/static.ts @@ -12,11 +12,11 @@ import { SUCCESS, TestStatus, LOCAL_DATABASE_NAME, - PluginEvents + PluginEvents, UNKNOWN_ATTEMPT, UPDATED } from '../constants'; -import {PreparedTestResult, SqliteAdapter} from '../sqlite-adapter'; +import type {PreparedTestResult, SqliteClient} from '../sqlite-client'; import {ReporterTestResult} from '../test-adapter'; -import {hasImage, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils'; +import {hasImage, saveErrorDetails, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils'; import {ReporterConfig} from '../types'; import {HtmlReporter} from '../plugin-api'; import {ImageHandler} from '../image-handler'; @@ -24,41 +24,46 @@ import {SqliteImageStore} from '../image-store'; import {getUrlWithBase, getError, getRelativeUrl, hasDiff, hasNoRefImageErrors} from '../common-utils'; import {getTestFromDb} from '../db-utils/server'; import {ImageDiffError} from '../errors'; +import {TestAttemptManager} from '../test-attempt-manager'; +import {copyAndUpdate} from '../test-adapter/utils'; +import {RegisterWorkers} from '../workers/create-workers'; const ignoredStatuses = [RUNNING, IDLE]; interface StaticReportBuilderOptions { - reuse: boolean; + dbClient: SqliteClient; } export class StaticReportBuilder { protected _htmlReporter: HtmlReporter; protected _pluginConfig: ReporterConfig; - protected _sqliteAdapter: SqliteAdapter; + protected _dbClient: SqliteClient; protected _imageHandler: ImageHandler; + protected _testAttemptManager: TestAttemptManager; + private _workers: RegisterWorkers<['saveDiffTo']> | null; static create( - this: new (htmlReporter: HtmlReporter, pluginConfig: ReporterConfig, options?: Partial) => T, + this: new (htmlReporter: HtmlReporter, pluginConfig: ReporterConfig, options: StaticReportBuilderOptions) => T, htmlReporter: HtmlReporter, pluginConfig: ReporterConfig, - options?: Partial + options: StaticReportBuilderOptions ): T { return new this(htmlReporter, pluginConfig, options); } - constructor(htmlReporter: HtmlReporter, pluginConfig: ReporterConfig, {reuse = false}: Partial = {}) { + constructor(htmlReporter: HtmlReporter, pluginConfig: ReporterConfig, {dbClient}: StaticReportBuilderOptions) { this._htmlReporter = htmlReporter; this._pluginConfig = pluginConfig; - this._sqliteAdapter = SqliteAdapter.create({ - htmlReporter: this._htmlReporter, - reportPath: this._pluginConfig.path, - reuse - }); + this._dbClient = dbClient; + + this._testAttemptManager = new TestAttemptManager(); - const imageStore = new SqliteImageStore(this._sqliteAdapter); + const imageStore = new SqliteImageStore(this._dbClient); this._imageHandler = new ImageHandler(imageStore, htmlReporter.imagesSaver, {reportPath: pluginConfig.path}); + this._workers = null; + this._htmlReporter.on(PluginEvents.IMAGES_SAVER_UPDATED, (newImagesSaver) => { this._imageHandler.setImagesSaver(newImagesSaver); }); @@ -70,10 +75,6 @@ export class StaticReportBuilder { return this._imageHandler; } - async init(): Promise { - await this._sqliteAdapter.init(); - } - async saveStaticFiles(): Promise { const destPath = this._pluginConfig.path; @@ -83,26 +84,26 @@ export class StaticReportBuilder { ]); } - addSkipped(result: ReporterTestResult): ReporterTestResult { + async addSkipped(result: ReporterTestResult): Promise { return this._addTestResult(result, { status: SKIPPED, skipReason: result.skipReason }); } - addSuccess(result: ReporterTestResult): ReporterTestResult { + async addSuccess(result: ReporterTestResult): Promise { return this._addTestResult(result, {status: SUCCESS}); } - addFail(result: ReporterTestResult): ReporterTestResult { + async addFail(result: ReporterTestResult): Promise { return this._addFailResult(result); } - addError(result: ReporterTestResult): ReporterTestResult { + async addError(result: ReporterTestResult): Promise { return this._addErrorResult(result); } - addRetry(result: ReporterTestResult): ReporterTestResult { + async addRetry(result: ReporterTestResult): Promise { if (hasDiff(result.assertViewResults as ImageDiffError[])) { return this._addFailResult(result); } else { @@ -110,15 +111,64 @@ export class StaticReportBuilder { } } - protected _addFailResult(formattedResult: ReporterTestResult): ReporterTestResult { + registerWorkers(workers: RegisterWorkers<['saveDiffTo']>): void { + this._workers = workers; + } + + private _ensureWorkers(): RegisterWorkers<['saveDiffTo']> { + if (!this._workers) { + throw new Error('You must register workers before using report builder.' + + 'Make sure registerWorkers() was called before adding any test results.'); + } + + return this._workers; + } + + /** If passed test result doesn't have attempt, this method registers new attempt and sets attempt number */ + private _provideAttempt(testResultOriginal: ReporterTestResult): ReporterTestResult { + let formattedResult = testResultOriginal; + + if (testResultOriginal.attempt === UNKNOWN_ATTEMPT) { + const imagesInfoFormatter = this._imageHandler; + const attempt = this._testAttemptManager.registerAttempt(testResultOriginal, testResultOriginal.status); + formattedResult = copyAndUpdate(testResultOriginal, {attempt}, {imagesInfoFormatter}); + } + + return formattedResult; + } + + private async _saveTestResultData(testResult: ReporterTestResult): Promise { + if ([IDLE, RUNNING, UPDATED].includes(testResult.status)) { + return; + } + + const actions: Promise[] = []; + + if (!_.isEmpty(testResult.assertViewResults)) { + actions.push(this._imageHandler.saveTestImages(testResult, this._ensureWorkers())); + } + + if (this._pluginConfig.saveErrorDetails && testResult.errorDetails) { + actions.push(saveErrorDetails(testResult, this._pluginConfig.path)); + } + + await Promise.all(actions); + } + + protected async _addFailResult(formattedResult: ReporterTestResult): Promise { return this._addTestResult(formattedResult, {status: FAIL}); } - protected _addErrorResult(formattedResult: ReporterTestResult): ReporterTestResult { + protected async _addErrorResult(formattedResult: ReporterTestResult): Promise { return this._addTestResult(formattedResult, {status: ERROR}); } - protected _addTestResult(formattedResult: ReporterTestResult, props: {status: TestStatus} & Partial): ReporterTestResult { + protected async _addTestResult(formattedResultOriginal: ReporterTestResult, props: {status: TestStatus} & Partial): Promise { + const formattedResult = this._provideAttempt(formattedResultOriginal); + + // Test result data has to be saved before writing to db, because user may save data to custom location + await this._saveTestResultData(formattedResult); + formattedResult.image = hasImage(formattedResult); const testResult = this._createTestResult(formattedResult, _.extend(props, { @@ -130,19 +180,19 @@ export class StaticReportBuilder { } // To prevent skips duplication on reporter startup - const isPreviouslySkippedTest = testResult.status === SKIPPED && getTestFromDb(this._sqliteAdapter, formattedResult); + const isPreviouslySkippedTest = testResult.status === SKIPPED && getTestFromDb(this._dbClient, formattedResult); if (!ignoredStatuses.includes(testResult.status) && !isPreviouslySkippedTest) { - this._writeTestResultToDb(testResult, formattedResult); + this._dbClient.write(formattedResult); } return formattedResult; } - protected _createTestResult(result: ReporterTestResult, props: {attempt?: number, status: TestStatus, timestamp: number;} & Partial): PreparedTestResult { + protected _createTestResult(result: ReporterTestResult, props: {attempt?: number | null, status: TestStatus, timestamp: number;} & Partial): PreparedTestResult { const { browserId, file, sessionId, description, history, - imagesInfo = [], screenshot, multipleTabs, errorDetails + imagesInfo = [], screenshot, multipleTabs, errorDetails, testPath } = result; const {baseHost, saveErrorDetails} = this._pluginConfig; @@ -152,7 +202,8 @@ export class StaticReportBuilder { const testResult: PreparedTestResult = Object.assign({ suiteUrl, name: browserId, metaInfo, description, history, - imagesInfo, screenshot: Boolean(screenshot), multipleTabs + imagesInfo, screenshot: Boolean(screenshot), multipleTabs, + suitePath: testPath, suiteName: _.last(testPath) as string }, props); const error = getError(result.error); @@ -167,19 +218,12 @@ export class StaticReportBuilder { return testResult; } - protected _writeTestResultToDb(testResult: PreparedTestResult, formattedResult: ReporterTestResult): void { - const suiteName = formattedResult.state.name; - const suitePath = formattedResult.testPath; - - this._sqliteAdapter.write({testResult, suitePath, suiteName}); - } - - protected _deleteTestResultFromDb(...args: Parameters): void { - this._sqliteAdapter.delete(...args); + protected _deleteTestResultFromDb(...args: Parameters): void { + this._dbClient.delete(...args); } async finalize(): Promise { - this._sqliteAdapter.close(); + this._dbClient.close(); const reportsSaver = this._htmlReporter.reportsSaver; diff --git a/lib/reporter-helpers.ts b/lib/reporter-helpers.ts index f0d278a41..f5d4ec2a2 100644 --- a/lib/reporter-helpers.ts +++ b/lib/reporter-helpers.ts @@ -29,10 +29,11 @@ export const updateReferenceImage = async (testResult: ReporterTestResult, repor ]); }; -export const revertReferenceImage = async (referenceId: string, testResult: ReporterTestResult, stateName: string): Promise => { +export const revertReferenceImage = async (removedResult: ReporterTestResult, newResult: ReporterTestResult, stateName: string): Promise => { + const referenceId = removedResult.id; const referenceHash = mkReferenceHash(referenceId, stateName); const oldReferencePath = path.resolve(tmp.tmpdir, referenceHash); - const referencePath = ImageHandler.getRefImg(testResult.assertViewResults, stateName)?.path; + const referencePath = ImageHandler.getRefImg(newResult.assertViewResults, stateName)?.path; if (!referencePath) { return; diff --git a/lib/server-utils.ts b/lib/server-utils.ts index cf60419a2..1db16430e 100644 --- a/lib/server-utils.ts +++ b/lib/server-utils.ts @@ -311,6 +311,25 @@ export function mapPlugins(plugins: ReporterConfig['plugins'], callback: (nam return result; } -export const formatTestResult = (rawResult: HermioneTestResult, status: TestStatus, {imageHandler}: {imageHandler: ImagesInfoFormatter}): ReporterTestResult => { - return new HermioneTestAdapter(rawResult, {status, imagesInfoFormatter: imageHandler}); +export const formatTestResult = ( + rawResult: HermioneTestResult, + status: TestStatus, + attempt: number, + {imageHandler}: {imageHandler: ImagesInfoFormatter} +): ReporterTestResult => { + return new HermioneTestAdapter(rawResult, {attempt, status, imagesInfoFormatter: imageHandler}); +}; + +export const saveErrorDetails = async (testResult: ReporterTestResult, reportPath: string): Promise => { + if (!testResult.errorDetails) { + return; + } + + const detailsFilePath = path.resolve(reportPath, testResult.errorDetails.filePath); + const detailsData = _.isObject(testResult.errorDetails.data) + ? JSON.stringify(testResult.errorDetails.data, null, 2) + : testResult.errorDetails.data; + + await makeDirFor(detailsFilePath); + await fs.writeFile(detailsFilePath, detailsData); }; diff --git a/lib/sqlite-adapter.ts b/lib/sqlite-client.ts similarity index 57% rename from lib/sqlite-adapter.ts rename to lib/sqlite-client.ts index 3e866a695..4575056f2 100644 --- a/lib/sqlite-adapter.ts +++ b/lib/sqlite-client.ts @@ -5,14 +5,14 @@ import fs from 'fs-extra'; import NestedError from 'nested-error-stacks'; import {getShortMD5} from './common-utils'; -import {TestStatus} from './constants'; -import {DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME} from './constants/database'; +import {TestStatus, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, LOCAL_DATABASE_NAME, DATABASE_URLS_JSON_NAME} from './constants'; import {createTablesQuery} from './db-utils/common'; -import {DbNotInitializedError} from './errors/db-not-initialized-error'; -import type {ErrorDetails, ImageInfoFull} from './types'; +import type {ErrorDetails, ImageInfoFull, TestError} from './types'; import {HtmlReporter} from './plugin-api'; +import {ReporterTestResult} from './test-adapter'; +import {DbTestResultTransformer} from './test-adapter/transformers/db'; -const debug = makeDebug('html-reporter:sqlite-adapter'); +const debug = makeDebug('html-reporter:sqlite-client'); interface QueryParams { select?: string; @@ -44,74 +44,87 @@ export interface PreparedTestResult { status: TestStatus; timestamp: number; errorDetails?: ErrorDetails; + suitePath: string[]; + suiteName: string; } -interface ParseTestResultParams { +export interface DbTestResult { + description?: string | null; + error?: TestError; + history: string[]; + imagesInfo: ImageInfoFull[]; + metaInfo: Record; + multipleTabs: boolean; + /* Browser name. Example: `"chrome"` */ + name: string; + screenshot: boolean; + skipReason?: string; + status: TestStatus; + /* Last part of `suitePath`. Example: `"Test 1"` */ suiteName: string; + /* Segments of full test name. Example: `["Title", "Test 1"]` */ suitePath: string[]; - testResult: PreparedTestResult; -} - -interface ParsedTestResult extends PreparedTestResult { - suiteName: ParseTestResultParams['suiteName']; - suitePath: ParseTestResultParams['suitePath']; + suiteUrl: string; + /* Unix time in ms. Example: `1700563430266` */ + timestamp: number; } -interface SqliteAdapterOptions { +interface SqliteClientOptions { + // TODO: get rid of htmlReporter in the future htmlReporter: HtmlReporter; reportPath: string; reuse?: boolean; } -export class SqliteAdapter { - private _htmlReporter: HtmlReporter; - private _reportPath: string; - private _reuse: boolean; - private _db: null | Database.Database; +export class SqliteClient { + private readonly _db: Database.Database; private _queryCache: Map; + private _transformer: DbTestResultTransformer; - static create(this: new (options: SqliteAdapterOptions) => T, options: SqliteAdapterOptions): T { - return new this(options); - } - - constructor({htmlReporter, reportPath, reuse = false}: SqliteAdapterOptions) { - this._htmlReporter = htmlReporter; - this._reportPath = reportPath; - this._reuse = reuse; - this._db = null; - this._queryCache = new Map(); - } + static async create( + this: { new(db: Database.Database, transformer: DbTestResultTransformer): T }, + options: SqliteClientOptions + ): Promise { + const {htmlReporter, reportPath} = options; + const dbPath = path.resolve(reportPath, LOCAL_DATABASE_NAME); + const dbUrlsJsonPath = path.resolve(reportPath, DATABASE_URLS_JSON_NAME); - async init(): Promise { - const dbPath = path.resolve(this._reportPath, LOCAL_DATABASE_NAME); - const dbUrlsJsonPath = path.resolve(this._reportPath, DATABASE_URLS_JSON_NAME); + let db: Database.Database; try { - if (!this._reuse) { + if (!options.reuse) { await Promise.all([ fs.remove(dbPath), fs.remove(dbUrlsJsonPath) ]); } - await fs.ensureDir(this._reportPath); + await fs.ensureDir(reportPath); - this._db = new Database(dbPath); + db = new Database(dbPath); debug('db connection opened'); - createTablesQuery().forEach((query) => this._db?.prepare(query).run()); - - this._htmlReporter.emit(this._htmlReporter.events.DATABASE_CREATED, this._db); + createTablesQuery().forEach((query) => db?.prepare(query).run()); } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any throw new NestedError(`Error creating database at "${dbPath}"`, err); } + + const transformer = new DbTestResultTransformer({baseHost: htmlReporter.config.baseHost}); + + return new this(db, transformer); } - close(): void { - if (!this._db) { - throw new DbNotInitializedError(); - } + constructor(db: Database.Database, transformer: DbTestResultTransformer) { + this._db = db; + this._queryCache = new Map(); + this._transformer = transformer; + } + + getRawConnection(): Database.Database { + return this._db; + } + close(): void { this._db.prepare('VACUUM').run(); debug('db connection closed'); @@ -119,10 +132,6 @@ export class SqliteAdapter { } query(queryParams: QueryParams = {}, ...queryArgs: string[]): T { - if (!this._db) { - throw new DbNotInitializedError(); - } - const {select, where, orderBy, orderDescending, limit, noCache = false} = queryParams; const cacheKey = (!noCache && getShortMD5(`${select}#${where}#${orderBy}${orderDescending}${queryArgs.join('#')}`)) as string; @@ -142,23 +151,15 @@ export class SqliteAdapter { return result as T; } - write(testResult: ParseTestResultParams): void { - if (!this._db) { - throw new DbNotInitializedError(); - } - - const testResultObj = this._parseTestResult(testResult); - const values = this._createValuesArray(testResultObj); + write(testResult: ReporterTestResult): void { + const dbTestResult = this._transformer.transform(testResult); + const values = this._createValuesArray(dbTestResult); const placeholders = values.map(() => '?').join(', '); this._db.prepare(`INSERT INTO ${DB_SUITES_TABLE_NAME} VALUES (${placeholders})`).run(...values); } delete(deleteParams: DeleteParams = {}, ...deleteArgs: string[]): void { - if (!this._db) { - throw new DbNotInitializedError(); - } - const sentence = `DELETE FROM ${DB_SUITES_TABLE_NAME}` + this._createSentence(deleteParams); @@ -183,43 +184,9 @@ export class SqliteAdapter { return sentence; } - _parseTestResult({suitePath, suiteName, testResult}: ParseTestResultParams): ParsedTestResult { - const { - name, - suiteUrl, - metaInfo, - history, - description, - error, - skipReason, - imagesInfo, - screenshot, - multipleTabs, - status, - timestamp = Date.now() - } = testResult; - - return { - suitePath, - suiteName, - name, - suiteUrl, - metaInfo, - history, - description, - error, - skipReason, - imagesInfo, - screenshot, - multipleTabs, - status, - timestamp - }; - } - - private _createValuesArray(testResult: PreparedTestResult): (string | number | null)[] { + private _createValuesArray(testResult: DbTestResult): (string | number | null)[] { return SUITES_TABLE_COLUMNS.reduce<(string | number | null)[]>((acc, {name}) => { - const value = testResult[name as keyof PreparedTestResult]; + const value = testResult[name as keyof DbTestResult]; if (value === undefined || value === null) { acc.push(null); return acc; diff --git a/lib/static/components/retry-switcher/item.jsx b/lib/static/components/retry-switcher/item.jsx index 326d46172..802b88c54 100644 --- a/lib/static/components/retry-switcher/item.jsx +++ b/lib/static/components/retry-switcher/item.jsx @@ -5,10 +5,8 @@ import {connect} from 'react-redux'; import {get} from 'lodash'; import {ERROR} from '../../../constants'; import { - isAssertViewError, - isFailStatus, - isImageDiffError, - isNoRefImageError + hasUnrelatedToScreenshotsErrors, + isFailStatus } from '../../../common-utils'; class RetrySwitcherItem extends Component { @@ -46,7 +44,7 @@ export default connect( const {status, attempt, error} = result; return { - status: hasUnrelatedToScreenshotsErrors(status, error) ? `${status}_${ERROR}` : status, + status: isFailStatus(status) && hasUnrelatedToScreenshotsErrors(error) ? `${status}_${ERROR}` : status, attempt, keyToGroupTestsBy, matchedSelectedGroup @@ -54,9 +52,3 @@ export default connect( } )(RetrySwitcherItem); -function hasUnrelatedToScreenshotsErrors(status, error) { - return isFailStatus(status) && - !isNoRefImageError(error) && - !isImageDiffError(error) && - !isAssertViewError(error); -} diff --git a/lib/static/modules/reducers/tree/helpers.js b/lib/static/modules/reducers/tree/helpers.js index afcc132bd..5cb15a46f 100644 --- a/lib/static/modules/reducers/tree/helpers.js +++ b/lib/static/modules/reducers/tree/helpers.js @@ -1,6 +1,7 @@ -import {get, set} from 'lodash'; +import {get, pick, set} from 'lodash'; import {EXPAND_ALL, EXPAND_ERRORS, EXPAND_RETRIES} from '../../../../constants/expand-modes'; import {getUpdatedProperty} from '../../utils/state'; +import {determineStatus, isUpdatedStatus} from '../../../../common-utils'; export function changeNodeState(nodesStateById, nodeId, state, diff = nodesStateById) { Object.keys(state).forEach((stateName) => { @@ -45,3 +46,26 @@ export function getShownCheckedChildCount(tree, suiteId, diff = tree) { return checkedChildBrowserCount + checkedChildSuitesCount; } + +export function resolveUpdatedStatuses(results, imagesById, suites) { + for (const result of Object.values(results)) { + if (!isUpdatedStatus(result.status)) { + continue; + } + const computedStatus = determineStatus({ + status: result.status, + error: result.error, + imagesInfo: Object.values(pick(imagesById, result.imageIds)) + }); + + result.status = computedStatus; + + const suitesFilteredKeys = Object.entries(suites) + .filter(([, suite]) => result.id.startsWith(suite.id) && isUpdatedStatus(suite.status)) + .map(([key]) => key); + + for (const suiteKey of suitesFilteredKeys) { + suites[suiteKey].status = computedStatus; + } + } +} diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index 4c7236100..0c511a47a 100644 --- a/lib/static/modules/reducers/tree/index.js +++ b/lib/static/modules/reducers/tree/index.js @@ -18,6 +18,7 @@ import {EXPAND_RETRIES} from '../../../../constants/expand-modes'; import {FAIL} from '../../../../constants/test-statuses'; import {isSuccessStatus} from '../../../../common-utils'; import {applyStateUpdate, ensureDiffProperty, getUpdatedProperty} from '../../utils/state'; +import {resolveUpdatedStatuses} from './helpers'; export default ((state, action) => { const diff = {tree: {}}; @@ -37,6 +38,7 @@ export default ((state, action) => { updateAllSuitesStatus(tree, filteredBrowsers); initNodesStates({tree, view: state.view}); + resolveUpdatedStatuses(tree.results.byId, tree.images.byId, tree.suites.byId); return {...state, tree}; } @@ -350,6 +352,8 @@ function addNodesToTree(state, payload) { const {tree, view} = state; [].concat(payload).forEach(({result, images, suites}) => { + resolveUpdatedStatuses([result], tree.images.byId, suites); + addResult(tree, result); setBrowsersLastRetry(tree, result.parentId); addImages(tree, images, view.expand); diff --git a/lib/static/modules/reducers/tree/nodes/suites.js b/lib/static/modules/reducers/tree/nodes/suites.js index 5a51430a3..56c674e72 100644 --- a/lib/static/modules/reducers/tree/nodes/suites.js +++ b/lib/static/modules/reducers/tree/nodes/suites.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import {getSuiteResults} from '../../../selectors/tree'; import {isNodeFailed} from '../../../utils'; import {ensureDiffProperty, getUpdatedProperty} from '../../../utils/state'; -import {determineStatus, isFailStatus} from '../../../../../common-utils'; +import {determineFinalStatus, isFailStatus} from '../../../../../common-utils'; import {changeNodeState, getShownCheckedChildCount, shouldNodeBeOpened} from '../helpers'; import {EXPAND_RETRIES} from '../../../../../constants/expand-modes'; import {FAIL} from '../../../../../constants/test-statuses'; @@ -285,5 +285,5 @@ function getChildSuitesStatus(tree, suite, filteredBrowsers, diff = tree) { childStatuses = childStatuses.concat(suiteBrowserStatuses); } - return determineStatus(childStatuses); + return determineFinalStatus(childStatuses); } diff --git a/lib/test-adapter/hermione.ts b/lib/test-adapter/hermione.ts index b7f93fa5a..f307998d9 100644 --- a/lib/test-adapter/hermione.ts +++ b/lib/test-adapter/hermione.ts @@ -1,11 +1,9 @@ import _ from 'lodash'; -import fs from 'fs-extra'; import path from 'path'; import {getCommandsHistory} from '../history-utils'; -import {ERROR_DETAILS_PATH, TestStatus} from '../constants'; -import {mkTestId, wrapLinkByTag} from '../common-utils'; -import * as utils from '../server-utils'; +import {TestStatus} from '../constants'; +import {wrapLinkByTag} from '../common-utils'; import { AssertViewResult, ErrorDetails, @@ -16,7 +14,7 @@ import { import {ImagesInfoFormatter} from '../image-handler'; import {ReporterTestResult} from './index'; import {getSuitePath} from '../plugin-utils'; -import {testsAttempts} from './cache/hermione'; +import {extractErrorDetails} from './utils'; const getSkipComment = (suite: HermioneTestResult | HermioneSuite): string | null | undefined => { return suite.skipReason || suite.parent && getSkipComment(suite.parent); @@ -27,6 +25,7 @@ const wrapSkipComment = (skipComment: string | null | undefined): string => { }; export interface HermioneTestAdapterOptions { + attempt: number; status: TestStatus; imagesInfoFormatter: ImagesInfoFormatter; } @@ -34,7 +33,6 @@ export interface HermioneTestAdapterOptions { export class HermioneTestAdapter implements ReporterTestResult { private _imagesInfoFormatter: ImagesInfoFormatter; private _testResult: HermioneTestResult; - private _testId: string; private _errorDetails: ErrorDetails | null; private _timestamp: number | undefined; private _attempt: number; @@ -44,10 +42,9 @@ export class HermioneTestAdapter implements ReporterTestResult { return new this(testResult, options); } - constructor(testResult: HermioneTestResult, {status, imagesInfoFormatter}: HermioneTestAdapterOptions) { + constructor(testResult: HermioneTestResult, {attempt, status, imagesInfoFormatter}: HermioneTestAdapterOptions) { this._imagesInfoFormatter = imagesInfoFormatter; this._testResult = testResult; - this._testId = mkTestId(testResult.fullTitle(), testResult.browserId); this._errorDetails = null; this._timestamp = this._testResult.timestamp ?? this._testResult.startTime ?? Date.now(); this._status = status; @@ -56,11 +53,7 @@ export class HermioneTestAdapter implements ReporterTestResult { _.set(this._testResult, 'meta.browserVersion', browserVersion); - if (utils.shouldUpdateAttempt(status)) { - testsAttempts.set(this._testId, _.isUndefined(testsAttempts.get(this._testId)) ? 0 : testsAttempts.get(this._testId) as number + 1); - } - - this._attempt = testsAttempts.get(this._testId) || 0; + this._attempt = attempt; } image?: boolean; @@ -93,11 +86,6 @@ export class HermioneTestAdapter implements ReporterTestResult { return this._attempt; } - set attempt(attemptNum: number) { - testsAttempts.set(this._testId, attemptNum); - this._attempt = attemptNum; - } - get assertViewResults(): AssertViewResult[] { return this._testResult.assertViewResults || []; } @@ -127,10 +115,6 @@ export class HermioneTestAdapter implements ReporterTestResult { return this.testPath.concat(this.browserId, this.attempt.toString()).join(' '); } - get isUpdated(): boolean | undefined { - return this._testResult.updated; - } - get screenshot(): ImageBase64 | undefined { return _.get(this._testResult, 'err.screenshot'); } @@ -148,19 +132,7 @@ export class HermioneTestAdapter implements ReporterTestResult { return this._errorDetails; } - const details = _.get(this._testResult, 'err.details', null); - - if (details) { - this._errorDetails = { - title: details.title || 'error details', - data: details.data, - filePath: `${ERROR_DETAILS_PATH}/${utils.getDetailsFileName( - this._testResult.id, this._testResult.browserId, this.attempt - )}` - }; - } else { - this._errorDetails = null; - } + this._errorDetails = extractErrorDetails(this); return this._errorDetails; } @@ -186,18 +158,4 @@ export class HermioneTestAdapter implements ReporterTestResult { this._timestamp = timestamp; } } - - async saveErrorDetails(reportPath: string): Promise { - if (!this.errorDetails) { - return; - } - - const detailsFilePath = path.resolve(reportPath, this.errorDetails.filePath); - const detailsData = _.isObject(this.errorDetails.data) - ? JSON.stringify(this.errorDetails.data, null, 2) - : this.errorDetails.data; - - await utils.makeDirFor(detailsFilePath); - await fs.writeFile(detailsFilePath, detailsData); - } } diff --git a/lib/test-adapter/index.ts b/lib/test-adapter/index.ts index 9ea46ce21..ffc2b3456 100644 --- a/lib/test-adapter/index.ts +++ b/lib/test-adapter/index.ts @@ -5,7 +5,7 @@ export * from './hermione'; export interface ReporterTestResult { readonly assertViewResults: AssertViewResult[]; - attempt: number; + readonly attempt: number; readonly browserId: string; readonly description: string | undefined; error: undefined | TestError; @@ -17,7 +17,6 @@ export interface ReporterTestResult { image?: boolean; readonly imageDir: string; readonly imagesInfo: ImageInfoFull[] | undefined; - readonly isUpdated?: boolean; readonly meta: Record; readonly multipleTabs: boolean; readonly screenshot: ImageBase64 | ImageData | null | undefined; diff --git a/lib/test-adapter/playwright.ts b/lib/test-adapter/playwright.ts index d65ba8bc7..be38db2f6 100644 --- a/lib/test-adapter/playwright.ts +++ b/lib/test-adapter/playwright.ts @@ -241,10 +241,6 @@ export class PlaywrightTestAdapter implements ReporterTestResult { return this._imagesInfoFormatter.getImagesInfo(this); } - get isUpdated(): boolean { - return false; - } - get meta(): Record { return Object.fromEntries(this._testCase.annotations.map(a => [a.type, a.description ?? ''])); } diff --git a/lib/test-adapter/reporter.ts b/lib/test-adapter/reporter.ts new file mode 100644 index 000000000..15f402d4f --- /dev/null +++ b/lib/test-adapter/reporter.ts @@ -0,0 +1,121 @@ +import {TestStatus} from '../constants'; +import {AssertViewResult, TestError, ErrorDetails, ImageInfoFull, ImageBase64, ImageData} from '../types'; +import {ReporterTestResult} from './index'; +import {ImagesInfoFormatter} from '../image-handler'; +import _ from 'lodash'; +import {extractErrorDetails} from './utils'; +import {getShortMD5} from '../common-utils'; + +interface ReporterTestAdapterOptions { + imagesInfoFormatter: ImagesInfoFormatter; +} + +// This class is primarily useful when cloning ReporterTestResult. +// It allows to override some properties while keeping computable +// properties valid, e.g. id + +export class ReporterTestAdapter implements ReporterTestResult { + private _testResult: ReporterTestResult; + private _imagesInfoFormatter: ImagesInfoFormatter; + private _errorDetails: ErrorDetails | null; + + constructor(testResult: ReporterTestResult, {imagesInfoFormatter}: ReporterTestAdapterOptions) { + this._testResult = testResult; + this._imagesInfoFormatter = imagesInfoFormatter; + this._errorDetails = null; + } + + get assertViewResults(): AssertViewResult[] { + return this._testResult.assertViewResults; + } + + get attempt(): number { + return this._testResult.attempt; + } + + get browserId(): string { + return this._testResult.browserId; + } + + get description(): string | undefined { + return this._testResult.description; + } + + get error(): TestError | undefined { + return this._testResult.error; + } + + get errorDetails(): ErrorDetails | null { + if (!_.isNil(this._errorDetails)) { + return this._errorDetails; + } + + this._errorDetails = extractErrorDetails(this); + + return this._errorDetails; + } + + get file(): string { + return this._testResult.file; + } + + get fullName(): string { + return this._testResult.fullName; + } + + get history(): string[] { + return this._testResult.history; + } + + get id(): string { + return this.testPath.concat(this.browserId, this.attempt.toString()).join(' '); + } + + get imageDir(): string { + return getShortMD5(this.fullName); + } + + get imagesInfo(): ImageInfoFull[] | undefined { + return this._imagesInfoFormatter.getImagesInfo(this); + } + + get meta(): Record { + return this._testResult.meta; + } + + get multipleTabs(): boolean { + return this._testResult.multipleTabs; + } + + get screenshot(): ImageBase64 | ImageData | null | undefined { + return this.error?.screenshot; + } + + get sessionId(): string { + return this._testResult.sessionId; + } + + get skipReason(): string | undefined { + return this._testResult.skipReason; + } + + get state(): {name: string;} { + return {name: this.testPath.at(-1) as string}; + } + + get status(): TestStatus { + return this._testResult.status; + } + + get testPath(): string[] { + return this._testResult.testPath; + } + + get timestamp(): number | undefined { + return this._testResult.timestamp; + } + + get url(): string | undefined { + return this._testResult.url; + } +} diff --git a/lib/test-adapter/transformers/db.ts b/lib/test-adapter/transformers/db.ts new file mode 100644 index 000000000..8ae83ca1c --- /dev/null +++ b/lib/test-adapter/transformers/db.ts @@ -0,0 +1,44 @@ +import {ReporterTestResult} from '../index'; +import {DbTestResult} from '../../sqlite-client'; +import {getError, getRelativeUrl, getUrlWithBase} from '../../common-utils'; +import _ from 'lodash'; + +interface Options { + baseHost?: string; +} + +export class DbTestResultTransformer { + private _options: Options; + + constructor(options: Options) { + this._options = options; + } + + transform(testResult: ReporterTestResult): DbTestResult { + const suiteUrl = getUrlWithBase(testResult.url, this._options.baseHost); + + const metaInfoFull = _.merge(_.cloneDeep(testResult.meta), { + url: getRelativeUrl(suiteUrl) ?? '', + file: testResult.file, + sessionId: testResult.sessionId + }); + const metaInfo = _.omitBy(metaInfoFull, _.isEmpty); + + return { + suitePath: testResult.testPath, + suiteName: _.last(testResult.testPath) as string, + name: testResult.browserId, + suiteUrl, + metaInfo, + history: testResult.history, + description: testResult.description, + error: getError(testResult.error), + skipReason: testResult.skipReason, + imagesInfo: testResult.imagesInfo ?? [], + screenshot: Boolean(testResult.screenshot), + multipleTabs: testResult.multipleTabs, + status: testResult.status, + timestamp: testResult.timestamp ?? Date.now() + }; + } +} diff --git a/lib/test-adapter/utils/index.ts b/lib/test-adapter/utils/index.ts new file mode 100644 index 000000000..48e5c7d29 --- /dev/null +++ b/lib/test-adapter/utils/index.ts @@ -0,0 +1,68 @@ +import _ from 'lodash'; +import {ReporterTestResult} from '../index'; +import {TupleToUnion} from 'type-fest'; +import {ErrorDetails} from '../../types'; +import {ERROR_DETAILS_PATH} from '../../constants'; +import * as utils from '../../server-utils'; +import {ReporterTestAdapter} from '../reporter'; +import {ImagesInfoFormatter} from '../../image-handler'; + +export const copyAndUpdate = ( + original: ReporterTestResult, + updates: Partial, + {imagesInfoFormatter}: {imagesInfoFormatter: ImagesInfoFormatter} +): ReporterTestResult => { + const keys = [ + 'assertViewResults', + 'attempt', + 'browserId', + 'description', + 'error', + 'errorDetails', + 'file', + 'fullName', + 'history', + 'id', + 'image', + 'imageDir', + 'imagesInfo', + 'meta', + 'multipleTabs', + 'screenshot', + 'sessionId', + 'skipReason', + 'state', + 'status', + 'testPath', + 'timestamp', + 'url' + ] as const; + + // Type-level check that we didn't forget to include any keys + type A = TupleToUnion; + type B = keyof ReporterTestResult; + + const keysTypeChecked: B extends A ? + A extends B ? typeof keys : never + : never = keys; + + const updatedTestResult = _.assign({}, _.pick(original, keysTypeChecked) as ReporterTestResult, updates); + + return new ReporterTestAdapter(updatedTestResult, {imagesInfoFormatter}); +}; + +export const extractErrorDetails = (testResult: ReporterTestResult): ErrorDetails | null => { + const details = testResult.error?.details ?? null; + + if (details) { + return { + title: details.title || 'error details', + data: details.data, + filePath: `${ERROR_DETAILS_PATH}/${utils.getDetailsFileName( + testResult.imageDir, testResult.browserId, testResult.attempt + )}` + }; + } + + return null; +}; diff --git a/lib/test-attempt-manager.ts b/lib/test-attempt-manager.ts new file mode 100644 index 000000000..2b65263e5 --- /dev/null +++ b/lib/test-attempt-manager.ts @@ -0,0 +1,58 @@ +import {ReporterTestResult} from './test-adapter'; +import {IDLE, RUNNING, TestStatus} from './constants'; + +type TestSpec = Pick + +interface AttemptData { + statuses: TestStatus[]; +} + +export class TestAttemptManager { + private _attempts: Map; + + constructor() { + this._attempts = new Map(); + } + + removeAttempt(testResult: TestSpec): number { + const [hash, data] = this._getData(testResult); + + if (data.statuses.length > 0) { + data.statuses.pop(); + } + + this._attempts.set(hash, data); + + return Math.max(data.statuses.length - 1, 0); + } + + getCurrentAttempt(testResult: TestSpec): number { + const [, data] = this._getData(testResult); + + return Math.max(data.statuses.length - 1, 0); + } + + registerAttempt(testResult: TestSpec, status: TestStatus, index: number | null = null): number { + const [hash, data] = this._getData(testResult); + + const isManualOverride = index !== null; + const isLastStatusTemporary = [IDLE, RUNNING].includes(data.statuses.at(-1) as TestStatus); + const shouldReplace = Number(isManualOverride || isLastStatusTemporary); + + data.statuses.splice(index ?? data.statuses.length - shouldReplace, shouldReplace, status); + + this._attempts.set(hash, data); + + return Math.max(data.statuses.length - 1, 0); + } + + private _getHash(testResult: TestSpec): string { + return `${testResult.fullName}.${testResult.browserId}`; + } + + private _getData(testResult: TestSpec): [string, AttemptData] { + const hash = this._getHash(testResult); + + return [hash, this._attempts.get(hash) ?? {statuses: []}]; + } +} diff --git a/lib/tests-tree-builder/base.ts b/lib/tests-tree-builder/base.ts index 69ea40d17..ee71e270f 100644 --- a/lib/tests-tree-builder/base.ts +++ b/lib/tests-tree-builder/base.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {determineStatus} from '../common-utils'; +import {determineFinalStatus} from '../common-utils'; import {BrowserVersions, PWT_TITLE_DELIMITER, TestStatus, ToolName} from '../constants'; import {ReporterTestResult} from '../test-adapter'; import {ImageInfoFull, ParsedSuitesRow} from '../types'; @@ -253,7 +253,7 @@ export class BaseTestsTreeBuilder { const childrenSuiteStatuses = _.compact(([] as (string | undefined)[]).concat(suite.suiteIds)) .map((childSuiteId: string) => this._tree.suites.byId[childSuiteId].status); - const status = determineStatus(_.compact([...resultStatuses, ...childrenSuiteStatuses])); + const status = determineFinalStatus(_.compact([...resultStatuses, ...childrenSuiteStatuses])); // if newly determined status is the same as current status, do nothing if (suite.status === status) { diff --git a/lib/tests-tree-builder/gui.ts b/lib/tests-tree-builder/gui.ts index ffd88676f..83c74ec59 100644 --- a/lib/tests-tree-builder/gui.ts +++ b/lib/tests-tree-builder/gui.ts @@ -2,7 +2,6 @@ import _ from 'lodash'; import {BaseTestsTreeBuilder, Tree, TreeImage, TreeResult, TreeSuite} from './base'; import {TestStatus, UPDATED} from '../constants'; import {isUpdatedStatus} from '../common-utils'; -import {ReporterTestResult} from '../test-adapter'; import {ImageInfoFail, ImageInfoWithState} from '../types'; interface SuiteBranch { @@ -42,36 +41,6 @@ interface TestUndoRefUpdateData { } export class GuiTestsTreeBuilder extends BaseTestsTreeBuilder { - getLastResult(formattedResult: ReporterTestResult): TreeResult | undefined { - const browserId = this._buildBrowserId(formattedResult); - const browser = this._tree.browsers.byId[browserId]; - const testResultId = _.last(browser.resultIds); - - if (!testResultId) { - return; - } - - return this._tree.results.byId[testResultId]; - } - - /* Returns "real" last test result - one that wasn't generated by clicking accept button */ - getLastActualResult(formattedResult: ReporterTestResult): TreeResult | undefined { - let attempt = formattedResult.attempt - 1; - while (attempt >= 0) { - const resultId = this._buildResultId(formattedResult, attempt); - - if (![TestStatus.RUNNING, TestStatus.UPDATED].includes(this._tree.results.byId[resultId].status)) { - break; - } - - attempt -= 1; - } - - const resultId = this._buildResultId(formattedResult, attempt); - - return this._tree.results.byId[resultId]; - } - getImagesInfo(testId: string): TreeImage[] { return this._tree.results.byId[testId].imageIds.map((imageId) => { return this._tree.images.byId[imageId]; @@ -265,18 +234,4 @@ export class GuiTestsTreeBuilder extends BaseTestsTreeBuilder { this._reuseSuiteStatus(testsTree, suite.parentId); } - - protected _buildBrowserId(formattedResult: Pick): string { - const {testPath, browserId: browserName} = formattedResult; - const suiteId = this._buildId(testPath); - const browserId = this._buildId(suiteId, browserName); - - return browserId; - } - - protected _buildResultId(formattedResult: ReporterTestResult, attempt: number): string { - const browserId = this._buildBrowserId(formattedResult); - - return `${browserId} ${attempt}`; - } } diff --git a/lib/tests-tree-builder/static.ts b/lib/tests-tree-builder/static.ts index b7a44aa39..2c1ffdb5e 100644 --- a/lib/tests-tree-builder/static.ts +++ b/lib/tests-tree-builder/static.ts @@ -201,6 +201,8 @@ function mkTestResult(row: RawSuitesRow, data: {attempt: number}): ParsedSuitesR name: row[DB_COLUMN_INDEXES.name] as string, screenshot: Boolean(row[DB_COLUMN_INDEXES.screenshot]), status: row[DB_COLUMN_INDEXES.status] as TestStatus, + suiteName: row[DB_COLUMN_INDEXES.suiteName] as string, + suitePath: JSON.parse(row[DB_COLUMN_INDEXES.suitePath] as string), suiteUrl: row[DB_COLUMN_INDEXES.suiteUrl] as string, skipReason: row[DB_COLUMN_INDEXES.skipReason] as string, error: JSON.parse(row[DB_COLUMN_INDEXES.error] as string), diff --git a/lib/types.ts b/lib/types.ts index 88107fd06..8c1d5db43 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -12,7 +12,6 @@ export {Suite as HermioneSuite} from 'hermione'; export interface HermioneTestResult extends HermioneTestResultOriginal { timestamp?: number; - updated?: boolean; } export interface ImagesSaver { @@ -98,7 +97,7 @@ export type ImageInfo = | Omit | Omit; -export type AssertViewResult = AssertViewSuccess | ImageDiffError | NoRefImageError; +export type AssertViewResult = (AssertViewSuccess | ImageDiffError | NoRefImageError) & {isUpdated?: boolean}; export interface TestError { name: string; @@ -133,6 +132,8 @@ export interface ParsedSuitesRow { screenshot: boolean; skipReason?: string; status: TestStatus; + suiteName: string; + suitePath: string[]; suiteUrl: string; timestamp: number; } diff --git a/lib/workers/create-workers.ts b/lib/workers/create-workers.ts index 3bf7ed593..48247c747 100644 --- a/lib/workers/create-workers.ts +++ b/lib/workers/create-workers.ts @@ -13,3 +13,5 @@ export const createWorkers = ( return runner.registerWorkers(workerFilepath, ['saveDiffTo']); }; + +export type CreateWorkersRunner = Parameters[0]; diff --git a/playwright.ts b/playwright.ts index bc222336d..03464bd9e 100644 --- a/playwright.ts +++ b/playwright.ts @@ -1,7 +1,10 @@ import {promisify} from 'util'; + +import {EventEmitter} from 'events'; import _ from 'lodash'; -import type {Reporter, TestCase, TestResult as PwtTestResult} from '@playwright/test/reporter'; +import PQueue from 'p-queue'; import workerFarm, {Workers} from 'worker-farm'; +import type {Reporter, TestCase, TestResult as PwtTestResult} from '@playwright/test/reporter'; import {StaticReportBuilder} from './lib/report-builder/static'; import {HtmlReporter} from './lib/plugin-api'; @@ -9,24 +12,24 @@ import {ReporterConfig} from './lib/types'; import {parseConfig} from './lib/config'; import {PluginEvents, TestStatus, ToolName} from './lib/constants'; import {RegisterWorkers} from './lib/workers/create-workers'; -import {EventEmitter} from 'events'; import {PlaywrightTestAdapter, getStatus} from './lib/test-adapter/playwright'; -import PQueue from 'p-queue'; +import {SqliteClient} from './lib/sqlite-client'; export {ReporterConfig} from './lib/types'; class MyReporter implements Reporter { protected _promiseQueue: PQueue = new PQueue(); - protected _staticReportBuilder: StaticReportBuilder; + protected _staticReportBuilder: StaticReportBuilder | null; protected _htmlReporter: HtmlReporter; protected _config: ReporterConfig; protected _workerFarm: Workers; protected _workers: RegisterWorkers<['saveDiffTo']>; + protected _initPromise: Promise; constructor(opts: Partial) { this._config = parseConfig(_.omit(opts, ['configDir'])); this._htmlReporter = HtmlReporter.create(this._config, {toolName: ToolName.Playwright}); - this._staticReportBuilder = StaticReportBuilder.create(this._htmlReporter, this._config); + this._staticReportBuilder = null; this._workerFarm = workerFarm(require.resolve('./lib/workers/worker'), ['saveDiffTo']); const workers: RegisterWorkers<['saveDiffTo']> = new EventEmitter() as RegisterWorkers<['saveDiffTo']>; @@ -34,33 +37,45 @@ class MyReporter implements Reporter { promisify(this._workerFarm.saveDiffTo)(imageDiffError, diffPath); this._workers = workers; - this._promiseQueue.add(() => this._staticReportBuilder.init() - .then(() => this._staticReportBuilder.saveStaticFiles()) - ); + this._initPromise = (async (htmlReporter: HtmlReporter, config: ReporterConfig): Promise => { + const dbClient = await SqliteClient.create({htmlReporter, reportPath: config.path}); + + this._staticReportBuilder = StaticReportBuilder.create(htmlReporter, config, {dbClient}); + this._staticReportBuilder.registerWorkers(workers); + + await this._staticReportBuilder.saveStaticFiles(); + })(this._htmlReporter, this._config); + + this._promiseQueue.add(async () => this._initPromise); } onTestEnd(test: TestCase, result: PwtTestResult): void { - const status = getStatus(result); - const formattedResult = new PlaywrightTestAdapter(test, result, {imagesInfoFormatter: this._staticReportBuilder.imageHandler}); - - if (status === TestStatus.FAIL) { - if (formattedResult.status === TestStatus.FAIL) { - this._staticReportBuilder.addFail(formattedResult); - } else { - this._staticReportBuilder.addError(formattedResult); + this._promiseQueue.add(async () => { + await this._initPromise; + + const staticReportBuilder = this._staticReportBuilder as StaticReportBuilder; + + const status = getStatus(result); + const formattedResult = new PlaywrightTestAdapter(test, result, {imagesInfoFormatter: staticReportBuilder.imageHandler}); + + if (status === TestStatus.FAIL) { + if (formattedResult.status === TestStatus.FAIL) { + await staticReportBuilder.addFail(formattedResult); + } else { + await staticReportBuilder.addError(formattedResult); + } + } else if (status === TestStatus.SUCCESS) { + await staticReportBuilder.addSuccess(formattedResult); + } else if (status === TestStatus.SKIPPED) { + await staticReportBuilder.addSkipped(formattedResult); } - } else if (status === TestStatus.SUCCESS) { - this._staticReportBuilder.addSuccess(formattedResult); - } else if (status === TestStatus.SKIPPED) { - this._staticReportBuilder.addSkipped(formattedResult); - } - this._promiseQueue.add(() => this._staticReportBuilder.imageHandler.saveTestImages(formattedResult, this._workers)); + }); } async onEnd(): Promise { await this._promiseQueue.onIdle(); - await this._staticReportBuilder.finalize(); + await this._staticReportBuilder?.finalize(); await this._htmlReporter.emitAsync(PluginEvents.REPORT_SAVED); } diff --git a/test/func/docker/browser-utils/download-chromium.js b/test/func/docker/browser-utils/download-chromium.js index e366148c2..34cb39a36 100644 --- a/test/func/docker/browser-utils/download-chromium.js +++ b/test/func/docker/browser-utils/download-chromium.js @@ -1,6 +1,44 @@ +const fs = require('fs'); const os = require('os'); const path = require('path'); -const downloadChromium = require('hermione-headless-chrome/lib/download-chromium-by-version'); +const {pipeline} = require('stream'); +const {promisify} = require('util'); +const AdmZip = require('adm-zip'); +const got = require('got'); -downloadChromium(process.env.CHROME_VERSION, path.join(os.homedir(), 'chrome-cache'), 10) - .then(chromePath => console.log(chromePath)); +const streamPipeline = promisify(pipeline); + +const download = async (url) => { + // Create a temporary directory to store the downloaded file + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-download-')); + + // Path for the downloaded zip file + const zipFilePath = path.join(tempDir, 'chrome-linux64.zip'); + + await streamPipeline( + got.stream(url), + fs.createWriteStream(zipFilePath) + ); + + return zipFilePath; +}; + +const extract = async (zipFilePath) => { + const zip = new AdmZip(zipFilePath); + + const outPath = path.dirname(zipFilePath); + + zip.extractAllTo(outPath); + + return path.join(outPath, 'chrome-linux64'); +}; + +(async () => { + // Available good versions of Chrome for testing: https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json + const CHROME_URL = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/116.0.5845.49/linux64/chrome-linux64.zip'; + + const chromeZipPath = await download(CHROME_URL); + const chromePath = await extract(chromeZipPath); + + console.log(chromePath); +})(); diff --git a/test/func/docker/browser-utils/install-chromium.sh b/test/func/docker/browser-utils/install-chromium.sh index 47eb29ad9..03bbabaed 100755 --- a/test/func/docker/browser-utils/install-chromium.sh +++ b/test/func/docker/browser-utils/install-chromium.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash -# The download script outputs progress messages, but we need only final path, hence we filter output here -CHROME_PATH=$(node ./download-chromium.js |& grep '^/home') -CHROME_PATH=$(dirname $CHROME_PATH) +CHROME_PATH=$(node ./download-chromium.js) mkdir ~/browsers mv $CHROME_PATH ~/browsers/chrome-linux +chmod +x ~/browsers/chrome-linux/chrome diff --git a/test/func/docker/browser-utils/package-lock.json b/test/func/docker/browser-utils/package-lock.json index 9af07e2b6..94cc6e375 100644 --- a/test/func/docker/browser-utils/package-lock.json +++ b/test/func/docker/browser-utils/package-lock.json @@ -8,2068 +8,496 @@ "name": "browser-utils", "version": "0.0.0", "dependencies": { - "hermione-headless-chrome": "^1.1.0" + "adm-zip": "^0.5.10", + "got": "^11.8.6" } }, "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dependencies": { - "defer-to-connect": "^1.0.1" + "defer-to-connect": "^2.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" } }, - "node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "engines": { - "node": ">=6" - } + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" + "@types/node": "*" } }, - "node_modules/ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "node_modules/@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" + "undici-types": "~5.26.4" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@types/node": "*" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", "engines": { - "node": "*" + "node": ">=6.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "engines": { - "node": ">= 0.8" + "node": ">=10.6.0" } }, "node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", + "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" + "mimic-response": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dependencies": { - "restore-cursor": "^2.0.0" + "node": ">=10" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "engines": { - "node": ">=0.8" + "node": ">=10" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dependencies": { - "mimic-response": "^1.0.0" + "once": "^1.4.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, - "node_modules/core-util-is": { + "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cpr": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", - "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dependencies": { - "graceful-fs": "^4.1.5", - "minimist": "^1.2.0", - "mkdirp": "~0.5.1", - "rimraf": "^2.5.4" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" }, - "bin": { - "cpr": "bin/cpr" + "engines": { + "node": ">=10.19.0" } }, - "node_modules/data-uri-to-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { - "ms": "2.1.2" - }, + "json-buffer": "3.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=8" } }, - "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "engines": { "node": ">=4" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dependencies": { - "clone": "^1.0.2" + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" - }, - "node_modules/degenerator": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", - "integrity": "sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { - "ast-types": "0.x.x", - "escodegen": "1.x.x", - "esprima": "3.x.x" + "wrappy": "1" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/download-chromium": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/download-chromium/-/download-chromium-2.2.1.tgz", - "integrity": "sha512-y3VF5hgjmQ19hYQWb348rRRxIE+ziS9oGRm2tvMz6aF6E0T7AXcrU9MVkcLyQmt0fxwPMMAi08LxnXHfqeOP8w==", + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dependencies": { - "cpr": "^3.0.1", - "debug": "^4.1.1", - "extract-zip": "^1.6.7", - "got": "^9.6.0", - "mkdirp": "^0.5.1", - "promisepipe": "^2.0.0", - "proxy-agent": "^3.1.0", - "proxy-from-env": "^1.0.0" - }, - "bin": { - "download-chromium": "bin.js" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dependencies": { - "es6-promise": "^4.0.3" + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, - "node_modules/escodegen/node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" } }, - "node_modules/esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } + "@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" + "@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "requires": { + "undici-types": "~5.26.4" } }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "requires": { + "@types/node": "*" } }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==" }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "requires": { + "mimic-response": "^1.0.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", - "dependencies": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ftp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/ftp/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } } }, - "node_modules/ftp/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, - "node_modules/gemini-configparser": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/gemini-configparser/-/gemini-configparser-1.3.2.tgz", - "integrity": "sha512-aCbDQIPTpL2RgpsgLLx2eKB+tM/vstAnoj38yzDaZIhwALOK/xaET+4+2ofyRtF/Vi4YzPkTcAmlL9P3LOeOdQ==", - "dependencies": { - "lodash": "^4.17.4" + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" } }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-uri": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", - "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", - "dependencies": { - "data-uri-to-buffer": "1", - "debug": "2", - "extend": "~3.0.2", - "file-uri-to-path": "1", - "ftp": "~0.3.10", - "readable-stream": "2" } }, - "node_modules/get-uri/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" } }, - "node_modules/get-uri/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" } }, - "node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/hermione-headless-chrome": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hermione-headless-chrome/-/hermione-headless-chrome-1.1.0.tgz", - "integrity": "sha512-0HuIoqr7+5h3hsbNdzNnPZOODdWbAwIS+tYSOBelKhYueXUZRv/2slVI8KLnF64Ji3vWC+ZYzuyDxKVw9NfcEg==", - "dependencies": { - "compare-versions": "^3.5.1", - "download-chromium": "^2.2.0", - "fs-extra": "^8.1.0", - "gemini-configparser": "^1.0.0", - "got": "^9.6.0", - "lodash": "^4.17.15", - "nested-error-stacks": "^2.1.0", - "ora": "^3.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dependencies": { - "agent-base": "4", - "debug": "3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dependencies": { - "chalk": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/nested-error-stacks": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", - "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==" - }, - "node_modules/netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", - "dependencies": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", - "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", - "dependencies": { - "agent-base": "^4.2.0", - "debug": "^4.1.1", - "get-uri": "^2.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "pac-resolver": "^3.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "^4.0.1" - } - }, - "node_modules/pac-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", - "dependencies": { - "co": "^4.6.0", - "degenerator": "^1.0.4", - "ip": "^1.1.5", - "netmask": "^1.0.6", - "thunkify": "^2.1.2" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promisepipe": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/promisepipe/-/promisepipe-2.1.3.tgz", - "integrity": "sha512-pPJRbWEGIuPrjWAdN2lZLRuv+5wMNyig0dniGbMfadS7EK+/kxKqkjAA02iZI+S5Om0tCu6MC1JrjWYs8BlheQ==" - }, - "node_modules/proxy-agent": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", - "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", - "dependencies": { - "agent-base": "^4.2.0", - "debug": "4", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^3.0.1", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "dependencies": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - }, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "dependencies": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/socks/node_modules/ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==" - }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", - "engines": { - "node": "*" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - }, - "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "requires": { - "tslib": "^2.0.1" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" - } - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==" - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" - }, - "clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "cpr": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", - "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", - "requires": { - "graceful-fs": "^4.1.5", - "minimist": "^1.2.0", - "mkdirp": "~0.5.1", - "rimraf": "^2.5.4" - } - }, - "data-uri-to-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "requires": { - "clone": "^1.0.2" - } - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" - }, - "degenerator": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", - "integrity": "sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==", - "requires": { - "ast-types": "0.x.x", - "escodegen": "1.x.x", - "esprima": "3.x.x" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "download-chromium": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/download-chromium/-/download-chromium-2.2.1.tgz", - "integrity": "sha512-y3VF5hgjmQ19hYQWb348rRRxIE+ziS9oGRm2tvMz6aF6E0T7AXcrU9MVkcLyQmt0fxwPMMAi08LxnXHfqeOP8w==", - "requires": { - "cpr": "^3.0.1", - "debug": "^4.1.1", - "extract-zip": "^1.6.7", - "got": "^9.6.0", - "mkdirp": "^0.5.1", - "promisepipe": "^2.0.0", - "proxy-agent": "^3.1.0", - "proxy-from-env": "^1.0.0" - } - }, - "duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - } - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==" - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", - "requires": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - } - } - }, - "gemini-configparser": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/gemini-configparser/-/gemini-configparser-1.3.2.tgz", - "integrity": "sha512-aCbDQIPTpL2RgpsgLLx2eKB+tM/vstAnoj38yzDaZIhwALOK/xaET+4+2ofyRtF/Vi4YzPkTcAmlL9P3LOeOdQ==", - "requires": { - "lodash": "^4.17.4" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "get-uri": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", - "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", - "requires": { - "data-uri-to-buffer": "1", - "debug": "2", - "extend": "~3.0.2", - "file-uri-to-path": "1", - "ftp": "~0.3.10", - "readable-stream": "2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "hermione-headless-chrome": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hermione-headless-chrome/-/hermione-headless-chrome-1.1.0.tgz", - "integrity": "sha512-0HuIoqr7+5h3hsbNdzNnPZOODdWbAwIS+tYSOBelKhYueXUZRv/2slVI8KLnF64Ji3vWC+ZYzuyDxKVw9NfcEg==", - "requires": { - "compare-versions": "^3.5.1", - "download-chromium": "^2.2.0", - "fs-extra": "^8.1.0", - "gemini-configparser": "^1.0.0", - "got": "^9.6.0", - "lodash": "^4.17.15", - "nested-error-stacks": "^2.1.0", - "ora": "^3.4.0" - } - }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "requires": { - "graceful-fs": "^4.1.6" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "requires": { - "json-buffer": "3.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" + "json-buffer": "3.0.1" } }, "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nested-error-stacks": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", - "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==" - }, - "netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==" - }, "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, "once": { "version": "1.4.0", @@ -2079,121 +507,10 @@ "wrappy": "1" } }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "ora": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", - "requires": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", - "wcwidth": "^1.0.1" - } - }, "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, - "pac-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", - "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", - "requires": { - "agent-base": "^4.2.0", - "debug": "^4.1.1", - "get-uri": "^2.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "pac-resolver": "^3.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "^4.0.1" - } - }, - "pac-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", - "requires": { - "co": "^4.6.0", - "degenerator": "^1.0.4", - "ip": "^1.1.5", - "netmask": "^1.0.6", - "thunkify": "^2.1.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "promisepipe": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/promisepipe/-/promisepipe-2.1.3.tgz", - "integrity": "sha512-pPJRbWEGIuPrjWAdN2lZLRuv+5wMNyig0dniGbMfadS7EK+/kxKqkjAA02iZI+S5Om0tCu6MC1JrjWYs8BlheQ==" - }, - "proxy-agent": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", - "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", - "requires": { - "agent-base": "^4.2.0", - "debug": "4", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^3.0.1", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^4.0.1" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" }, "pump": { "version": "3.0.0", @@ -2204,243 +521,33 @@ "once": "^1.3.1" } }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" }, "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "socks": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "requires": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - }, - "dependencies": { - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==" - } - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==" - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "requires": { - "prepend-http": "^2.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "requires": { - "defaults": "^1.0.3" + "lowercase-keys": "^2.0.0" } }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } } } } diff --git a/test/func/docker/browser-utils/package.json b/test/func/docker/browser-utils/package.json index 5f874c0a3..ff7565d4f 100644 --- a/test/func/docker/browser-utils/package.json +++ b/test/func/docker/browser-utils/package.json @@ -6,6 +6,7 @@ "install-chromium": "./install-chromium.sh" }, "dependencies": { - "hermione-headless-chrome": "1.1.0" + "adm-zip": "^0.5.10", + "got": "^11.8.6" } } diff --git a/test/unit/hermione.js b/test/unit/hermione.js index 9c9883fa5..6dd21fdbf 100644 --- a/test/unit/hermione.js +++ b/test/unit/hermione.js @@ -5,7 +5,7 @@ const Database = require('better-sqlite3'); const fsOriginal = require('fs-extra'); const proxyquire = require('proxyquire').noPreserveCache(); const {logger} = require('lib/common-utils'); -const {stubTool} = require('./utils'); +const {stubTool, NoRefImageError, ImageDiffError} = require('./utils'); const mkSqliteDb = () => { const instance = sinon.createStubInstance(Database); @@ -20,13 +20,15 @@ describe('lib/hermione', () => { let hermione; let cacheExpectedPaths = new Map(), cacheAllImages = new Map(), cacheDiffImages = new Map(); + let program; + const fs = _.clone(fsOriginal); const originalUtils = proxyquire('lib/server-utils', { 'fs-extra': fs }); const utils = _.clone(originalUtils); - const {SqliteAdapter} = proxyquire('lib/sqlite-adapter', { + const {SqliteClient} = proxyquire('lib/sqlite-client', { 'fs-extra': fs, 'better-sqlite3': sinon.stub().returns(mkSqliteDb()) }); @@ -45,23 +47,21 @@ describe('lib/hermione', () => { const {StaticReportBuilder} = proxyquire('lib/report-builder/static', { 'fs-extra': fs, '../server-utils': utils, - '../sqlite-adapter': {SqliteAdapter}, '../test-adapter': {TestAdapter}, '../image-handler': {ImageHandler} }); - const {PluginAdapter} = proxyquire('lib/plugin-adapter', { - './server-utils': utils, - './report-builder/static': {StaticReportBuilder}, - './plugin-api': proxyquire('lib/plugin-api', { - './local-images-saver': proxyquire('lib/local-images-saver', { - './server-utils': utils - }) + const HtmlReporter = proxyquire('lib/plugin-api', { + './local-images-saver': proxyquire('lib/local-images-saver', { + './server-utils': utils }) - }); + }).HtmlReporter; - const HermioneReporter = proxyquire('../../hermione', { - './lib/plugin-adapter': {PluginAdapter} + const runHtmlReporter = proxyquire('../../hermione', { + './lib/sqlite-client': {SqliteClient}, + './lib/server-utils': utils, + './lib/report-builder/static': {StaticReportBuilder}, + './lib/plugin-api': {HtmlReporter} }); const events = { @@ -75,24 +75,6 @@ describe('lib/hermione', () => { AFTER_TESTS_READ: 'afterTestsRead' }; - class ImageDiffError extends Error { - name = 'ImageDiffError'; - constructor() { - super(); - this.stateName = ''; - this.currImg = { - path: '' - }; - this.refImg = { - path: '' - }; - } - } - - class NoRefImageError extends Error { - name = 'NoRefImageError'; - } - function mkHermione_() { return stubTool({ forBrowser: sinon.stub().returns({ @@ -103,6 +85,25 @@ describe('lib/hermione', () => { }, events, {ImageDiffError, NoRefImageError}); } + function mkCommander() { + const commander = {}; + const props = [ + 'command', + 'allowUnknownOption', + 'description', + 'option', + 'action', + 'on', + 'prependListener' + ]; + + for (const prop of props) { + commander[prop] = sinon.stub().returns(commander); + } + + return commander; + } + function initReporter_(opts) { opts = _.defaults(opts, { enabled: true, @@ -110,7 +111,9 @@ describe('lib/hermione', () => { baseHost: '' }); - HermioneReporter(hermione, opts); + runHtmlReporter(hermione, opts); + + hermione.emit(hermione.events.CLI, program); return hermione.emitAsync(hermione.events.INIT); } @@ -139,9 +142,9 @@ describe('lib/hermione', () => { beforeEach(async () => { hermione = mkHermione_(); - sandbox.spy(PluginAdapter.prototype, 'addApi'); - sandbox.spy(PluginAdapter.prototype, 'addCliCommands'); - sandbox.spy(PluginAdapter.prototype, 'init'); + program = mkCommander(); + + sandbox.spy(HtmlReporter, 'create'); sandbox.stub(fs, 'ensureDir').resolves(); sandbox.stub(fs, 'writeFile').resolves(); @@ -157,16 +160,15 @@ describe('lib/hermione', () => { sandbox.stub(logger, 'log'); sandbox.stub(logger, 'warn'); - sandbox.stub(StaticReportBuilder.prototype, 'addSkipped'); - sandbox.stub(StaticReportBuilder.prototype, 'addSuccess'); - sandbox.stub(StaticReportBuilder.prototype, 'addError'); - sandbox.stub(StaticReportBuilder.prototype, 'addFail'); - sandbox.stub(StaticReportBuilder.prototype, 'addRetry'); + sandbox.stub(StaticReportBuilder.prototype, 'addSkipped').callsFake(_.identity); + sandbox.stub(StaticReportBuilder.prototype, 'addSuccess').callsFake(_.identity); + sandbox.stub(StaticReportBuilder.prototype, 'addError').callsFake(_.identity); + sandbox.stub(StaticReportBuilder.prototype, 'addFail').callsFake(_.identity); + sandbox.stub(StaticReportBuilder.prototype, 'addRetry').callsFake(_.identity); sandbox.stub(StaticReportBuilder.prototype, 'saveStaticFiles'); sandbox.stub(StaticReportBuilder.prototype, 'finalize'); - sandbox.stub(SqliteAdapter.prototype, 'init').resolves({}); - sandbox.stub(SqliteAdapter.prototype, 'query'); + sandbox.stub(SqliteClient.prototype, 'query'); sandbox.stub(fs, 'readFile').resolves(Buffer.from('')); }); @@ -179,24 +181,22 @@ describe('lib/hermione', () => { sandbox.restore(); }); - it('should do nothing if plugin is disabled', () => { - return initReporter_({enabled: false}).then(() => { - assert.notCalled(PluginAdapter.prototype.addCliCommands); - }); - }); + it('should do nothing if plugin is disabled', async () => { + await initReporter_({enabled: false}); - it('should add api', () => { - return initReporter_() - .then(() => assert.calledOnce(PluginAdapter.prototype.addCliCommands)); + assert.notCalled(HtmlReporter.create); }); - it('should add cli commands', () => { - return initReporter_() - .then(() => assert.calledOnce(PluginAdapter.prototype.addCliCommands)); + it('should add api', async () => { + await initReporter_(); + + assert.isObject(hermione.htmlReporter); }); - it('should init plugin', () => { - return initReporter_().then(() => assert.calledOnce(PluginAdapter.prototype.init)); + it('should add cli commands', async () => { + await initReporter_(); + + assert.called(program.command); }); it('should add skipped test to result', async () => { @@ -224,7 +224,7 @@ describe('lib/hermione', () => { hermione.emit(events[event], testResult); await hermione.emitAsync(hermione.events.RUNNER_END); - assert.deepEqual(StaticReportBuilder.prototype.addError.args[0][0].state, {name: 'some-title'}); + assert.deepEqual(StaticReportBuilder.prototype.addFail.args[0][0].state, {name: 'some-title'}); }); it(`errored assert view to result on ${event} event`, async () => { @@ -235,7 +235,7 @@ describe('lib/hermione', () => { hermione.emit(events[event], mkStubResult_({title: 'some-title', assertViewResults: [err]})); await hermione.emitAsync(hermione.events.RUNNER_END); - assert.deepEqual(StaticReportBuilder.prototype.addError.args[0][0].state, {name: 'some-title'}); + assert.deepEqual(StaticReportBuilder.prototype.addFail.args[0][0].state, {name: 'some-title'}); }); it(`failed test to result on ${event} event`, async () => { @@ -267,83 +267,4 @@ describe('lib/hermione', () => { }); }); }); - - it('should save image from passed test', async () => { - utils.getReferencePath.callsFake(({stateName}) => `report/${stateName}`); - - await initReporter_({path: '/absolute'}); - const testData = mkStubResult_({assertViewResults: [{refImg: {path: 'ref/path'}, stateName: 'plain'}]}); - hermione.emit(events.TEST_PASS, testData); - await hermione.emitAsync(events.RUNNER_END); - - assert.calledOnceWith(utils.copyFileAsync, 'ref/path', 'report/plain', {reportDir: '/absolute'}); - }); - - it('should save image from assert view error', async () => { - utils.getCurrentPath.callsFake(({stateName}) => `report/${stateName}`); - await initReporter_({path: '/absolute'}); - const err = new NoRefImageError(); - err.stateName = 'plain'; - err.currImg = {path: 'current/path'}; - - hermione.emit(events.RETRY, mkStubResult_({assertViewResults: [err]})); - await hermione.emitAsync(events.RUNNER_END); - - assert.calledOnceWith(utils.copyFileAsync, 'current/path', 'report/plain', {reportDir: '/absolute'}); - }); - - it('should save reference image from assert view fail', async () => { - utils.getReferencePath.callsFake(({stateName}) => `report/${stateName}`); - await initReporter_({path: '/absolute'}); - await stubWorkers(); - - const err = new ImageDiffError(); - err.stateName = 'plain'; - err.refImg = {path: 'reference/path'}; - - hermione.emit(events.TEST_FAIL, mkStubResult_({assertViewResults: [err]})); - await hermione.emitAsync(events.RUNNER_END); - - assert.calledWith(utils.copyFileAsync, 'reference/path', 'report/plain', {reportDir: '/absolute'}); - }); - - it('should save current image from assert view fail', async () => { - utils.getCurrentPath.callsFake(({stateName}) => `report/${stateName}`); - await initReporter_({path: '/absolute'}); - await hermione.emitAsync(events.RUNNER_START, { - registerWorkers: () => { - return {saveDiffTo: sandbox.stub()}; - } - }); - const err = new ImageDiffError(); - err.stateName = 'plain'; - err.currImg = {path: 'current/path'}; - - hermione.emit(events.TEST_FAIL, mkStubResult_({assertViewResults: [err]})); - await hermione.emitAsync(events.RUNNER_END); - - assert.calledWith(utils.copyFileAsync, 'current/path', 'report/plain', {reportDir: '/absolute'}); - }); - - it('should save current diff image from assert view fail', async () => { - fs.readFile.resolves(Buffer.from('some-buff')); - utils.getDiffPath.callsFake(({stateName}) => `report/${stateName}`); - const saveDiffTo = sinon.stub().resolves(); - const err = new ImageDiffError(); - err.stateName = 'plain'; - - await initReporter_(); - - await hermione.emitAsync(events.RUNNER_START, { - registerWorkers: () => { - return {saveDiffTo}; - } - }); - hermione.emit(events.TEST_FAIL, mkStubResult_({assertViewResults: [err]})); - await hermione.emitAsync(events.RUNNER_END); - - assert.calledWith( - saveDiffTo, sinon.match.instanceOf(ImageDiffError), sinon.match('/report/plain') - ); - }); }); diff --git a/test/unit/lib/common-utils.ts b/test/unit/lib/common-utils.ts index 98b0a0f1d..995d651de 100644 --- a/test/unit/lib/common-utils.ts +++ b/test/unit/lib/common-utils.ts @@ -1,4 +1,4 @@ -import {determineStatus, getError, hasDiff, getUrlWithBase} from 'lib/common-utils'; +import {determineFinalStatus, getError, hasDiff, getUrlWithBase} from 'lib/common-utils'; import {RUNNING, QUEUED, ERROR, FAIL, UPDATED, SUCCESS, IDLE, SKIPPED} from 'lib/constants/test-statuses'; import {ErrorName} from 'lib/errors'; @@ -66,13 +66,13 @@ describe('common-utils', () => { describe('determineStatus', () => { it(`should not rewrite suite status to "${IDLE}" if some test already has final status`, () => { - const status = determineStatus([SUCCESS, IDLE]); + const status = determineFinalStatus([SUCCESS, IDLE]); assert.equal(status, SUCCESS); }); it(`should return "${SUCCESS}" if statuses is not passed`, () => { - const status = determineStatus([]); + const status = determineFinalStatus([]); assert.equal(status, SUCCESS); }); @@ -81,8 +81,8 @@ describe('common-utils', () => { it(`should return "${RUNNING}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS, UPDATED, FAIL, ERROR, QUEUED, RUNNING]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, RUNNING); assert.equal(statusInReverseOrder, RUNNING); @@ -91,8 +91,8 @@ describe('common-utils', () => { it(`should return "${QUEUED}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS, UPDATED, FAIL, ERROR, QUEUED]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, QUEUED); assert.equal(statusInReverseOrder, QUEUED); @@ -101,8 +101,8 @@ describe('common-utils', () => { it(`should return "${ERROR}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS, UPDATED, FAIL, ERROR]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, ERROR); assert.equal(statusInReverseOrder, ERROR); @@ -111,8 +111,8 @@ describe('common-utils', () => { it(`should return "${FAIL}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS, UPDATED, FAIL]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, FAIL); assert.equal(statusInReverseOrder, FAIL); @@ -121,8 +121,8 @@ describe('common-utils', () => { it(`should return "${UPDATED}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS, UPDATED]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, UPDATED); assert.equal(statusInReverseOrder, UPDATED); @@ -131,8 +131,8 @@ describe('common-utils', () => { it(`should return "${SUCCESS}" regardless of order`, () => { const statuses = [SKIPPED, IDLE, SUCCESS]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, SUCCESS); assert.equal(statusInReverseOrder, SUCCESS); @@ -141,15 +141,15 @@ describe('common-utils', () => { it(`should return "${IDLE}" regardless of order`, () => { const statuses = [SKIPPED, IDLE]; - const statusInDirectOrder = determineStatus(statuses); - const statusInReverseOrder = determineStatus(statuses.reverse()); + const statusInDirectOrder = determineFinalStatus(statuses); + const statusInReverseOrder = determineFinalStatus(statuses.reverse()); assert.equal(statusInDirectOrder, IDLE); assert.equal(statusInReverseOrder, IDLE); }); it(`should return "${SKIPPED}"`, () => { - const status = determineStatus([SKIPPED]); + const status = determineFinalStatus([SKIPPED]); assert.equal(status, SKIPPED); }); diff --git a/test/unit/lib/gui/tool-runner/index.js b/test/unit/lib/gui/tool-runner/index.js index ccf975cfe..f9949ab97 100644 --- a/test/unit/lib/gui/tool-runner/index.js +++ b/test/unit/lib/gui/tool-runner/index.js @@ -8,6 +8,8 @@ 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 {SqliteClient} = require('lib/sqlite-client'); +const {PluginEvents, TestStatus} = require('lib/constants'); describe('lib/gui/tool-runner/index', () => { const sandbox = sinon.createSandbox(); @@ -75,7 +77,7 @@ describe('lib/gui/tool-runner/index', () => { }; reportBuilder = sinon.createStubInstance(GuiReportBuilder); - reportBuilder.getUpdatedAttempt.returns(0); + reportBuilder.addUpdated.callsFake(_.identity); subscribeOnToolEvents = sandbox.stub().named('reportSubscriber').resolves(); looksSame = sandbox.stub().named('looksSame').resolves({equal: true}); @@ -92,6 +94,7 @@ describe('lib/gui/tool-runner/index', () => { './runner': {createTestRunner}, './report-subscriber': {subscribeOnToolEvents}, './utils': toolRunnerUtils, + '../../sqlite-client': {SqliteClient: {create: () => sinon.createStubInstance(SqliteClient)}}, '../../db-utils/server': {getTestsTreeFromDatabase}, '../../reporter-helpers': { updateReferenceImage: sandbox.stub().resolves(), @@ -108,7 +111,7 @@ describe('lib/gui/tool-runner/index', () => { describe('initialize', () => { it('should set values added through api', () => { - const htmlReporter = {values: {foo: 'bar'}}; + const htmlReporter = {emit: sinon.stub(), values: {foo: 'bar'}}; hermione = stubTool(stubConfig(), {}, {}, htmlReporter); const gui = initGuiReporter(hermione); @@ -219,7 +222,8 @@ describe('lib/gui/tool-runner/index', () => { await gui.initialize(); - assert.callOrder(hermione.readTests, reportBuilder.init); + assert.callOrder(hermione.readTests, hermione.htmlReporter.emit); + assert.calledOnceWith(hermione.htmlReporter.emit, PluginEvents.DATABASE_CREATED, sinon.match.any); }); }); @@ -312,7 +316,10 @@ describe('lib/gui/tool-runner/index', () => { const mkUndoTestData_ = async (stubResult, {stateName = 'plain'} = {}) => { reportBuilder.undoAcceptImage.withArgs(sinon.match({ fullName: 'some-title' - }), 'plain').returns(stubResult); + }), 'plain').returns({ + newResult: {fullName: 'some-title'}, + ...stubResult + }); const tests = [{ id: 'some-id', fullTitle: () => 'some-title', @@ -325,7 +332,8 @@ describe('lib/gui/tool-runner/index', () => { stateName, actualImg: { size: {height: 100, width: 200} - } + }, + status: TestStatus.UPDATED }) ] }]; diff --git a/test/unit/lib/gui/tool-runner/report-subsciber.js b/test/unit/lib/gui/tool-runner/report-subsciber.js index ea19cb2ca..5616a8ae0 100644 --- a/test/unit/lib/gui/tool-runner/report-subsciber.js +++ b/test/unit/lib/gui/tool-runner/report-subsciber.js @@ -8,7 +8,7 @@ const {GuiReportBuilder} = require('lib/report-builder/gui'); const {ClientEvents} = require('lib/gui/constants'); const {stubTool, stubConfig} = require('test/unit/utils'); const {HermioneTestAdapter} = require('lib/test-adapter'); -const {ErrorName} = require('lib/errors'); +const {UNKNOWN_ATTEMPT} = require('lib/constants'); describe('lib/gui/tool-runner/hermione/report-subscriber', () => { const sandbox = sinon.createSandbox(); @@ -34,7 +34,10 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { beforeEach(() => { reportBuilder = sinon.createStubInstance(GuiReportBuilder); - reportBuilder.getCurrAttempt.returns(0); + reportBuilder.addFail.callsFake(_.identity); + reportBuilder.addError.callsFake(_.identity); + reportBuilder.addRunning.callsFake(_.identity); + reportBuilder.addSkipped.callsFake(_.identity); sandbox.stub(GuiReportBuilder, 'create').returns(reportBuilder); sandbox.stub(reportBuilder, 'imageHandler').value({saveTestImages: sinon.stub()}); @@ -61,7 +64,7 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { const testResult = mkHermioneTestResult(); const mediator = sinon.spy().named('mediator'); - reportBuilder.imageHandler.saveTestImages.callsFake(() => Promise.delay(100).then(mediator)); + reportBuilder.addError.callsFake(() => Promise.delay(100).then(mediator).then(() => ({id: 'some-id'}))); subscribeOnToolEvents(hermione, reportBuilder, client); hermione.emit(hermione.events.TEST_FAIL, testResult); @@ -72,16 +75,18 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { }); describe('TEST_BEGIN', () => { - it('should emit "BEGIN_STATE" event for client with correct data', () => { + it('should emit "BEGIN_STATE" event for client with correct data', async () => { const hermione = mkHermione_(); const testResult = mkHermioneTestResult(); + reportBuilder.addRunning.resolves({id: 'some-id'}); reportBuilder.getTestBranch.withArgs('some-id').returns('test-tree-branch'); subscribeOnToolEvents(hermione, reportBuilder, client); hermione.emit(hermione.events.TEST_BEGIN, testResult); + await hermione.emitAsync(hermione.events.RUNNER_END); - assert.calledOnceWith(client.emit, ClientEvents.BEGIN_STATE, 'test-tree-branch'); + assert.calledWith(client.emit, ClientEvents.BEGIN_STATE, 'test-tree-branch'); }); }); @@ -97,7 +102,7 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { assert.calledOnceWith(reportBuilder.addSkipped, sinon.match({ fullName: 'some-title', browserId: 'some-browser', - attempt: 0 + attempt: UNKNOWN_ATTEMPT })); }); @@ -116,30 +121,6 @@ describe('lib/gui/tool-runner/hermione/report-subscriber', () => { }); describe('TEST_FAIL', () => { - it('should add correct attempt', async () => { - const hermione = mkHermione_(); - const testResult = mkHermioneTestResult({assertViewResults: [{name: ErrorName.IMAGE_DIFF}]}); - - reportBuilder.getCurrAttempt.returns(1); - - subscribeOnToolEvents(hermione, reportBuilder, client); - hermione.emit(hermione.events.TEST_FAIL, testResult); - await hermione.emitAsync(hermione.events.RUNNER_END); - - assert.calledWithMatch(reportBuilder.addFail, {attempt: 1}); - }); - - it('should save images before fail adding', async () => { - const hermione = mkHermione_(); - const testResult = mkHermioneTestResult({assertViewResults: [{name: ErrorName.IMAGE_DIFF}]}); - - subscribeOnToolEvents(hermione, reportBuilder, client); - hermione.emit(hermione.events.TEST_FAIL, testResult); - await hermione.emitAsync(hermione.events.RUNNER_END); - - assert.callOrder(reportBuilder.imageHandler.saveTestImages, reportBuilder.addFail); - }); - it('should emit "TEST_RESULT" event for client with test data', async () => { const hermione = mkHermione_(); const testResult = mkHermioneTestResult(); diff --git a/test/unit/lib/plugin-adapter.js b/test/unit/lib/plugin-adapter.js deleted file mode 100644 index a2dd57201..000000000 --- a/test/unit/lib/plugin-adapter.js +++ /dev/null @@ -1,210 +0,0 @@ -'use strict'; - -const {EventEmitter} = require('events'); -const _ = require('lodash'); -const proxyquire = require('proxyquire'); -const {logger} = require('lib/common-utils'); -const {StaticReportBuilder} = require('lib/report-builder/static'); -const {HtmlReporter} = require('lib/plugin-api'); -const {stubTool, stubConfig} = require('../utils'); -const {GUI, MERGE_REPORTS, REMOVE_UNUSED_SCREENS} = require('lib/cli-commands').cliCommands; - -describe('lib/plugin-adapter', () => { - const sandbox = sinon.createSandbox(); - const cliCommands = {}; - let parseConfig; - let tool; - let toolReporter; - let commander; - let prepareData; - - const events = { - CLI: 'cli', - INIT: 'init' - }; - - function mkHermione_() { - return stubTool(stubConfig(), Object.assign(events, {RUNNER_END: 'runnerEnd'})); - } - - function initReporter_(opts = {}) { - opts = _.defaults(opts, {enabled: true, path: ''}); - parseConfig.returns(opts); - - return toolReporter.create(tool, opts) - .addCliCommands() - .init(prepareData); - } - - function initApiReporter_(opts) { - initReporter_(opts); - return tool.emitAsync(tool.events.INIT); - } - - function initCliReporter_(opts, {command = 'foo'} = {}) { - initReporter_(opts); - - const commander = mkCommander_(command); - tool.emit(tool.events.CLI, commander); - commander.emit(`command:${command}`); - - return tool.emitAsync(tool.events.INIT); - } - - function mkCommander_(commands = ['default-command']) { - commander = new EventEmitter(); - commander.commands = [].concat(commands).map((cmd) => ({name: () => cmd})); - - return commander; - } - - beforeEach(() => { - sandbox.stub(logger, 'log'); - sandbox.stub(logger, 'error'); - - sandbox.stub(StaticReportBuilder, 'create').returns(Object.create(StaticReportBuilder.prototype)); - sandbox.stub(StaticReportBuilder.prototype, 'saveStaticFiles').resolves(); - sandbox.stub(StaticReportBuilder.prototype, 'init').resolves(); - sandbox.stub(StaticReportBuilder.prototype, 'finalize').resolves(); - - prepareData = sandbox.stub().resolves(); - - tool = mkHermione_(); - parseConfig = sandbox.stub().returns({enabled: true}); - cliCommands[GUI] = sandbox.stub(); - cliCommands[MERGE_REPORTS] = sandbox.stub(); - cliCommands[REMOVE_UNUSED_SCREENS] = sandbox.stub(); - - toolReporter = proxyquire('lib/plugin-adapter', { - './config': {parseConfig}, - [`./cli-commands/${GUI}`]: cliCommands[GUI], - [`./cli-commands/${MERGE_REPORTS}`]: cliCommands[MERGE_REPORTS], - [`./cli-commands/${REMOVE_UNUSED_SCREENS}`]: cliCommands[REMOVE_UNUSED_SCREENS] - }).PluginAdapter; - }); - - afterEach(() => sandbox.restore()); - - it('should parse config using passed options', () => { - const opts = {path: 'some/path', enabled: false, baseHost: 'some-host'}; - - toolReporter.create(tool, opts); - - assert.calledWith(parseConfig, {path: 'some/path', enabled: false, baseHost: 'some-host'}); - }); - - describe('isEnabled', () => { - it('should be enabled', () => { - const plugin = toolReporter.create(tool, {enabled: true}); - - assert.isTrue(plugin.isEnabled()); - }); - - it('should be disabled', () => { - const opts = {enabled: false}; - parseConfig.withArgs(opts).returns(opts); - - const plugin = toolReporter.create(tool, opts); - - assert.isFalse(plugin.isEnabled()); - }); - }); - - [ - 'gui', - 'merge-reports' - ].forEach((commandName) => { - describe(`${commandName} command`, () => { - it('should register command on "CLI" event', () => { - const opts = {enabled: true}; - const commander = mkCommander_(commandName); - - parseConfig.withArgs(opts).returns(opts); - const plugin = toolReporter.create(tool, opts); - - plugin.addCliCommands(); - tool.emit(tool.events.CLI, commander); - - assert.calledOnceWith(cliCommands[commandName], commander, opts, tool); - }); - - it('should add api', () => { - const opts = {enabled: true}; - const plugin = toolReporter.create(tool, opts); - - assert.deepEqual(plugin.addApi(), plugin); - assert.instanceOf(tool.htmlReporter, HtmlReporter); - }); - - it(`should not register command if hermione called via API`, () => { - return initApiReporter_({}).then(() => assert.notCalled(cliCommands[commandName])); - }); - - it('should not init html-reporter on running command', () => { - return initCliReporter_({}, {command: commandName}).then(() => assert.notCalled(StaticReportBuilder.create)); - }); - }); - }); - - describe('html-reporter', () => { - let reportBuilder; - - beforeEach(() => { - reportBuilder = Object.create(StaticReportBuilder.prototype); - StaticReportBuilder.create.returns(reportBuilder); - }); - - it(`should init html-reporter if hermione called via API`, () => { - return initApiReporter_({}).then(() => assert.calledOnce(StaticReportBuilder.create)); - }); - - it('should prepare data', () => { - return initCliReporter_({}, {}) - .then(() => assert.calledOnceWith(prepareData, tool, reportBuilder)); - }); - - it('should save report', () => { - return initCliReporter_({}, {}) - .then(() => { - tool.emit(tool.events.END); - - return tool.emitAsync(tool.events.RUNNER_END).then(() => { - assert.calledOnce(StaticReportBuilder.prototype.finalize); - }); - }); - }); - - it('should emit REPORT_SAVED event', async () => { - await initCliReporter_({path: '/some/report/path'}, {}); - - tool.emit(tool.events.END); - await tool.emitAsync(tool.events.RUNNER_END); - - assert.calledOnceWith(tool.htmlReporter.emitAsync, 'reportSaved', {reportPath: '/some/report/path'}); - }); - - it('should log correct path to html report', () => { - return initCliReporter_({path: 'some/path'}, {}) - .then(() => { - tool.emit(tool.events.END); - - return tool.emitAsync(tool.events.RUNNER_END).then(() => { - assert.calledWithMatch(logger.log, 'some/path'); - }); - }); - }); - - it('should log an error', () => { - StaticReportBuilder.prototype.finalize.rejects('some-error'); - - return initCliReporter_({}, {}) - .then(() => { - tool.emit(tool.events.END); - - return tool.emitAsync(tool.events.RUNNER_END).then(() => { - assert.calledWith(logger.error, sinon.match('Html-reporter runtime error: some-error')); - }); - }); - }); - }); -}); diff --git a/test/unit/lib/report-builder/gui.js b/test/unit/lib/report-builder/gui.js index c6acec0aa..a269c6c71 100644 --- a/test/unit/lib/report-builder/gui.js +++ b/test/unit/lib/report-builder/gui.js @@ -5,34 +5,43 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); const serverUtils = require('lib/server-utils'); const {HermioneTestAdapter} = require('lib/test-adapter'); -const {SqliteAdapter} = require('lib/sqlite-adapter'); +const {SqliteClient} = require('lib/sqlite-client'); const {GuiTestsTreeBuilder} = require('lib/tests-tree-builder/gui'); const {HtmlReporter} = require('lib/plugin-api'); const {SUCCESS, FAIL, ERROR, SKIPPED, IDLE, RUNNING, UPDATED} = require('lib/constants/test-statuses'); const {LOCAL_DATABASE_NAME} = require('lib/constants/database'); -const {ErrorName} = require('lib/errors'); +const {TestAttemptManager} = require('lib/test-attempt-manager'); +const {ImageDiffError} = require('../../utils'); +const {ImageHandler} = require('lib/image-handler'); const TEST_REPORT_PATH = 'test'; const TEST_DB_PATH = `${TEST_REPORT_PATH}/${LOCAL_DATABASE_NAME}`; describe('GuiReportBuilder', () => { const sandbox = sinon.sandbox.create(); - let hasImage, deleteFile, GuiReportBuilder; + let hasImage, deleteFile, GuiReportBuilder, dbClient, testAttemptManager, copyAndUpdate; const mkGuiReportBuilder_ = async ({toolConfig, pluginConfig} = {}) => { toolConfig = _.defaults(toolConfig || {}, {getAbsoluteUrl: _.noop}); pluginConfig = _.defaults(pluginConfig || {}, {baseHost: '', path: TEST_REPORT_PATH, baseTestPath: ''}); + const htmlReporter = HtmlReporter.create({baseHost: ''}); + const browserConfigStub = {getAbsoluteUrl: toolConfig.getAbsoluteUrl}; const hermione = { forBrowser: sandbox.stub().returns(browserConfigStub), - htmlReporter: HtmlReporter.create() + htmlReporter }; HermioneTestAdapter.create = (obj) => obj; - const reportBuilder = GuiReportBuilder.create(hermione.htmlReporter, pluginConfig); - await reportBuilder.init(); + dbClient = await SqliteClient.create({htmlReporter, reportPath: TEST_REPORT_PATH}); + testAttemptManager = new TestAttemptManager(); + + const reportBuilder = GuiReportBuilder.create(hermione.htmlReporter, pluginConfig, {dbClient, testAttemptManager}); + + const workers = {saveDiffTo: () => {}}; + reportBuilder.registerWorkers(workers); return reportBuilder; }; @@ -74,15 +83,23 @@ describe('GuiReportBuilder', () => { sandbox.stub(fs, 'writeFileSync'); sandbox.stub(serverUtils, 'prepareCommonJSData'); + copyAndUpdate = sandbox.stub().callsFake(_.identity); + + const imageHandler = sandbox.createStubInstance(ImageHandler); + hasImage = sandbox.stub().returns(true); deleteFile = sandbox.stub().resolves(); GuiReportBuilder = proxyquire('lib/report-builder/gui', { './static': { StaticReportBuilder: proxyquire('lib/report-builder/static', { - '../sqlite-adapter': {SqliteAdapter} + '../sqlite-client': {SqliteClient}, + '../image-handler': {ImageHandler: function() { + return imageHandler; + }} }).StaticReportBuilder }, - '../server-utils': {hasImage, deleteFile} + '../server-utils': {hasImage, deleteFile}, + '../test-adapter/utils': {copyAndUpdate} }).GuiReportBuilder; sandbox.stub(GuiTestsTreeBuilder, 'create').returns(Object.create(GuiTestsTreeBuilder.prototype)); @@ -92,7 +109,6 @@ describe('GuiReportBuilder', () => { sandbox.stub(GuiTestsTreeBuilder.prototype, 'getTestsDataToUpdateRefs').returns([]); sandbox.stub(GuiTestsTreeBuilder.prototype, 'getImageDataToFindEqualDiffs').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'getImagesInfo').returns([]); - sandbox.stub(GuiTestsTreeBuilder.prototype, 'getLastResult').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'addTestResult').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'getResultDataToUnacceptImage').returns({}); sandbox.stub(GuiTestsTreeBuilder.prototype, 'updateImageInfo').returns({}); @@ -108,7 +124,7 @@ describe('GuiReportBuilder', () => { it(`should add "${IDLE}" status to result`, async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addIdle(stubTest_()); + await reportBuilder.addIdle(stubTest_()); assert.equal(getTestResult_().status, IDLE); }); @@ -118,7 +134,7 @@ describe('GuiReportBuilder', () => { it(`should add "${RUNNING}" status to result`, async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addRunning(stubTest_()); + await reportBuilder.addRunning(stubTest_()); assert.equal(getTestResult_().status, RUNNING); }); @@ -128,7 +144,7 @@ describe('GuiReportBuilder', () => { it('should add skipped test to results', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSkipped(stubTest_({ + await reportBuilder.addSkipped(stubTest_({ browserId: 'bro1', skipReason: 'some skip comment', fullName: 'suite-full-name' @@ -147,7 +163,7 @@ describe('GuiReportBuilder', () => { it('should add success test to result', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSuccess(stubTest_({ + await reportBuilder.addSuccess(stubTest_({ browserId: 'bro1' })); @@ -162,7 +178,7 @@ describe('GuiReportBuilder', () => { it('should add failed test to result', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addFail(stubTest_({ + await reportBuilder.addFail(stubTest_({ browserId: 'bro1', imageDir: 'some-image-dir' })); @@ -178,7 +194,7 @@ describe('GuiReportBuilder', () => { it('should add error test to result', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addError(stubTest_({error: {message: 'some-error-message'}})); + await reportBuilder.addError(stubTest_({error: {message: 'some-error-message'}})); assert.match(getTestResult_(), { status: ERROR, @@ -191,7 +207,7 @@ describe('GuiReportBuilder', () => { it('should add "fail" status to result if test result has not equal images', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addRetry(stubTest_({assertViewResults: [{name: ErrorName.IMAGE_DIFF}]})); + await reportBuilder.addRetry(stubTest_({assertViewResults: [new ImageDiffError()]})); assert.equal(getTestResult_().status, FAIL); }); @@ -199,31 +215,32 @@ describe('GuiReportBuilder', () => { it('should add "error" status to result if test result has no image', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addRetry(stubTest_({assertViewResults: [{name: 'some-error-name'}]})); + await reportBuilder.addRetry(stubTest_({assertViewResults: [{name: 'some-error-name'}]})); assert.equal(getTestResult_().status, ERROR); }); }); describe('"addUpdated" method', () => { - it(`should add "${SUCCESS}" status to result if all images updated`, async () => { + it(`should add "${UPDATED}" status to result if all images updated`, async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addUpdated(stubTest_({imagesInfo: [{status: UPDATED}]})); + await reportBuilder.addUpdated(stubTest_({testPath: [], imagesInfo: [{status: UPDATED}]})); - assert.equal(getTestResult_().status, SUCCESS); + assert.equal(getTestResult_().status, UPDATED); }); - it(`should correctly determine the status based on current result`, async () => { + it(`should add "${UPDATED}" status even if result has errors`, async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addUpdated(stubTest_({ + await reportBuilder.addUpdated(stubTest_({ + testPath: [], error: {name: 'some-error', message: 'some-message'}, imagesInfo: [{status: FAIL}], attempt: 4 })); - assert.equal(getTestResult_().status, ERROR); + assert.equal(getTestResult_().status, UPDATED); }); it('should update test image for current state name', async () => { @@ -240,12 +257,13 @@ describe('GuiReportBuilder', () => { imagesInfo: [ {stateName: 'plain1', status: UPDATED}, {stateName: 'plain2', status: FAIL} - ] + ], + testPath: [] }); - reportBuilder.addFail(failedTest); + await reportBuilder.addFail(failedTest); GuiTestsTreeBuilder.prototype.getImagesInfo.returns(failedTest.imagesInfo); - reportBuilder.addUpdated(updatedTest); + await reportBuilder.addUpdated(updatedTest); const updatedTestResult = GuiTestsTreeBuilder.prototype.addTestResult.secondCall.args[0]; @@ -266,12 +284,13 @@ describe('GuiReportBuilder', () => { id: 'result-2', imagesInfo: [ {status: UPDATED} - ] + ], + testPath: [] }); - reportBuilder.addFail(failedTest); + await reportBuilder.addFail(failedTest); GuiTestsTreeBuilder.prototype.getImagesInfo.returns(failedTest.imagesInfo); - reportBuilder.addUpdated(updatedTest, 'result-2'); + await reportBuilder.addUpdated(updatedTest, 'result-2'); const {imagesInfo} = GuiTestsTreeBuilder.prototype.addTestResult.secondCall.args[0]; @@ -293,7 +312,7 @@ describe('GuiReportBuilder', () => { [ { method: 'reuseTestsTree', - arg: 'some-tree' + arg: {results: {byId: {}}} }, { method: 'getTestBranch', @@ -335,32 +354,6 @@ describe('GuiReportBuilder', () => { }); }); - describe('"getCurrAttempt" method', () => { - [IDLE, SKIPPED].forEach((status) => { - it(`should return attempt for last result if status is "${status}"`, async () => { - const formattedResult = {status, attempt: 100500}; - GuiTestsTreeBuilder.prototype.getLastResult.returns(formattedResult); - const reportBuilder = await mkGuiReportBuilder_(); - - const currAttempt = reportBuilder.getCurrAttempt(formattedResult); - - assert.equal(currAttempt, formattedResult.attempt); - }); - }); - - [SUCCESS, FAIL, ERROR, UPDATED].forEach((status) => { - it(`should return attempt for next result if status is "${status}"`, async () => { - const formattedResult = {status, attempt: 100500}; - GuiTestsTreeBuilder.prototype.getLastResult.returns(formattedResult); - const reportBuilder = await mkGuiReportBuilder_(); - - const currAttempt = reportBuilder.getCurrAttempt(formattedResult); - - assert.equal(currAttempt, formattedResult.attempt + 1); - }); - }); - }); - describe('"undoAcceptImages"', () => { let reportBuilder; @@ -391,7 +384,7 @@ describe('GuiReportBuilder', () => { beforeEach(async () => { reportBuilder = await mkGuiReportBuilder_(); - sandbox.stub(SqliteAdapter.prototype, 'delete'); + sandbox.stub(SqliteClient.prototype, 'delete'); }); describe('if image status is not "updated"', () => { @@ -419,7 +412,7 @@ describe('GuiReportBuilder', () => { it('should not delete test from db', async () => { await tryUndoFailedImage_(); - assert.notCalled(SqliteAdapter.prototype.delete); + assert.notCalled(SqliteClient.prototype.delete); }); }); @@ -434,7 +427,7 @@ describe('GuiReportBuilder', () => { assert.calledOnceWith(GuiTestsTreeBuilder.prototype.removeTestResult, 'result-id'); }); - it('should resolve removed result id', async () => { + it('should resolve removed result', async () => { const resultId = 'result-id'; const stateName = 's-name'; const formattedResult = mkFormattedResultStub_({id: resultId, stateName}); @@ -442,7 +435,7 @@ describe('GuiReportBuilder', () => { const {removedResult} = await reportBuilder.undoAcceptImage(formattedResult, stateName); - assert.deepEqual(removedResult, 'result-id'); + assert.deepEqual(removedResult, formattedResult); }); it('should update image info if "shouldRemoveResult" is false', async () => { @@ -491,7 +484,7 @@ describe('GuiReportBuilder', () => { await reportBuilder.undoAcceptImage(formattedResult, stateName); assert.calledOnceWith( - SqliteAdapter.prototype.delete, + SqliteClient.prototype.delete, sinon.match.any, '["s","p"]', 'bro-name', @@ -529,7 +522,7 @@ describe('GuiReportBuilder', () => { it('"suiteUrl" field', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSuccess(stubTest_({ + await reportBuilder.addSuccess(stubTest_({ url: 'some-url' })); @@ -539,7 +532,7 @@ describe('GuiReportBuilder', () => { it('"name" field as browser id', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSuccess(stubTest_({browserId: 'yabro'})); + await reportBuilder.addSuccess(stubTest_({browserId: 'yabro'})); assert.equal(getTestResult_().name, 'yabro'); }); @@ -547,7 +540,7 @@ describe('GuiReportBuilder', () => { it('"metaInfo" field', async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSuccess(stubTest_({ + await reportBuilder.addSuccess(stubTest_({ meta: {some: 'value', sessionId: '12345'}, file: '/path/file.js', url: '/test/url' @@ -567,7 +560,7 @@ describe('GuiReportBuilder', () => { it(`add "${name}" field`, async () => { const reportBuilder = await mkGuiReportBuilder_(); - reportBuilder.addSuccess(stubTest_({[name]: value})); + await reportBuilder.addSuccess(stubTest_({[name]: value})); assert.deepEqual(getTestResult_()[name], value); }); @@ -578,7 +571,7 @@ describe('GuiReportBuilder', () => { const reportBuilder = await mkGuiReportBuilder_({pluginConfig: {saveErrorDetails: false}}); const errorDetails = {title: 'some-title', filePath: 'some-path'}; - reportBuilder.addFail(stubTest_({errorDetails})); + await reportBuilder.addFail(stubTest_({errorDetails})); assert.isUndefined(getTestResult_().errorDetails); }); @@ -587,7 +580,7 @@ describe('GuiReportBuilder', () => { const reportBuilder = await mkGuiReportBuilder_({pluginConfig: {saveErrorDetails: true}}); const errorDetails = {title: 'some-title', filePath: 'some-path'}; - reportBuilder.addFail(stubTest_({errorDetails})); + await reportBuilder.addFail(stubTest_({errorDetails})); assert.deepEqual(getTestResult_().errorDetails, errorDetails); }); diff --git a/test/unit/lib/report-builder/static.js b/test/unit/lib/report-builder/static.js index 996fb2a7c..081dd5c10 100644 --- a/test/unit/lib/report-builder/static.js +++ b/test/unit/lib/report-builder/static.js @@ -7,13 +7,18 @@ const proxyquire = require('proxyquire'); const {HtmlReporter} = require('lib/plugin-api'); const {SUCCESS, FAIL, ERROR, SKIPPED} = require('lib/constants/test-statuses'); const {LOCAL_DATABASE_NAME} = require('lib/constants/database'); +const {SqliteClient} = require('lib/sqlite-client'); +const {NoRefImageError, ImageDiffError} = require('../../utils'); +const sinon = require('sinon'); +const path = require('path'); const TEST_REPORT_PATH = 'test'; const TEST_DB_PATH = `${TEST_REPORT_PATH}/${LOCAL_DATABASE_NAME}`; describe('StaticReportBuilder', () => { const sandbox = sinon.sandbox.create(); - let StaticReportBuilder, htmlReporter; + let StaticReportBuilder, htmlReporter, dbClient; + let cacheExpectedPaths = new Map(), cacheAllImages = new Map(), cacheDiffImages = new Map(); const fs = _.clone(fsOriginal); @@ -22,17 +27,32 @@ describe('StaticReportBuilder', () => { }); const utils = _.clone(originalUtils); - const mkStaticReportBuilder_ = async ({pluginConfig} = {}) => { + const {ImageHandler} = proxyquire('lib/image-handler', { + 'fs-extra': fs, + './image-cache': {cacheExpectedPaths, cacheAllImages, cacheDiffImages}, + './server-utils': utils + }); + + const {LocalImagesSaver} = proxyquire('lib/local-images-saver', { + './server-utils': utils + }); + + const mkStaticReportBuilder_ = async ({pluginConfig, workers} = {}) => { pluginConfig = _.defaults(pluginConfig, {baseHost: '', path: TEST_REPORT_PATH, baseTestPath: ''}); - htmlReporter = _.extend(HtmlReporter.create(), { + htmlReporter = _.extend(HtmlReporter.create({baseHost: ''}), { reportsSaver: { saveReportData: sandbox.stub() - } + }, + imagesSaver: LocalImagesSaver }); - const reportBuilder = StaticReportBuilder.create(htmlReporter, pluginConfig); - await reportBuilder.init(); + dbClient = await SqliteClient.create({htmlReporter, reportPath: TEST_REPORT_PATH}); + + const reportBuilder = StaticReportBuilder.create(htmlReporter, pluginConfig, {dbClient}); + workers = workers ?? {saveDiffTo: () => {}}; + + reportBuilder.registerWorkers(workers); return reportBuilder; }; @@ -52,11 +72,16 @@ describe('StaticReportBuilder', () => { StaticReportBuilder = proxyquire('lib/report-builder/static', { 'fs-extra': fs, - '../server-utils': utils + '../server-utils': utils, + '../image-handler': {ImageHandler} }).StaticReportBuilder; }); afterEach(() => { + cacheAllImages.clear(); + cacheExpectedPaths.clear(); + cacheDiffImages.clear(); + fs.removeSync(TEST_DB_PATH); sandbox.restore(); }); @@ -69,7 +94,7 @@ describe('StaticReportBuilder', () => { }); it('should add skipped test', async () => { - await reportBuilder.addSkipped(stubTest_()); + await reportBuilder.addSkipped(stubTest_({status: SKIPPED})); const db = new Database(TEST_DB_PATH); const [{status}] = db.prepare('SELECT * from suites').all(); @@ -79,7 +104,7 @@ describe('StaticReportBuilder', () => { }); it('should add success test', async () => { - await reportBuilder.addSuccess(stubTest_()); + await reportBuilder.addSuccess(stubTest_({status: SUCCESS})); const db = new Database(TEST_DB_PATH); const [{status}] = db.prepare('SELECT * from suites').all(); @@ -89,7 +114,7 @@ describe('StaticReportBuilder', () => { }); it('should add failed test', async () => { - await reportBuilder.addFail(stubTest_()); + await reportBuilder.addFail(stubTest_({status: FAIL})); const db = new Database(TEST_DB_PATH); const [{status}] = db.prepare('SELECT * from suites').all(); @@ -99,7 +124,7 @@ describe('StaticReportBuilder', () => { }); it('should add error test', async () => { - await reportBuilder.addError(stubTest_()); + await reportBuilder.addError(stubTest_({status: ERROR})); const db = new Database(TEST_DB_PATH); const [{status}] = db.prepare('SELECT * from suites').all(); @@ -129,6 +154,121 @@ describe('StaticReportBuilder', () => { }); }); + describe('saving images', () => { + let reportBuilder, saveDiffTo; + + beforeEach(async () => { + saveDiffTo = sinon.stub(); + + reportBuilder = await mkStaticReportBuilder_({workers: {saveDiffTo}}); + + sandbox.stub(utils, 'getReferencePath'); + sandbox.stub(utils, 'getCurrentPath'); + sandbox.stub(utils, 'getDiffPath').returns('diff'); + sandbox.stub(utils, 'copyFileAsync'); + sandbox.stub(utils, 'makeDirFor'); + + sandbox.stub(fs, 'readFile').resolves(Buffer.from('')); + }); + + it('should save image from passed test', async () => { + utils.getReferencePath.callsFake(({stateName}) => `report/${stateName}`); + + await reportBuilder.addSuccess(stubTest_({assertViewResults: [{refImg: {path: 'ref/path'}, stateName: 'plain'}]})); + + assert.calledOnceWith(utils.copyFileAsync, 'ref/path', 'report/plain', {reportDir: 'test'}); + }); + + it('should save image from assert view error', async () => { + utils.getCurrentPath.callsFake(({stateName}) => `report/${stateName}`); + + const err = new NoRefImageError(); + err.stateName = 'plain'; + err.currImg = {path: 'current/path'}; + + await reportBuilder.addFail(stubTest_({assertViewResults: [err]})); + + assert.calledOnceWith(utils.copyFileAsync, 'current/path', 'report/plain', {reportDir: 'test'}); + }); + + it('should save reference image from assert view fail', async () => { + utils.getReferencePath.callsFake(({stateName}) => `report/${stateName}`); + + const err = new ImageDiffError(); + err.stateName = 'plain'; + err.refImg = {path: 'reference/path'}; + + await reportBuilder.addFail(stubTest_({assertViewResults: [err]})); + + assert.calledWith(utils.copyFileAsync, 'reference/path', 'report/plain', {reportDir: 'test'}); + }); + + it('should save current image from assert view fail', async () => { + utils.getCurrentPath.callsFake(({stateName}) => `report/${stateName}`); + + const err = new ImageDiffError(); + err.stateName = 'plain'; + err.currImg = {path: 'current/path'}; + + await reportBuilder.addFail(stubTest_({assertViewResults: [err]})); + + assert.calledWith(utils.copyFileAsync, 'current/path', 'report/plain', {reportDir: 'test'}); + }); + + it('should save current diff image from assert view fail', async () => { + fs.readFile.resolves(Buffer.from('some-buff')); + utils.getDiffPath.callsFake(({stateName}) => `report/${stateName}`); + + const err = new ImageDiffError(); + err.stateName = 'plain'; + + await reportBuilder.addFail(stubTest_({assertViewResults: [err]})); + + assert.calledWith( + saveDiffTo, sinon.match.instanceOf(ImageDiffError), sinon.match('/report/plain') + ); + }); + }); + + describe('saving error details', () => { + let reportBuilder; + + beforeEach(async () => { + reportBuilder = await mkStaticReportBuilder_({pluginConfig: {saveErrorDetails: true}}); + + sandbox.stub(utils, 'makeDirFor').resolves(); + sandbox.stub(utils, 'getDetailsFileName').returns('md5-bro-n-time'); + + sandbox.stub(fs, 'writeFile'); + sandbox.stub(fs, 'mkdirs'); + }); + + it('should do nothing if no error details are available', async () => { + await reportBuilder.addFail(stubTest_()); + + assert.notCalled(fs.writeFile); + }); + + it('should save error details to correct path', async () => { + await reportBuilder.addFail(stubTest_({errorDetails: {filePath: 'some-path'}})); + + assert.calledWithMatch(fs.writeFile, path.resolve(`${TEST_REPORT_PATH}/some-path`), sinon.match.any); + }); + + it('should create directory for error details', async () => { + await reportBuilder.addFail(stubTest_({errorDetails: {filePath: `some-dir/some-path`}})); + + assert.calledOnceWith(fs.mkdirs, path.resolve(TEST_REPORT_PATH, 'some-dir')); + }); + + it('should save error details', async () => { + const data = {foo: 'bar'}; + await reportBuilder.addFail(stubTest_({errorDetails: {filePath: 'some-path', data}})); + + assert.calledWith(fs.writeFile, sinon.match.any, JSON.stringify(data, null, 2)); + }); + }); + describe('finalization', () => { let reportBuilder; diff --git a/test/unit/lib/sqlite-adapter.js b/test/unit/lib/sqlite-client.js similarity index 65% rename from test/unit/lib/sqlite-adapter.js rename to test/unit/lib/sqlite-client.js index 90a2866ce..7b697566e 100644 --- a/test/unit/lib/sqlite-adapter.js +++ b/test/unit/lib/sqlite-client.js @@ -4,21 +4,19 @@ const fs = require('fs-extra'); const proxyquire = require('proxyquire'); const Database = require('better-sqlite3'); -const {SqliteAdapter} = require('lib/sqlite-adapter'); +const {SqliteClient} = require('lib/sqlite-client'); const {HtmlReporter} = require('lib/plugin-api'); -describe('lib/sqlite-adapter', () => { +describe('lib/sqlite-client', () => { const sandbox = sinon.createSandbox(); let htmlReporter; - const makeSqliteAdapter_ = async () => { - const sqliteAdapter = SqliteAdapter.create({htmlReporter, reportPath: 'test'}); - await sqliteAdapter.init(); - return sqliteAdapter; + const makeSqliteClient_ = async () => { + return SqliteClient.create({htmlReporter, reportPath: 'test'}); }; beforeEach(() => { - htmlReporter = HtmlReporter.create(); + htmlReporter = HtmlReporter.create({baseHost: 'some-host'}); }); afterEach(() => { @@ -27,13 +25,13 @@ describe('lib/sqlite-adapter', () => { }); it('should create database', async () => { - await makeSqliteAdapter_(); + await makeSqliteClient_(); assert.equal(fs.existsSync('test/sqlite.db'), true); }); it('should create database with correct structure', async () => { - await makeSqliteAdapter_(); + await makeSqliteClient_(); const db = new Database('test/sqlite.db'); const tableStructure = [ {cid: 0, name: 'suitePath', type: 'TEXT'}, @@ -62,112 +60,99 @@ describe('lib/sqlite-adapter', () => { }); }); - it('should emit "DATABASE_CREATED" event with new database connection', async () => { - const onDatabaseCreated = sinon.spy(); - htmlReporter.on(htmlReporter.events.DATABASE_CREATED, onDatabaseCreated); - - await makeSqliteAdapter_(); - - assert.calledOnceWith(onDatabaseCreated, sinon.match.instanceOf(Database)); - }); - describe('query', () => { - let getStub, prepareStub, sqliteAdapter; + let getStub, prepareStub, sqliteClient; beforeEach(async () => { getStub = sandbox.stub(); prepareStub = sandbox.stub(Database.prototype, 'prepare').returns({get: getStub}); - sqliteAdapter = proxyquire('lib/sqlite-adapter', { + sqliteClient = await proxyquire('lib/sqlite-client', { './db-utils/common': {createTablesQuery: () => []} - }).SqliteAdapter.create({htmlReporter, reportPath: 'test'}); - - await sqliteAdapter.init(); + }).SqliteClient.create({htmlReporter, reportPath: 'test'}); }); describe('should create valid query string', () => { it('if called with no query params', () => { - sqliteAdapter.query(); + sqliteClient.query(); assert.calledOnceWith(prepareStub, 'SELECT * FROM suites'); }); it('if called with "select", "where", "order" and "orderDescending"', () => { - sqliteAdapter.query({select: 'foo', where: 'bar', orderBy: 'baz', orderDescending: true, limit: 42}); + sqliteClient.query({select: 'foo', where: 'bar', orderBy: 'baz', orderDescending: true, limit: 42}); assert.calledOnceWith(prepareStub, 'SELECT foo FROM suites WHERE bar ORDER BY baz DESC LIMIT 42'); }); it('if "orderDescending" is not specified', () => { - sqliteAdapter.query({select: 'foo', orderBy: 'baz'}); + sqliteClient.query({select: 'foo', orderBy: 'baz'}); assert.calledOnceWith(prepareStub, 'SELECT foo FROM suites ORDER BY baz ASC'); }); }); it('should apply query arguments', () => { - sqliteAdapter.query({}, 'foo', 'bar'); + sqliteClient.query({}, 'foo', 'bar'); assert.calledOnceWith(getStub, 'foo', 'bar'); }); it('should cache equal queries by default', () => { - sqliteAdapter.query({select: 'foo', where: 'bar'}); - sqliteAdapter.query({select: 'foo', where: 'bar'}); + sqliteClient.query({select: 'foo', where: 'bar'}); + sqliteClient.query({select: 'foo', where: 'bar'}); assert.calledOnce(getStub); }); it('should not cache queries if "noCache" is set', () => { - sqliteAdapter.query({select: 'foo', noCache: true}); - sqliteAdapter.query({select: 'foo', noCache: true}); + sqliteClient.query({select: 'foo', noCache: true}); + sqliteClient.query({select: 'foo', noCache: true}); assert.calledTwice(getStub); }); it('should not use cache for different queries', () => { - sqliteAdapter.query({select: 'foo', where: 'bar'}); - sqliteAdapter.query({select: 'foo', where: 'baz'}); + sqliteClient.query({select: 'foo', where: 'bar'}); + sqliteClient.query({select: 'foo', where: 'baz'}); assert.calledTwice(getStub); }); it('should not use cache for queries with different args', () => { - sqliteAdapter.query({select: 'foo', where: 'bar = ?'}, 'baz'); - sqliteAdapter.query({select: 'foo', where: 'bar = ?'}, 'qux'); + sqliteClient.query({select: 'foo', where: 'bar = ?'}, 'baz'); + sqliteClient.query({select: 'foo', where: 'bar = ?'}, 'qux'); assert.calledTwice(getStub); }); }); describe('delete', () => { - let runStub, prepareStub, sqliteAdapter; + let runStub, prepareStub, sqliteClient; beforeEach(async () => { runStub = sandbox.stub(); prepareStub = sandbox.stub(Database.prototype, 'prepare').returns({run: runStub}); - sqliteAdapter = proxyquire('lib/sqlite-adapter', { + sqliteClient = await proxyquire('lib/sqlite-client', { './db-utils/common': {createTablesQuery: () => []} - }).SqliteAdapter.create({htmlReporter, reportPath: 'test'}); - - await sqliteAdapter.init(); + }).SqliteClient.create({htmlReporter, reportPath: 'test'}); }); describe('should create valid sentence string', () => { it('if called with no query params', () => { - sqliteAdapter.delete(); + sqliteClient.delete(); assert.calledOnceWith(prepareStub, 'DELETE FROM suites'); }); it('if called with "select", "where", "order" and "orderDescending"', () => { - sqliteAdapter.delete({where: 'bar', orderBy: 'baz', orderDescending: true, limit: 42}); + sqliteClient.delete({where: 'bar', orderBy: 'baz', orderDescending: true, limit: 42}); assert.calledOnceWith(prepareStub, 'DELETE FROM suites WHERE bar ORDER BY baz DESC LIMIT 42'); }); }); it('should apply delete arguments', () => { - sqliteAdapter.delete({}, 'foo', 'bar'); + sqliteClient.delete({}, 'foo', 'bar'); assert.calledOnceWith(runStub, 'foo', 'bar'); }); diff --git a/test/unit/lib/test-adapter/hermione.ts b/test/unit/lib/test-adapter/hermione.ts index 891d145b0..fad52c465 100644 --- a/test/unit/lib/test-adapter/hermione.ts +++ b/test/unit/lib/test-adapter/hermione.ts @@ -4,12 +4,13 @@ import proxyquire from 'proxyquire'; import sinon from 'sinon'; import tmpOriginal from 'tmp'; -import {SKIPPED, TestStatus} from 'lib/constants/test-statuses'; +import {TestStatus} from 'lib/constants/test-statuses'; import {ERROR_DETAILS_PATH} from 'lib/constants/paths'; import {HermioneTestAdapter, HermioneTestAdapterOptions, ReporterTestResult} from 'lib/test-adapter'; -import {ErrorDetails, HermioneTestResult} from 'lib/types'; +import {HermioneTestResult} from 'lib/types'; import {ImagesInfoFormatter} from 'lib/image-handler'; import * as originalUtils from 'lib/server-utils'; +import * as originalTestAdapterUtils from 'lib/test-adapter/utils'; describe('HermioneTestAdapter', () => { const sandbox = sinon.sandbox.create(); @@ -21,6 +22,7 @@ describe('HermioneTestAdapter', () => { let fs: sinon.SinonStubbedInstance; let tmp: typeof tmpOriginal; let hermioneCache: typeof import('lib/test-adapter/cache/hermione'); + let testAdapterUtils: sinon.SinonStubbedInstance; const mkImagesInfoFormatter = (): sinon.SinonStubbedInstance => { return {} as sinon.SinonStubbedInstance; @@ -30,7 +32,7 @@ describe('HermioneTestAdapter', () => { testResult: HermioneTestResult, {status = TestStatus.SUCCESS, imagesInfoFormatter = mkImagesInfoFormatter()}: {status?: TestStatus, imagesInfoFormatter?: ImagesInfoFormatter} = {} ): HermioneTestAdapter => { - return new HermioneTestAdapter(testResult, {status, imagesInfoFormatter}) as HermioneTestAdapter; + return new HermioneTestAdapter(testResult, {status, imagesInfoFormatter, attempt: 0}) as HermioneTestAdapter; }; const mkTestResult_ = (result: Partial): HermioneTestResult => _.defaults(result, { @@ -50,13 +52,19 @@ describe('HermioneTestAdapter', () => { }); utils = _.clone(originalUtils); + const originalTestAdapterUtils = proxyquire('lib/test-adapter/utils', { + '../../server-utils': utils + }); + testAdapterUtils = _.clone(originalTestAdapterUtils); + HermioneTestAdapter = proxyquire('lib/test-adapter/hermione', { tmp, 'fs-extra': fs, '../plugin-utils': {getSuitePath}, '../history-utils': {getCommandsHistory}, '../server-utils': utils, - './cache/hermione': hermioneCache + './cache/hermione': hermioneCache, + './utils': testAdapterUtils }).HermioneTestAdapter; sandbox.stub(utils, 'getCurrentPath').returns(''); sandbox.stub(utils, 'getDiffPath').returns(''); @@ -69,25 +77,6 @@ describe('HermioneTestAdapter', () => { afterEach(() => sandbox.restore()); - it('should return suite attempt', () => { - const firstTestResult = mkTestResult_({fullTitle: () => 'some-title'}); - const secondTestResult = mkTestResult_({fullTitle: () => 'other-title'}); - - mkHermioneTestResultAdapter(firstTestResult); - - assert.equal(mkHermioneTestResultAdapter(firstTestResult).attempt, 1); - assert.equal(mkHermioneTestResultAdapter(secondTestResult).attempt, 0); - }); - - it('should not increment attempt for skipped tests', () => { - const testResult = mkTestResult_({fullTitle: () => 'some-title'}); - - mkHermioneTestResultAdapter(testResult, {status: SKIPPED}); - const result = mkHermioneTestResultAdapter(testResult, {status: SKIPPED}); - - assert.equal(result.attempt, 0); - }); - it('should return full test error', () => { getCommandsHistory.withArgs([{name: 'foo'}], ['foo']).returns(['some-history']); const testResult = mkTestResult_({ @@ -178,6 +167,7 @@ describe('HermioneTestAdapter', () => { }); it('should be memoized', () => { + const extractErrorDetails = sandbox.stub(testAdapterUtils, 'extractErrorDetails').returns({}); const testResult = mkTestResult_({ err: { details: {title: 'some-title', data: {foo: 'bar'}} @@ -188,7 +178,7 @@ describe('HermioneTestAdapter', () => { const firstErrDetails = hermioneTestAdapter.errorDetails; const secondErrDetails = hermioneTestAdapter.errorDetails; - assert.calledOnce(getDetailsFileName); + assert.calledOnce(extractErrorDetails); assert.deepEqual(firstErrDetails, secondErrDetails); }); @@ -217,52 +207,6 @@ describe('HermioneTestAdapter', () => { }); }); - describe('saveErrorDetails', () => { - beforeEach(() => { - sandbox.stub(utils, 'makeDirFor').resolves(); - sandbox.stub(utils, 'getDetailsFileName').returns('md5-bro-n-time'); - }); - - it('should do nothing if no error details are available', async () => { - const hermioneTestAdapter = mkHermioneTestResultAdapter(mkTestResult_({err: {} as any})); - - await hermioneTestAdapter.saveErrorDetails(''); - - assert.notCalled(fs.writeFile); - }); - - it('should save error details to correct path', async () => { - const testResult = mkTestResult_({err: { - details: {title: 'some-title', data: {}} - } as any}); - const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult); - const {filePath} = hermioneTestAdapter.errorDetails as ErrorDetails; - - await hermioneTestAdapter.saveErrorDetails('report-path'); - - assert.calledWithMatch(fs.writeFile, `report-path/${filePath}`, sinon.match.any); - }); - - it('should create directory for error details', async () => { - const testResult = mkTestResult_({err: {details: {data: {}}} as any}); - const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult); - - await hermioneTestAdapter.saveErrorDetails('report-path'); - - assert.calledOnceWith(utils.makeDirFor, sinon.match(`report-path/${ERROR_DETAILS_PATH}`)); - }); - - it('should save error details', async () => { - const data = {foo: 'bar'}; - const testResult = mkTestResult_({err: {details: {data}} as any}); - const hermioneTestAdapter = mkHermioneTestResultAdapter(testResult); - - await hermioneTestAdapter.saveErrorDetails(''); - - assert.calledWith(fs.writeFile, sinon.match.any, JSON.stringify(data, null, 2)); - }); - }); - it('should return image dir', () => { const testResult = mkTestResult_({id: 'some-id'}); diff --git a/test/unit/lib/tests-tree-builder/base.js b/test/unit/lib/tests-tree-builder/base.js index de57131ea..fb0ce8e36 100644 --- a/test/unit/lib/tests-tree-builder/base.js +++ b/test/unit/lib/tests-tree-builder/base.js @@ -8,7 +8,7 @@ const {ToolName} = require('lib/constants'); describe('ResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); - let ResultsTreeBuilder, builder, determineStatus; + let ResultsTreeBuilder, builder, determineFinalStatus; const mkTestResult_ = (result) => { return _.defaults(result, {imagesInfo: [], metaInfo: {}}); @@ -23,9 +23,9 @@ describe('ResultsTreeBuilder', () => { }; beforeEach(() => { - determineStatus = sandbox.stub().returns(SUCCESS); + determineFinalStatus = sandbox.stub().returns(SUCCESS); ResultsTreeBuilder = proxyquire('lib/tests-tree-builder/base', { - '../common-utils': {determineStatus} + '../common-utils': {determineFinalStatus} }).BaseTestsTreeBuilder; builder = ResultsTreeBuilder.create({toolName: ToolName.Hermione}); @@ -247,16 +247,16 @@ describe('ResultsTreeBuilder', () => { }); describe('determine statuses for suites', () => { - it('should call "determineStatus" with test result status', () => { + it('should call "determineFinalStatus" with test result status', () => { builder.addTestResult( mkTestResult_({status: SUCCESS}), mkFormattedResult_({testPath: ['s1']}) ); - assert.calledOnceWith(determineStatus, [SUCCESS]); + assert.calledOnceWith(determineFinalStatus, [SUCCESS]); }); - it('should call "determineStatus" with test result status from last attempt', () => { + it('should call "determineFinalStatus" with test result status from last attempt', () => { builder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], attempt: 0}) @@ -266,10 +266,10 @@ describe('ResultsTreeBuilder', () => { mkFormattedResult_({testPath: ['s1'], attempt: 1}) ); - assert.calledWith(determineStatus.lastCall, [SUCCESS]); + assert.calledWith(determineFinalStatus.lastCall, [SUCCESS]); }); - it('should call "determineStatus" with all test statuses from each browser', () => { + it('should call "determineFinalStatus" with all test statuses from each browser', () => { builder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1'], browserId: 'b1'}) @@ -279,12 +279,12 @@ describe('ResultsTreeBuilder', () => { mkFormattedResult_({testPath: ['s1'], browserId: 'b2'}) ); - assert.calledWith(determineStatus.secondCall, [FAIL, SUCCESS]); + assert.calledWith(determineFinalStatus.secondCall, [FAIL, SUCCESS]); }); - it('should call "determineStatus" with statuses from child suites', () => { - determineStatus.withArgs([FAIL]).returns('s1 s2 status'); - determineStatus.withArgs([ERROR]).returns('s1 s3 status'); + it('should call "determineFinalStatus" with statuses from child suites', () => { + determineFinalStatus.withArgs([FAIL]).returns('s1 s2 status'); + determineFinalStatus.withArgs([ERROR]).returns('s1 s3 status'); builder.addTestResult( mkTestResult_({status: FAIL}), mkFormattedResult_({testPath: ['s1', 's2']}) @@ -294,7 +294,7 @@ describe('ResultsTreeBuilder', () => { mkFormattedResult_({testPath: ['s1', 's3']}) ); - assert.calledWith(determineStatus.getCall(3), ['s1 s2 status', 's1 s3 status']); + assert.calledWith(determineFinalStatus.getCall(3), ['s1 s2 status', 's1 s3 status']); }); }); }); diff --git a/test/unit/lib/tests-tree-builder/gui.js b/test/unit/lib/tests-tree-builder/gui.js index dff7b4061..e20961a1d 100644 --- a/test/unit/lib/tests-tree-builder/gui.js +++ b/test/unit/lib/tests-tree-builder/gui.js @@ -26,34 +26,6 @@ describe('GuiResultsTreeBuilder', () => { builder = mkGuiTreeBuilder(); }); - describe('"getLastResult" method', () => { - it('should return last result from tree', () => { - const formattedRes1 = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 0}); - const formattedRes2 = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 1}); - builder.addTestResult(mkTestResult_(), formattedRes1); - builder.addTestResult(mkTestResult_(), formattedRes2); - - const lastResult = builder.getLastResult({testPath: ['s'], browserId: 'b'}); - - assert.deepEqual(lastResult, builder.tree.results.byId['s b 1']); - }); - }); - - describe('"getLastActualResult" method', () => { - it('should return previous result from tree', () => { - const formattedRes1 = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 0}); - const formattedRes2 = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 1}); - const formattedRes3 = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 2}); - builder.addTestResult(mkTestResult_(), formattedRes1); - builder.addTestResult(mkTestResult_(), formattedRes2); - builder.addTestResult(mkTestResult_(), formattedRes3); - - const lastResult = builder.getLastActualResult({testPath: ['s'], browserId: 'b', attempt: 1}); - - assert.deepEqual(lastResult, builder.tree.results.byId['s b 0']); - }); - }); - describe('"getImagesInfo" method', () => { it('should return images from tree for passed test result id', () => { const formattedRes = mkFormattedResult_({testPath: ['s'], browserId: 'b', attempt: 0}); diff --git a/test/unit/lib/tests-tree-builder/static.js b/test/unit/lib/tests-tree-builder/static.js index 7a2521c21..6270e7085 100644 --- a/test/unit/lib/tests-tree-builder/static.js +++ b/test/unit/lib/tests-tree-builder/static.js @@ -86,13 +86,13 @@ describe('StaticResultsTreeBuilder', () => { assert.calledWith( StaticTestsTreeBuilder.prototype.addTestResult.firstCall, - formatToTestResult(dataFromDb1, {attempt: 0}), - {browserId: 'yabro', testPath: ['s1'], attempt: 0} + sinon.match(formatToTestResult(dataFromDb1, {attempt: 0})), + sinon.match({browserId: 'yabro', testPath: ['s1'], attempt: 0}) ); assert.calledWith( StaticTestsTreeBuilder.prototype.addTestResult.secondCall, - formatToTestResult(dataFromDb2, {attempt: 0}), - {browserId: 'yabro', testPath: ['s2'], attempt: 0} + sinon.match(formatToTestResult(dataFromDb2, {attempt: 0})), + sinon.match({browserId: 'yabro', testPath: ['s2'], attempt: 0}) ); }); @@ -105,13 +105,13 @@ describe('StaticResultsTreeBuilder', () => { assert.calledWith( StaticTestsTreeBuilder.prototype.addTestResult.firstCall, - formatToTestResult(dataFromDb1, {attempt: 0}), - {browserId: 'yabro', testPath: ['s1'], attempt: 0} + sinon.match(formatToTestResult(dataFromDb1, {attempt: 0})), + sinon.match({browserId: 'yabro', testPath: ['s1'], attempt: 0}) ); assert.calledWith( StaticTestsTreeBuilder.prototype.addTestResult.secondCall, - formatToTestResult(dataFromDb2, {attempt: 1}), - {browserId: 'yabro', testPath: ['s1'], attempt: 1} + sinon.match(formatToTestResult(dataFromDb2, {attempt: 1})), + sinon.match({browserId: 'yabro', testPath: ['s1'], attempt: 1}) ); }); }); diff --git a/test/unit/utils.js b/test/unit/utils.js index 08f1e8945..1131994db 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const EventEmitter2 = require('eventemitter2'); +const {HtmlReporter} = require('lib/plugin-api'); function stubConfig(config = {}) { const browsers = config.browsers || {}; @@ -30,7 +31,7 @@ function stubTool(config = stubConfig(), events = {}, errors = {}, htmlReporter) tool.run = sinon.stub().resolves(false); tool.readTests = sinon.stub().resolves(stubTestCollection()); - tool.htmlReporter = htmlReporter || sinon.stub(); + tool.htmlReporter = htmlReporter || sinon.createStubInstance(HtmlReporter); _.defaultsDeep(tool.htmlReporter, { emitAsync: sinon.stub(), events: {REPORT_SAVED: 'reportSaved'} @@ -129,6 +130,24 @@ function mkFormattedTest(result) { }); } +class NoRefImageError extends Error { + name = 'NoRefImageError'; +} + +class ImageDiffError extends Error { + name = 'ImageDiffError'; + constructor() { + super(); + this.stateName = ''; + this.currImg = { + path: '' + }; + this.refImg = { + path: '' + }; + } +} + module.exports = { stubConfig, stubTestCollection, @@ -140,5 +159,7 @@ module.exports = { mkImagesInfo, mkSuiteTree, mkStorage, - mkFormattedTest + mkFormattedTest, + NoRefImageError, + ImageDiffError }; diff --git a/tsconfig.json b/tsconfig.json index 68083ee03..aded57834 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.common.json", - "include": ["lib", "hermione.js", "gemini.js", "playwright.ts"], + "include": ["lib", "hermione.ts", "gemini.js", "playwright.ts"], "exclude": ["lib/static",], "compilerOptions": { "outDir": "build", diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 375ce25db..52affde5f 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -1,13 +1,12 @@ { "extends": "./tsconfig.common.json", - "include": ["lib", "test", "hermione.js", "gemini.js", "playwright.ts"], + "include": ["lib", "test", "hermione.ts", "gemini.js", "playwright.ts"], "exclude": ["test/func"], "compilerOptions": { - "baseUrl": ".", "jsx": "react", "noEmit": true, "paths": { - "lib/*": ["lib/*"] + "lib/*": ["./lib/*"] } }, }