diff --git a/docs/en/html-reporter-events.md b/docs/en/html-reporter-events.md index b6cc98e07..eeb6029a7 100644 --- a/docs/en/html-reporter-events.md +++ b/docs/en/html-reporter-events.md @@ -29,7 +29,7 @@ A database instance is passed to the event handler. ### Usage example ```javascript -const parseConfig = require('./config'); +const {parseConfig} = require('./config'); module.exports = (hermione, opts) => { const pluginConfig = parseConfig(opts); diff --git a/docs/ru/html-reporter-events.md b/docs/ru/html-reporter-events.md index 299161af1..3b5ec2065 100644 --- a/docs/ru/html-reporter-events.md +++ b/docs/ru/html-reporter-events.md @@ -29,7 +29,7 @@ hermione.htmlReporter.on(hermione.htmlReporter.events.DATABASE_CREATED, (db) => ### Пример использования ```javascript -const parseConfig = require('./config'); +const {parseConfig} = require('./config'); module.exports = (hermione, opts) => { const pluginConfig = parseConfig(opts); diff --git a/hermione.js b/hermione.js index 7c2032b21..0d9b70d6a 100644 --- a/hermione.js +++ b/hermione.js @@ -2,8 +2,8 @@ const os = require('os'); const PQueue = require('p-queue'); -const PluginAdapter = require('./lib/plugin-adapter'); -const createWorkers = require('./lib/workers/create-workers'); +const {PluginAdapter} = require('./lib/plugin-adapter'); +const {createWorkers} = require('./lib/workers/create-workers'); let workers; diff --git a/lib/cli-commands/gui.js b/lib/cli-commands/gui.js index f10f74091..155489e5b 100644 --- a/lib/cli-commands/gui.js +++ b/lib/cli-commands/gui.js @@ -1,8 +1,10 @@ 'use strict'; +const {cliCommands} = require('.'); const runGui = require('../gui').default; const Api = require('../gui/api'); -const {GUI: commandName} = require('./'); + +const {GUI: commandName} = cliCommands; module.exports = (program, pluginConfig, hermione) => { // must be executed here because it adds `gui` field in `gemini` and `hermione tool`, diff --git a/lib/cli-commands/index.js b/lib/cli-commands/index.ts similarity index 71% rename from lib/cli-commands/index.js rename to lib/cli-commands/index.ts index 5ccabff27..f6ec574a7 100644 --- a/lib/cli-commands/index.js +++ b/lib/cli-commands/index.ts @@ -1,7 +1,5 @@ -'use strict'; - -module.exports = { +export const cliCommands = { GUI: 'gui', MERGE_REPORTS: 'merge-reports', REMOVE_UNUSED_SCREENS: 'remove-unused-screens' -}; +} as const; diff --git a/lib/cli-commands/merge-reports.js b/lib/cli-commands/merge-reports.js index 3701478c1..d7349a2b4 100644 --- a/lib/cli-commands/merge-reports.js +++ b/lib/cli-commands/merge-reports.js @@ -1,9 +1,11 @@ 'use strict'; -const {MERGE_REPORTS: commandName} = require('./'); +const {cliCommands} = require('.'); const mergeReports = require('../merge-reports'); const {logError} = require('../server-utils'); +const {MERGE_REPORTS: commandName} = cliCommands; + module.exports = (program, pluginConfig, hermione) => { program .command(`${commandName} [paths...]`) diff --git a/lib/cli-commands/remove-unused-screens/index.js b/lib/cli-commands/remove-unused-screens/index.js index b564699b3..9f5186b4e 100644 --- a/lib/cli-commands/remove-unused-screens/index.js +++ b/lib/cli-commands/remove-unused-screens/index.js @@ -8,11 +8,13 @@ const chalk = require('chalk'); const filesize = require('filesize'); const Promise = require('bluebird'); -const {REMOVE_UNUSED_SCREENS: commandName} = require('..'); +const {cliCommands} = require('..'); const {getTestsFromFs, findScreens, askQuestion, identifyOutdatedScreens, identifyUnusedScreens, removeScreens} = require('./utils'); const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../../constants/database'); const {logger} = require('../../common-utils'); +const {REMOVE_UNUSED_SCREENS: commandName} = cliCommands; + // TODO: remove hack after add ability to add controllers from plugin in silent mode function proxyHermione() { const proxyHandler = { diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 5e114172e..37bce0fa3 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -2,14 +2,14 @@ import crypto from 'crypto'; import {pick} from 'lodash'; import url from 'url'; import axios, {AxiosRequestConfig} from 'axios'; -import {SUCCESS, FAIL, ERROR, SKIPPED, UPDATED, IDLE, RUNNING, QUEUED} from './constants/test-statuses'; +import {SUCCESS, FAIL, ERROR, SKIPPED, UPDATED, IDLE, RUNNING, QUEUED, TestStatus} from './constants'; import {UNCHECKED, INDETERMINATE, CHECKED} from './constants/checked-statuses'; export const getShortMD5 = (str: string): string => { return crypto.createHash('md5').update(str, 'ascii').digest('hex').substr(0, 7); }; -const statusPriority: string[] = [ +const statusPriority: TestStatus[] = [ // non-final RUNNING, QUEUED, @@ -19,15 +19,15 @@ const statusPriority: string[] = [ export const logger = pick(console, ['log', 'warn', 'error']); -export const isSuccessStatus = (status: string): boolean => status === SUCCESS; -export const isFailStatus = (status: string): boolean => status === FAIL; -export const isIdleStatus = (status: string): boolean => status === IDLE; -export const isRunningStatus = (status: string): boolean => status === RUNNING; -export const isErroredStatus = (status: string): boolean => status === ERROR; -export const isSkippedStatus = (status: string): boolean => status === SKIPPED; -export const isUpdatedStatus = (status: string): boolean => status === UPDATED; +export const isSuccessStatus = (status: TestStatus): boolean => status === SUCCESS; +export const isFailStatus = (status: TestStatus): boolean => status === FAIL; +export const isIdleStatus = (status: TestStatus): boolean => status === IDLE; +export const isRunningStatus = (status: TestStatus): boolean => status === RUNNING; +export const isErroredStatus = (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: string[]): string | null => { +export const determineStatus = (statuses: TestStatus[]): TestStatus | null => { if (!statuses.length) { return SUCCESS; } diff --git a/lib/config/custom-gui-asserts.js b/lib/config/custom-gui-asserts.ts similarity index 55% rename from lib/config/custom-gui-asserts.js rename to lib/config/custom-gui-asserts.ts index 0f41554de..884ed9346 100644 --- a/lib/config/custom-gui-asserts.js +++ b/lib/config/custom-gui-asserts.ts @@ -1,10 +1,9 @@ -'use strict'; +import {isUndefined, isArray, isEmpty, isFunction, isPlainObject, isString} from 'lodash'; +import CustomGuiControlTypes from '../gui/constants/custom-gui-control-types'; -const {isUndefined, isArray, isEmpty, isFunction, isPlainObject, isString} = require('lodash'); +const SUPPORTED_CONTROL_TYPES: string[] = Object.values(CustomGuiControlTypes); -const SUPPORTED_CONTROL_TYPES = Object.values(require('../gui/constants/custom-gui-control-types')); - -const assertSectionGroupType = (context, type) => { +const assertSectionGroupType = (context: string, type: unknown): void => { if (isUndefined(type)) { throw new Error(`${context} must contain field "type"`); } @@ -16,7 +15,7 @@ const assertSectionGroupType = (context, type) => { } }; -const assertSectionGroupControls = (context, controls) => { +const assertSectionGroupControls = (context: string, controls: unknown): void => { if (isUndefined(controls)) { throw new Error(`${context} must contain field "controls"`); } @@ -26,14 +25,14 @@ const assertSectionGroupControls = (context, controls) => { if (isEmpty(controls)) { throw new Error(`${context} must contain non-empty array in the field "controls"`); } - controls.forEach((control) => { + controls.forEach((control: unknown) => { if (!isPlainObject(control)) { throw new Error(`${context} must contain objects in the array "controls"`); } }); }; -const assertSectionGroupAction = (context, action) => { +const assertSectionGroupAction = (context: string, action: unknown): void => { if (isUndefined(action)) { throw new Error(`${context} must contain field "action"`); } @@ -42,34 +41,35 @@ const assertSectionGroupAction = (context, action) => { } }; -const assertSectionGroup = (sectionName, group, groupIndex) => { +const assertSectionGroup = (sectionName: string, group: unknown, groupIndex: number): void => { const context = `customGui["${sectionName}"][${groupIndex}]`; if (!isPlainObject(group)) { throw new Error(`${context} must be plain object, but got ${typeof group}`); } - assertSectionGroupType(context, group.type); - assertSectionGroupControls(context, group.controls); - assertSectionGroupAction(context, group.action); + const groupObj = group as Record; + + assertSectionGroupType(context, groupObj.type); + assertSectionGroupControls(context, groupObj.controls); + assertSectionGroupAction(context, groupObj.action); }; -const assertSection = (section, sectionName) => { +const assertSection = (section: unknown, sectionName: string): void => { if (!isArray(section)) { throw new Error(`customGui["${sectionName}"] must be an array, but got ${typeof section}`); } - section.forEach((group, groupIndex) => assertSectionGroup(sectionName, group, groupIndex)); + section.forEach((group: unknown, groupIndex: number) => assertSectionGroup(sectionName, group, groupIndex)); }; -const assertCustomGui = (customGui) => { +export const assertCustomGui = (customGui: unknown): void => { if (!isPlainObject(customGui)) { throw new Error(`"customGui" option must be plain object, but got ${typeof customGui}`); } - for (const sectionName in customGui) { - assertSection(customGui[sectionName], sectionName); - } -}; -module.exports = { - assertCustomGui + const customGuiObj = customGui as Record; + + for (const sectionName in customGuiObj) { + assertSection(customGuiObj[sectionName], sectionName); + } }; diff --git a/lib/config/index.js b/lib/config/index.ts similarity index 64% rename from lib/config/index.js rename to lib/config/index.ts index 2dc859ade..6e3ae9bb7 100644 --- a/lib/config/index.js +++ b/lib/config/index.ts @@ -1,47 +1,43 @@ -'use strict'; +import _ from 'lodash'; +import {root, section, option} from 'gemini-configparser'; +import chalk from 'chalk'; -const _ = require('lodash'); -const configParser = require('gemini-configparser'); -const chalk = require('chalk'); - -const {logger} = require('../common-utils'); -const {config: configDefaults} = require('../constants/defaults'); -const {DiffModes} = require('../constants/diff-modes'); -const saveFormats = require('../constants/save-formats'); -const {assertCustomGui} = require('./custom-gui-asserts'); - -const root = configParser.root; -const section = configParser.section; -const option = configParser.option; +import {logger} from '../common-utils'; +import {configDefaults, DiffModeId, DiffModes, SaveFormat, ViewMode} from '../constants'; +import {assertCustomGui} from './custom-gui-asserts'; +import {ErrorPattern, PluginDescription, ReporterConfig, ReporterOptions} from '../types'; const ENV_PREFIX = 'html_reporter_'; const CLI_PREFIX = '--html-reporter-'; const ALLOWED_PLUGIN_DESCRIPTION_FIELDS = new Set(['name', 'component', 'point', 'position', 'config']); -const assertType = (name, validationFn, type) => { - return (v) => { +type TypePredicateFn = (value: unknown) => value is T; +type AssertionFn = (value: unknown) => asserts value is T; + +const assertType = (name: string, validationFn: (value: unknown) => value is T, type: string): AssertionFn => { + return (v: unknown): asserts v is T => { if (!validationFn(v)) { throw new Error(`"${name}" option must be ${type}, but got ${typeof v}`); } }; }; -const assertString = (name) => assertType(name, _.isString, 'string'); -const assertBoolean = (name) => assertType(name, _.isBoolean, 'boolean'); -const assertNumber = (name) => assertType(name, _.isNumber, 'number'); +const assertString = (name: string): AssertionFn => assertType(name, _.isString, 'string'); +const assertBoolean = (name: string): AssertionFn => assertType(name, _.isBoolean, 'boolean'); +const assertNumber = (name: string): AssertionFn => assertType(name, _.isNumber, 'number'); -const assertSaveFormat = saveFormat => { - const formats = Object.values(saveFormats); +const assertSaveFormat = (saveFormat: unknown): asserts saveFormat is SaveFormat => { + const formats = Object.values(SaveFormat); if (!_.isString(saveFormat)) { throw new Error(`"saveFormat" option must be string, but got ${typeof saveFormat}`); } - if (!formats.includes(saveFormat)) { + if (!formats.includes(saveFormat as SaveFormat)) { throw new Error(`"saveFormat" must be "${formats.join('", "')}", but got "${saveFormat}"`); } }; -const assertErrorPatterns = (errorPatterns) => { +const assertErrorPatterns = (errorPatterns: unknown): asserts errorPatterns is (string | ErrorPattern)[] => { if (!_.isArray(errorPatterns)) { throw new Error(`"errorPatterns" option must be array, but got ${typeof errorPatterns}`); } @@ -59,51 +55,54 @@ const assertErrorPatterns = (errorPatterns) => { } }; -const assertMetaInfoBaseUrls = (metaInfoBaseUrls) => { +const assertMetaInfoBaseUrls = (metaInfoBaseUrls: unknown): asserts metaInfoBaseUrls is Record => { if (!_.isObject(metaInfoBaseUrls)) { throw new Error(`"metaInfoBaseUrls" option must be object, but got ${typeof metaInfoBaseUrls}`); } - for (const key in metaInfoBaseUrls) { + for (const keyStr in metaInfoBaseUrls) { + const key = keyStr as keyof typeof metaInfoBaseUrls; if (!_.isString(metaInfoBaseUrls[key])) { throw new Error(`Value of "${key}" in "metaInfoBaseUrls" option must be string, but got ${typeof metaInfoBaseUrls[key]}`); } } }; -const assertArrayOf = (itemsType, name, assertFn) => { - return (value) => { +const assertArrayOf = (itemsType: string, name: string, predicateFn: TypePredicateFn) => { + return (value: unknown): asserts value is T[] => { if (!_.isArray(value)) { throw new Error(`"${name}" option must be an array, but got ${typeof value}`); } for (const item of value) { - if (!assertFn(item)) { + if (!predicateFn(item)) { throw new Error(`"${name}" option must be an array of ${itemsType} but got ${typeof item} for one of items`); } } }; }; -const assertPluginDescription = (description) => { - if (!_.isPlainObject(description)) { +const assertPluginDescription = (description: unknown): description is PluginDescription => { + const maybeDescription = description as PluginDescription; + + if (!_.isPlainObject(maybeDescription)) { throw new Error(`plugin description expected to be an object but got ${typeof description}`); } - for (const field of ['name', 'component']) { - if (!description[field] || !_.isString(description[field])) { - throw new Error(`"plugins.${field}" option must be non-empty string but got ${typeof description[field]}`); + for (const field of ['name', 'component'] as const) { + if (!maybeDescription[field] || !_.isString(maybeDescription[field])) { + throw new Error(`"plugins.${field}" option must be non-empty string but got ${typeof maybeDescription[field]}`); } } - if (description.point && !_.isString(description.point)) { - throw new Error(`"plugins.point" option must be string but got ${typeof description.point}`); + if (maybeDescription.point && !_.isString(maybeDescription.point)) { + throw new Error(`"plugins.point" option must be string but got ${typeof maybeDescription.point}`); } - if (description.position && !['after', 'before', 'wrap'].includes(description.position)) { - throw new Error(`"plugins.position" option got an unexpected value "${description.position}"`); + if (maybeDescription.position && !['after', 'before', 'wrap'].includes(maybeDescription.position)) { + throw new Error(`"plugins.position" option got an unexpected value "${maybeDescription.position}"`); } - if (description.config && !_.isPlainObject(description.config)) { - throw new Error(`plugin configuration expected to be an object but got ${typeof description.config}`); + if (maybeDescription.config && !_.isPlainObject(maybeDescription.config)) { + throw new Error(`plugin configuration expected to be an object but got ${typeof maybeDescription.config}`); } _.forOwn(description, (value, key) => { @@ -115,19 +114,19 @@ const assertPluginDescription = (description) => { return true; }; -const assertDiffMode = (diffMode) => { +const assertDiffMode = (diffMode: unknown): asserts diffMode is DiffModeId => { if (!_.isString(diffMode)) { throw new Error(`"diffMode" option must be a string, but got ${typeof diffMode}`); } const availableValues = Object.values(DiffModes).map(v => v.id); - if (!availableValues.includes(diffMode)) { + if (!availableValues.includes(diffMode as DiffModeId)) { throw new Error(`"diffMode" must be one of "${availableValues.join('", "')}", but got "${diffMode}"`); } }; -const mapErrorPatterns = (errorPatterns) => { +const mapErrorPatterns = (errorPatterns: (string | ErrorPattern)[]): ErrorPattern[] => { return errorPatterns.map(patternInfo => { return _.isString(patternInfo) ? {name: patternInfo, pattern: patternInfo} @@ -135,12 +134,12 @@ const mapErrorPatterns = (errorPatterns) => { }); }; -const deprecationWarning = (name) => { +const deprecationWarning = (name: string): void => { logger.warn(chalk.red(`Warning: field "${name}" is deprecated and will be removed soon from html-reporter config.`)); }; -const getParser = () => { - return root(section({ +const getParser = (): ReturnType> => { + return root(section({ enabled: option({ defaultValue: true, parseEnv: JSON.parse, @@ -152,7 +151,7 @@ const getParser = () => { validate: assertString('path') }), saveFormat: option({ - defaultValue: saveFormats.SQLITE, + defaultValue: SaveFormat.SQLITE, validate: assertSaveFormat }), saveErrorDetails: option({ @@ -165,11 +164,11 @@ const getParser = () => { defaultValue: configDefaults.commandsWithShortHistory, validate: assertArrayOf('strings', 'commandsWithShortHistory', _.isString) }), - defaultView: option({ + defaultView: option({ defaultValue: configDefaults.defaultView, validate: assertString('defaultView') }), - diffMode: option({ + diffMode: option({ defaultValue: configDefaults.diffMode, validate: assertDiffMode }), @@ -181,7 +180,7 @@ const getParser = () => { defaultValue: configDefaults.lazyLoadOffset, validate: (value) => _.isNull(value) || deprecationWarning('lazyLoadOffset') }), - errorPatterns: option({ + errorPatterns: option<(string | ErrorPattern)[], ErrorPattern[]>({ defaultValue: configDefaults.errorPatterns, parseEnv: JSON.parse, validate: assertErrorPatterns, @@ -197,7 +196,7 @@ const getParser = () => { defaultValue: configDefaults.customGui, validate: assertCustomGui }), - customScripts: option({ + customScripts: option<(() => void)[]>({ defaultValue: configDefaults.customScripts, validate: assertArrayOf('functions', 'customScripts', _.isFunction) }), @@ -224,9 +223,10 @@ const getParser = () => { }), {envPrefix: ENV_PREFIX, cliPrefix: CLI_PREFIX}); }; -module.exports = (options) => { +export const parseConfig = (options: Partial): ReporterConfig => { const env = process.env; const argv = process.argv; - return getParser()({options, env, argv}); + // TODO: add support for different types of input and output in gemini-configparser + return getParser()({options: options as ReporterConfig, env, argv}); }; diff --git a/lib/constants/browser.js b/lib/constants/browser.js deleted file mode 100644 index 221371b0b..000000000 --- a/lib/constants/browser.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.versions = { - UNKNOWN: 'unknown' -}; diff --git a/lib/constants/browser.ts b/lib/constants/browser.ts new file mode 100644 index 000000000..52f0e7ac3 --- /dev/null +++ b/lib/constants/browser.ts @@ -0,0 +1,3 @@ +export enum BrowserVersions { + UNKNOWN = 'unknown' +} diff --git a/lib/constants/defaults.js b/lib/constants/defaults.js deleted file mode 100644 index 6bf6ec1ae..000000000 --- a/lib/constants/defaults.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const {DiffModes} = require('./diff-modes'); -const {ViewMode} = require('./view-modes'); - -module.exports = { - CIRCLE_RADIUS: 150, - config: { - saveErrorDetails: false, - commandsWithShortHistory: [], - defaultView: ViewMode.ALL, - diffMode: DiffModes.THREE_UP.id, - baseHost: '', - lazyLoadOffset: null, - errorPatterns: [], - metaInfoBaseUrls: {}, - customGui: {}, - customScripts: [], - yandexMetrika: { - counterNumber: null - }, - pluginsEnabled: false, - plugins: [] - } -}; diff --git a/lib/constants/defaults.ts b/lib/constants/defaults.ts new file mode 100644 index 000000000..9bb2d57da --- /dev/null +++ b/lib/constants/defaults.ts @@ -0,0 +1,27 @@ +import {DiffModes} from './diff-modes'; +import {ViewMode} from './view-modes'; +import {ReporterConfig} from '../types'; +import {SaveFormat} from './save-formats'; + +export const CIRCLE_RADIUS = 150; + +export const configDefaults: ReporterConfig = { + baseHost: '', + commandsWithShortHistory: [], + customGui: {}, + customScripts: [], + defaultView: ViewMode.ALL, + diffMode: DiffModes.THREE_UP.id, + enabled: false, + errorPatterns: [], + lazyLoadOffset: null, + metaInfoBaseUrls: {}, + path: '', + plugins: [], + pluginsEnabled: false, + saveErrorDetails: false, + saveFormat: SaveFormat.SQLITE, + yandexMetrika: { + counterNumber: null + } +}; diff --git a/lib/constants/diff-modes.ts b/lib/constants/diff-modes.ts index 594d77006..af9176002 100644 --- a/lib/constants/diff-modes.ts +++ b/lib/constants/diff-modes.ts @@ -41,3 +41,5 @@ export const DiffModes = { export type DiffModes = typeof DiffModes; export type DiffMode = ValueOf; + +export type DiffModeId = DiffModes[keyof DiffModes]['id']; diff --git a/lib/constants/index.ts b/lib/constants/index.ts index 3dfdc50ac..be8296aa2 100644 --- a/lib/constants/index.ts +++ b/lib/constants/index.ts @@ -1,5 +1,9 @@ +export * from './browser'; export * from './database'; +export * from './defaults'; export * from './diff-modes'; export * from './paths'; +export * from './plugin-events'; +export * from './save-formats'; export * from './test-statuses'; export * from './view-modes'; diff --git a/lib/constants/plugin-events.js b/lib/constants/plugin-events.js deleted file mode 100644 index 402d90172..000000000 --- a/lib/constants/plugin-events.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const getSyncEvents = () => ({ - DATABASE_CREATED: 'databaseCreated', - TEST_SCREENSHOTS_SAVED: 'testScreenshotsSaved', - REPORT_SAVED: 'reportSaved' -}); - -const events = getSyncEvents(); -Object.defineProperty(events, 'getSync', {value: getSyncEvents, enumerable: false}); - -module.exports = events; diff --git a/lib/constants/plugin-events.ts b/lib/constants/plugin-events.ts new file mode 100644 index 000000000..077f3c216 --- /dev/null +++ b/lib/constants/plugin-events.ts @@ -0,0 +1,5 @@ +export enum PluginEvents { + DATABASE_CREATED = 'databaseCreated', + TEST_SCREENSHOTS_SAVED = 'testScreenshotsSaved', + REPORT_SAVED = 'reportSaved' +} diff --git a/lib/constants/save-formats.js b/lib/constants/save-formats.js deleted file mode 100644 index 42cfe215f..000000000 --- a/lib/constants/save-formats.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - SQLITE: 'sqlite' -}; diff --git a/lib/constants/save-formats.ts b/lib/constants/save-formats.ts new file mode 100644 index 000000000..c3614bf97 --- /dev/null +++ b/lib/constants/save-formats.ts @@ -0,0 +1,3 @@ +export enum SaveFormat { + SQLITE = 'sqlite' +} diff --git a/lib/db-utils/client.js b/lib/db-utils/client.js index 51da2ed59..d30668488 100644 --- a/lib/db-utils/client.js +++ b/lib/db-utils/client.js @@ -2,6 +2,7 @@ /* eslint-env browser */ const {isEmpty} = require('lodash'); +/** @type Record unknown> */ const commonSqliteUtils = require('./common'); const {fetchFile, normalizeUrls} = require('../common-utils'); diff --git a/lib/db-utils/common.js b/lib/db-utils/common.js deleted file mode 100644 index 3b219ba50..000000000 --- a/lib/db-utils/common.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const {logger} = require('../common-utils'); -const {DB_MAX_AVAILABLE_PAGE_SIZE, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, DB_COLUMN_INDEXES} = require('../constants/database'); - -exports.selectAllQuery = (tableName) => `SELECT * FROM ${tableName}`; -exports.selectAllSuitesQuery = () => exports.selectAllQuery(DB_SUITES_TABLE_NAME); - -exports.createTablesQuery = () => [ - createTableQuery(DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS) -]; - -exports.compareDatabaseRowsByTimestamp = (row1, row2) => { - return row1[DB_COLUMN_INDEXES.timestamp] - row2[DB_COLUMN_INDEXES.timestamp]; -}; - -exports.handleDatabases = async (dbJsonUrls, opts = {}) => { - return _.flattenDeep( - await Promise.all( - dbJsonUrls.map(async dbJsonUrl => { - try { - const currentJsonResponse = await opts.loadDbJsonUrl(dbJsonUrl); - - if (!currentJsonResponse.data) { - logger.warn(`Cannot get data from ${dbJsonUrl}`); - - return opts.formatData ? opts.formatData(dbJsonUrl, currentJsonResponse.status) : []; - } - - // JSON format declare at lib/static/modules/actions.js - const {dbUrls, jsonUrls} = currentJsonResponse.data; - - // paths from databaseUrls.json may be relative or absolute - const preparedDbUrls = opts.prepareUrls(dbUrls, dbJsonUrl); - const preparedDbJsonUrls = opts.prepareUrls(jsonUrls, dbJsonUrl); - - return await Promise.all([ - exports.handleDatabases(preparedDbJsonUrls, opts), - ...preparedDbUrls.map(dbUrl => opts.loadDbUrl(dbUrl, opts)) - ]); - } catch (e) { - logger.warn(`Error while downloading databases from ${dbJsonUrl}`, e); - - return opts.formatData ? opts.formatData(dbJsonUrl) : []; - } - }) - ) - ); -}; - -exports.mergeTables = ({db, dbPaths, getExistingTables = () => {}}) => { - db.prepare(`PRAGMA page_size = ${DB_MAX_AVAILABLE_PAGE_SIZE}`).run(); - - for (const dbPath of dbPaths) { - db.prepare(`ATTACH DATABASE '${dbPath}' AS attached`).run(); - - const getTablesStatement = db.prepare(`SELECT name FROM attached.sqlite_master WHERE type='table'`); - const tables = getExistingTables(getTablesStatement); - - for (const tableName of tables) { - db.prepare(`CREATE TABLE IF NOT EXISTS ${tableName} AS SELECT * FROM attached.${tableName} LIMIT 0`).run(); - db.prepare(`INSERT OR IGNORE INTO ${tableName} SELECT * FROM attached.${tableName}`).run(); - } - - db.prepare(`DETACH attached`).run(); - } -}; - -function createTableQuery(tableName, columns) { - const formattedColumns = columns - .map(({name, type}) => `${name} ${type}`) - .join(', '); - - return `CREATE TABLE IF NOT EXISTS ${tableName} (${formattedColumns})`; -} diff --git a/lib/db-utils/common.ts b/lib/db-utils/common.ts new file mode 100644 index 000000000..6183cc358 --- /dev/null +++ b/lib/db-utils/common.ts @@ -0,0 +1,85 @@ +import _ from 'lodash'; +import {logger} from '../common-utils'; +import {DB_MAX_AVAILABLE_PAGE_SIZE, DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS, DB_COLUMN_INDEXES} from '../constants'; +import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types'; +import type {Database, Statement} from 'better-sqlite3'; + +export const selectAllQuery = (tableName: string): string => `SELECT * FROM ${tableName}`; +export const selectAllSuitesQuery = (): string => selectAllQuery(DB_SUITES_TABLE_NAME); + +export const createTablesQuery = (): string[] => [ + createTableQuery(DB_SUITES_TABLE_NAME, SUITES_TABLE_COLUMNS) +]; + +export const compareDatabaseRowsByTimestamp = (row1: RawSuitesRow, row2: RawSuitesRow): number => { + return (row1[DB_COLUMN_INDEXES.timestamp] as number) - (row2[DB_COLUMN_INDEXES.timestamp] as number); +}; + +export interface DbLoadResult { + url: string; status: string; data: null | unknown +} + +export interface HandleDatabasesOptions { + pluginConfig: ReporterConfig; + loadDbJsonUrl: (dbJsonUrl: string) => Promise<{data: DbUrlsJsonData | null; status?: string}>; + formatData?: (dbJsonUrl: string, status?: string) => DbLoadResult; + prepareUrls: (dbUrls: string[], baseUrls: string) => string[]; + loadDbUrl: (dbUrl: string, opts: HandleDatabasesOptions) => Promise; +} + +export const handleDatabases = async (dbJsonUrls: string[], opts: HandleDatabasesOptions): Promise<(string | DbLoadResult)[]> => { + return _.flattenDeep( + await Promise.all( + dbJsonUrls.map(async dbJsonUrl => { + try { + const currentJsonResponse = await opts.loadDbJsonUrl(dbJsonUrl); + + if (!currentJsonResponse.data) { + logger.warn(`Cannot get data from ${dbJsonUrl}`); + + return opts.formatData ? opts.formatData(dbJsonUrl, currentJsonResponse.status) : []; + } + + const {dbUrls, jsonUrls} = currentJsonResponse.data; + const preparedDbUrls = opts.prepareUrls(dbUrls, dbJsonUrl); + const preparedDbJsonUrls = opts.prepareUrls(jsonUrls, dbJsonUrl); + + return await Promise.all([ + handleDatabases(preparedDbJsonUrls, opts), + ...preparedDbUrls.map((dbUrl: string) => opts.loadDbUrl(dbUrl, opts)) + ]); + } catch (e) { + logger.warn(`Error while downloading databases from ${dbJsonUrl}`, e); + + return opts.formatData ? opts.formatData(dbJsonUrl) : []; + } + }) + ) + ); +}; + +export const mergeTables = ({db, dbPaths, getExistingTables = (): string[] => []}: { db: Database, dbPaths: string[], getExistingTables?: (getTablesStatement: Statement<[]>) => string[] }): void => { + db.prepare(`PRAGMA page_size = ${DB_MAX_AVAILABLE_PAGE_SIZE}`).run(); + + for (const dbPath of dbPaths) { + db.prepare(`ATTACH DATABASE '${dbPath}' AS attached`).run(); + + const getTablesStatement = db.prepare<[]>(`SELECT name FROM attached.sqlite_master WHERE type='table'`); + const tables = getExistingTables(getTablesStatement); + + for (const tableName of tables) { + db.prepare(`CREATE TABLE IF NOT EXISTS ${tableName} AS SELECT * FROM attached.${tableName} LIMIT 0`).run(); + db.prepare(`INSERT OR IGNORE INTO ${tableName} SELECT * FROM attached.${tableName}`).run(); + } + + db.prepare(`DETACH attached`).run(); + } +}; + +function createTableQuery(tableName: string, columns: { name: string, type: string }[]): string { + const formattedColumns = columns + .map(({name, type}) => `${name} ${type}`) + .join(', '); + + return `CREATE TABLE IF NOT EXISTS ${tableName} (${formattedColumns})`; +} diff --git a/lib/db-utils/server.js b/lib/db-utils/server.ts similarity index 53% rename from lib/db-utils/server.js rename to lib/db-utils/server.ts index 1530e7e61..4d840f7ba 100644 --- a/lib/db-utils/server.js +++ b/lib/db-utils/server.ts @@ -1,21 +1,23 @@ -'use strict'; - -const path = require('path'); -const crypto = require('crypto'); -const axios = require('axios'); -const fs = require('fs-extra'); -const Database = require('better-sqlite3'); -const chalk = require('chalk'); -const NestedError = require('nested-error-stacks'); - -const StaticTestsTreeBuilder = require('../tests-tree-builder/static'); -const commonSqliteUtils = require('./common'); -const {isUrl, fetchFile, normalizeUrls, logger} = require('../common-utils'); - -const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../constants/database'); - -function downloadDatabases(dbJsonUrls, opts) { - const loadDbJsonUrl = async (dbJsonUrl) => { +import path from 'path'; +import crypto from 'crypto'; +import axios from 'axios'; +import fs from 'fs-extra'; +import Database from 'better-sqlite3'; +import chalk from 'chalk'; +import NestedError from 'nested-error-stacks'; + +import {StaticTestsTreeBuilder} from '../tests-tree-builder/static'; +import * as commonSqliteUtils from './common'; +import {isUrl, fetchFile, normalizeUrls, logger} from '../common-utils'; +import {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} from '../constants'; +import {DbLoadResult, HandleDatabasesOptions} from './common'; +import {DbUrlsJsonData, RawSuitesRow, ReporterConfig} from '../types'; +import {Tree} from '../tests-tree-builder/base'; + +export * from './common'; + +export async function downloadDatabases(dbJsonUrls: string[], opts: HandleDatabasesOptions): Promise<(string | DbLoadResult)[]> { + const loadDbJsonUrl = async (dbJsonUrl: string): Promise<{data: DbUrlsJsonData | null}> => { if (isUrl(dbJsonUrl)) { return fetchFile(dbJsonUrl); } @@ -24,20 +26,20 @@ function downloadDatabases(dbJsonUrls, opts) { return {data}; }; - const prepareUrls = (urls, baseUrl) => isUrl(baseUrl) ? normalizeUrls(urls, baseUrl) : urls; - const loadDbUrl = (dbUrl, opts) => downloadSingleDatabase(dbUrl, opts); + const prepareUrls = (urls: string[], baseUrl: string): string[] => isUrl(baseUrl) ? normalizeUrls(urls, baseUrl) : urls; + const loadDbUrl = (dbUrl: string, opts: HandleDatabasesOptions): Promise => downloadSingleDatabase(dbUrl, opts); return commonSqliteUtils.handleDatabases(dbJsonUrls, {...opts, loadDbJsonUrl, prepareUrls, loadDbUrl}); } -async function mergeDatabases(srcDbPaths, reportPath) { +export async function mergeDatabases(srcDbPaths: string[], reportPath: string): Promise { try { const mainDatabaseUrls = path.resolve(reportPath, DATABASE_URLS_JSON_NAME); const mergedDbPath = path.resolve(reportPath, LOCAL_DATABASE_NAME); const mergedDb = new Database(mergedDbPath); commonSqliteUtils.mergeTables({db: mergedDb, dbPaths: srcDbPaths, getExistingTables: (statement) => { - return statement.all().map((table) => table.name); + return statement.all().map((table) => (table as {name: string}).name); }}); for (const dbPath of srcDbPaths) { @@ -47,31 +49,31 @@ async function mergeDatabases(srcDbPaths, reportPath) { await rewriteDatabaseUrls([mergedDbPath], mainDatabaseUrls, reportPath); mergedDb.close(); - } catch (err) { + } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any throw new NestedError('Error while merging databases', err); } } -function getTestsTreeFromDatabase(dbPath) { +export function getTestsTreeFromDatabase(dbPath: string): Tree { try { const db = new Database(dbPath, {readonly: true, fileMustExist: true}); const testsTreeBuilder = StaticTestsTreeBuilder.create(); - const suitesRows = db.prepare(commonSqliteUtils.selectAllSuitesQuery()) + const suitesRows = (db.prepare(commonSqliteUtils.selectAllSuitesQuery()) .raw() - .all() + .all() as RawSuitesRow[]) .sort(commonSqliteUtils.compareDatabaseRowsByTimestamp); const {tree} = testsTreeBuilder.build(suitesRows); db.close(); return tree; - } catch (err) { + } catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any throw new NestedError('Error while getting data from database', err); } } -async function downloadSingleDatabase(dbUrl, {pluginConfig} = {}) { +async function downloadSingleDatabase(dbUrl: string, {pluginConfig}: {pluginConfig: ReporterConfig}): Promise { if (!isUrl(dbUrl)) { return path.resolve(pluginConfig.path, dbUrl); } @@ -97,7 +99,7 @@ async function downloadSingleDatabase(dbUrl, {pluginConfig} = {}) { return dest; } -function getUniqueFileNameForLink(link) { +function getUniqueFileNameForLink(link: string): string { const fileName = crypto .createHash('sha256') .update(link) @@ -107,7 +109,7 @@ function getUniqueFileNameForLink(link) { return `${fileName}${fileExt}`; } -async function rewriteDatabaseUrls(dbPaths, mainDatabaseUrls, reportPath) { +async function rewriteDatabaseUrls(dbPaths: string[], mainDatabaseUrls: string, reportPath: string): Promise { const dbUrls = dbPaths.map(p => path.relative(reportPath, p)); await fs.writeJson(mainDatabaseUrls, { @@ -115,10 +117,3 @@ async function rewriteDatabaseUrls(dbPaths, mainDatabaseUrls, reportPath) { jsonUrls: [] }); } - -module.exports = { - ...commonSqliteUtils, - downloadDatabases, - mergeDatabases, - getTestsTreeFromDatabase -}; diff --git a/lib/gui/tool-runner/report-subscriber.js b/lib/gui/tool-runner/report-subscriber.js index 03f487b06..696efee1b 100644 --- a/lib/gui/tool-runner/report-subscriber.js +++ b/lib/gui/tool-runner/report-subscriber.js @@ -5,7 +5,7 @@ const PQueue = require('p-queue'); const clientEvents = require('../constants/client-events'); const {RUNNING} = require('../../constants/test-statuses'); const {getSuitePath} = require('../../plugin-utils'); -const createWorkers = require('../../workers/create-workers'); +const {createWorkers} = require('../../workers/create-workers'); const {logError} = require('../../server-utils'); let workers; diff --git a/lib/local-images-saver.js b/lib/local-images-saver.js deleted file mode 100644 index 466fbc61d..000000000 --- a/lib/local-images-saver.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const utils = require('./server-utils'); - -module.exports = { - saveImg: async (srcCurrPath, {destPath, reportDir}) => { - await utils.copyFileAsync(srcCurrPath, destPath, {reportDir}); - - return destPath; - } -}; diff --git a/lib/local-images-saver.ts b/lib/local-images-saver.ts new file mode 100644 index 000000000..dea4dc890 --- /dev/null +++ b/lib/local-images-saver.ts @@ -0,0 +1,10 @@ +import {copyFileAsync} from './server-utils'; +import type {ImagesSaver} from './types'; + +export const LocalImagesSaver: ImagesSaver = { + saveImg: async (srcCurrPath, {destPath, reportDir}) => { + await copyFileAsync(srcCurrPath, destPath, {reportDir}); + + return destPath; + } +}; diff --git a/lib/plugin-adapter.js b/lib/plugin-adapter.js deleted file mode 100644 index 61309f887..000000000 --- a/lib/plugin-adapter.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const Promise = require('bluebird'); -const parseConfig = require('./config'); -const StaticReportBuilder = require('./report-builder/static'); -const utils = require('./server-utils'); -const cliCommands = require('./cli-commands'); -const PluginApi = require('./plugin-api'); - -module.exports = class PluginAdapter { - static create(hermione, opts) { - return new this(hermione, opts); - } - - constructor(hermione, opts) { - this._hermione = hermione; - this._config = parseConfig(opts); - } - - isEnabled() { - return this._config.enabled; - } - - addApi() { - this._hermione.htmlReporter = PluginApi.create(this._config); - - return this; - } - - addCliCommands() { - _.values(cliCommands).forEach((command) => { - this._hermione.on(this._hermione.events.CLI, (commander) => { - require(`./cli-commands/${command}`)(commander, this._config, this._hermione); - commander.prependListener(`command:${command}`, () => this._run = _.noop); - }); - }); - - return this; - } - - init(prepareData) { - this._hermione.on(this._hermione.events.INIT, () => this._run(prepareData)); - - return this; - } - - async _createStaticReportBuilder(prepareData) { - const staticReportBuilder = StaticReportBuilder.create(this._hermione, 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}); - }); - } - - async _run(prepareData) { - 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/plugin-adapter.ts b/lib/plugin-adapter.ts new file mode 100644 index 000000000..dfe710872 --- /dev/null +++ b/lib/plugin-adapter.ts @@ -0,0 +1,82 @@ +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'; + +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); + 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, 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/plugin-api.js b/lib/plugin-api.js deleted file mode 100644 index d0c0becc3..000000000 --- a/lib/plugin-api.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -const EventsEmitter2 = require('eventemitter2'); -const pluginEvents = require('./constants/plugin-events'); -const {downloadDatabases, mergeDatabases, getTestsTreeFromDatabase} = require('./db-utils/server'); - -module.exports = class HtmlReporter extends EventsEmitter2 { - static create(config) { - return new this(config); - } - - constructor(config) { - super(); - - this._config = config; - this._values = { - extraItems: {}, - metaInfoExtenders: {}, - imagesSaver: require('./local-images-saver'), - reportsSaver: null - }; - this._version = require('../package.json').version; - } - - get config() { - return this._config; - } - - get version() { - return this._version; - } - - get events() { - return pluginEvents; - } - - addExtraItem(key, value) { - this._values.extraItems[key] = value; - } - - get extraItems() { - return this._values.extraItems; - } - - addMetaInfoExtender(key, value) { - this._values.metaInfoExtenders[key] = value; - } - - get metaInfoExtenders() { - return this._values.metaInfoExtenders; - } - - set imagesSaver(imagesSaver) { - this._values.imagesSaver = imagesSaver; - } - - get imagesSaver() { - return this._values.imagesSaver; - } - - set reportsSaver(reportsSaver) { - this._values.reportsSaver = reportsSaver; - } - - get reportsSaver() { - return this._values.reportsSaver; - } - - get values() { - return this._values; - } - - downloadDatabases(...args) { - return downloadDatabases(...args); - } - - mergeDatabases(...args) { - return mergeDatabases(...args); - } - - getTestsTreeFromDatabase(...args) { - return getTestsTreeFromDatabase(...args); - } -}; diff --git a/lib/plugin-api.ts b/lib/plugin-api.ts new file mode 100644 index 000000000..42a9d17e5 --- /dev/null +++ b/lib/plugin-api.ts @@ -0,0 +1,96 @@ +import EventsEmitter2 from 'eventemitter2'; +import {PluginEvents} from './constants'; +import {downloadDatabases, mergeDatabases, getTestsTreeFromDatabase} from './db-utils/server'; +import {LocalImagesSaver} from './local-images-saver'; +import {version} from '../package.json'; +import {ImagesSaver, ReporterConfig, ReportsSaver} from './types'; + +interface HtmlReporterValues { + extraItems: Record; + metaInfoExtenders: Record; + imagesSaver: ImagesSaver; + reportsSaver: ReportsSaver | null; +} + +export class HtmlReporter extends EventsEmitter2 { + protected _config: ReporterConfig; + protected _values: HtmlReporterValues; + protected _version: string; + + static create(this: new (config: ReporterConfig) => T, config: ReporterConfig): T { + return new this(config); + } + + constructor(config: ReporterConfig) { + super(); + + this._config = config; + this._values = { + extraItems: {}, + metaInfoExtenders: {}, + imagesSaver: LocalImagesSaver, + reportsSaver: null + }; + this._version = version; + } + + get config(): ReporterConfig { + return this._config; + } + + get version(): string { + return this._version; + } + + get events(): typeof PluginEvents { + return PluginEvents; + } + + addExtraItem(key: string, value: string): void { + this._values.extraItems[key] = value; + } + + get extraItems(): Record { + return this._values.extraItems; + } + + addMetaInfoExtender(key: string, value: string): void { + this._values.metaInfoExtenders[key] = value; + } + + get metaInfoExtenders(): Record { + return this._values.metaInfoExtenders; + } + + set imagesSaver(imagesSaver: ImagesSaver) { + this._values.imagesSaver = imagesSaver; + } + + get imagesSaver(): ImagesSaver { + return this._values.imagesSaver; + } + + set reportsSaver(reportsSaver: ReportsSaver) { + this._values.reportsSaver = reportsSaver; + } + + get reportsSaver(): ReportsSaver | null { + return this._values.reportsSaver; + } + + get values(): HtmlReporterValues { + return this._values; + } + + downloadDatabases(...args: Parameters): ReturnType { + return downloadDatabases(...args); + } + + mergeDatabases(...args: Parameters): ReturnType { + return mergeDatabases(...args); + } + + getTestsTreeFromDatabase(...args: Parameters): ReturnType { + return getTestsTreeFromDatabase(...args); + } +} diff --git a/lib/report-builder/gui.js b/lib/report-builder/gui.js index acc99e6d2..b5f393fd7 100644 --- a/lib/report-builder/gui.js +++ b/lib/report-builder/gui.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const path = require('path'); -const StaticReportBuilder = require('./static'); +const {StaticReportBuilder} = require('./static'); const GuiTestsTreeBuilder = require('../tests-tree-builder/gui'); const {IDLE, RUNNING, SKIPPED, FAIL, SUCCESS, UPDATED} = require('../constants/test-statuses'); const {hasResultFails, hasNoRefImageErrors} = require('../static/modules/utils'); diff --git a/lib/report-builder/static.js b/lib/report-builder/static.js deleted file mode 100644 index fa0f12e03..000000000 --- a/lib/report-builder/static.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const path = require('path'); -const fs = require('fs-extra'); - -const {IDLE, RUNNING, SKIPPED, FAIL, ERROR, SUCCESS} = require('../constants/test-statuses'); -const {LOCAL_DATABASE_NAME} = require('../constants/database'); -const {SqliteAdapter} = require('../sqlite-adapter'); -const {TestAdapter} = require('../test-adapter'); -const {hasNoRefImageErrors} = require('../static/modules/utils'); -const {hasImage, saveStaticFilesToReportDir, writeDatabaseUrlsFile} = require('../server-utils'); - -const ignoredStatuses = [RUNNING, IDLE]; - -module.exports = class StaticReportBuilder { - static create(...args) { - return new this(...args); - } - - constructor(hermione, pluginConfig, {reuse = false} = {}) { - this._hermione = hermione; - this._pluginConfig = pluginConfig; - - this._sqliteAdapter = SqliteAdapter.create({ - hermione: this._hermione, - reportPath: this._pluginConfig.path, - reuse - }); - } - - async init() { - await this._sqliteAdapter.init(); - } - - format(result, status) { - result.timestamp = Date.now(); - - return result instanceof TestAdapter - ? result - : TestAdapter.create(result, { - hermione: this._hermione, - sqliteAdapter: this._sqliteAdapter, - status - }); - } - - async saveStaticFiles() { - const destPath = this._pluginConfig.path; - - await Promise.all( - [ - saveStaticFilesToReportDir(this._hermione, this._pluginConfig, destPath), - writeDatabaseUrlsFile(destPath, [LOCAL_DATABASE_NAME]) - ] - ); - } - - addSkipped(result) { - const formattedResult = this.format(result); - - return this._addTestResult(formattedResult, { - status: SKIPPED, - skipReason: formattedResult.suite.skipComment - }); - } - - addSuccess(result) { - return this._addTestResult(this.format(result), {status: SUCCESS}); - } - - addFail(result) { - return this._addFailResult(this.format(result)); - } - - addError(result) { - return this._addErrorResult(this.format(result)); - } - - addRetry(result) { - const formattedResult = this.format(result); - - if (formattedResult.hasDiff()) { - return this._addFailResult(formattedResult); - } else { - return this._addErrorResult(formattedResult); - } - } - - _addFailResult(formattedResult) { - return this._addTestResult(formattedResult, {status: FAIL}); - } - - _addErrorResult(formattedResult) { - return this._addTestResult(formattedResult, {status: ERROR, error: formattedResult.error}); - } - - _addTestResult(formattedResult, props) { - formattedResult.image = hasImage(formattedResult); - - const testResult = this._createTestResult(formattedResult, _.extend(props, { - timestamp: formattedResult.timestamp - })); - - if (hasNoRefImageErrors(formattedResult)) { - testResult.status = FAIL; - } - - if (!ignoredStatuses.includes(testResult.status)) { - this._writeTestResultToDb(testResult, formattedResult); - } - - return formattedResult; - } - - _createTestResult(result, props) { - const { - browserId, suite, sessionId, description, history, - imagesInfo, screenshot, multipleTabs, errorDetails - } = result; - - const {baseHost, saveErrorDetails} = this._pluginConfig; - const suiteUrl = suite.getUrl({browserId, baseHost}); - const metaInfo = _.merge(_.cloneDeep(result.meta), {url: suite.fullUrl, file: suite.file, sessionId}); - - const testResult = Object.assign({ - suiteUrl, name: browserId, metaInfo, description, history, - imagesInfo, screenshot: Boolean(screenshot), multipleTabs - }, props); - - if (saveErrorDetails && errorDetails) { - testResult.errorDetails = _.pick(errorDetails, ['title', 'filePath']); - } - - return testResult; - } - - _writeTestResultToDb(testResult, formattedResult) { - const {suite} = formattedResult; - const suiteName = formattedResult.state.name; - const suitePath = suite.path.concat(suiteName); - - this._sqliteAdapter.write({testResult, suitePath, suiteName}); - } - - _deleteTestResultFromDb({where, orderBy, orderDescending, limit}, ...args) { - this._sqliteAdapter.delete({where, orderBy, orderDescending, limit}, ...args); - } - - async finalize() { - this._sqliteAdapter.close(); - - const reportsSaver = this._hermione.htmlReporter.reportsSaver; - - if (reportsSaver) { - const reportDir = this._pluginConfig.path; - const src = path.join(reportDir, LOCAL_DATABASE_NAME); - const dbPath = await reportsSaver.saveReportData(src, {destPath: LOCAL_DATABASE_NAME, reportDir: reportDir}); - await writeDatabaseUrlsFile(reportDir, [dbPath]); - await fs.remove(src); - } - } -}; diff --git a/lib/report-builder/static.ts b/lib/report-builder/static.ts new file mode 100644 index 000000000..388c4bbb9 --- /dev/null +++ b/lib/report-builder/static.ts @@ -0,0 +1,173 @@ +import _ from 'lodash'; +import path from 'path'; +import fs from 'fs-extra'; +import type {default as Hermione} from 'hermione'; + +import {IDLE, RUNNING, SKIPPED, FAIL, ERROR, SUCCESS, TestStatus, LOCAL_DATABASE_NAME} from '../constants'; +import {PreparedTestResult, SqliteAdapter} from '../sqlite-adapter'; +import {TestAdapter} from '../test-adapter'; +import {hasNoRefImageErrors} from '../static/modules/utils'; +import {hasImage, saveStaticFilesToReportDir, writeDatabaseUrlsFile} from '../server-utils'; +import {HtmlReporterApi, ReporterConfig, TestResult} from '../types'; + +const ignoredStatuses = [RUNNING, IDLE]; + +interface StaticReportBuilderOptions { + reuse: boolean; +} + +export class StaticReportBuilder { + protected _hermione: Hermione & HtmlReporterApi; + protected _pluginConfig: ReporterConfig; + protected _sqliteAdapter: SqliteAdapter; + + static create( + this: new (hermione: Hermione & HtmlReporterApi, pluginConfig: ReporterConfig, options?: Partial) => T, + hermione: Hermione & HtmlReporterApi, + pluginConfig: ReporterConfig, + options?: Partial + ): T { + return new this(hermione, pluginConfig, options); + } + + constructor(hermione: Hermione & HtmlReporterApi, pluginConfig: ReporterConfig, {reuse = false}: Partial = {}) { + this._hermione = hermione; + this._pluginConfig = pluginConfig; + + this._sqliteAdapter = SqliteAdapter.create({ + hermione: this._hermione, + reportPath: this._pluginConfig.path, + reuse + }); + } + + async init(): Promise { + await this._sqliteAdapter.init(); + } + + format(result: TestResult | TestAdapter, status: TestStatus): TestAdapter { + result.timestamp = Date.now(); + + return result instanceof TestAdapter + ? result + : TestAdapter.create(result, { + hermione: this._hermione, + sqliteAdapter: this._sqliteAdapter, + status + }); + } + + async saveStaticFiles(): Promise { + const destPath = this._pluginConfig.path; + + await Promise.all([ + saveStaticFilesToReportDir(this._hermione, this._pluginConfig, destPath), + writeDatabaseUrlsFile(destPath, [LOCAL_DATABASE_NAME]) + ]); + } + + addSkipped(result: TestResult | TestAdapter): TestAdapter { + const formattedResult = this.format(result, SKIPPED); + + return this._addTestResult(formattedResult, { + status: SKIPPED, + skipReason: formattedResult.suite.skipComment + }); + } + + addSuccess(result: TestResult | TestAdapter): TestAdapter { + return this._addTestResult(this.format(result, SUCCESS), {status: SUCCESS}); + } + + addFail(result: TestResult | TestAdapter): TestAdapter { + return this._addFailResult(this.format(result, FAIL)); + } + + addError(result: TestResult | TestAdapter): TestAdapter { + return this._addErrorResult(this.format(result, ERROR)); + } + + addRetry(result: TestResult | TestAdapter): TestAdapter { + const formattedResult = this.format(result, FAIL); + + if (formattedResult.hasDiff()) { + return this._addFailResult(formattedResult); + } else { + return this._addErrorResult(formattedResult); + } + } + + protected _addFailResult(formattedResult: TestAdapter): TestAdapter { + return this._addTestResult(formattedResult, {status: FAIL}); + } + + protected _addErrorResult(formattedResult: TestAdapter): TestAdapter { + return this._addTestResult(formattedResult, {status: ERROR, error: formattedResult.error}); + } + + protected _addTestResult(formattedResult: TestAdapter, props: {status: TestStatus} & Partial): TestAdapter { + formattedResult.image = hasImage(formattedResult); + + const testResult = this._createTestResult(formattedResult, _.extend(props, { + timestamp: formattedResult.timestamp + })); + + if (hasNoRefImageErrors(formattedResult)) { + testResult.status = FAIL; + } + + if (!ignoredStatuses.includes(testResult.status)) { + this._writeTestResultToDb(testResult, formattedResult); + } + + return formattedResult; + } + + _createTestResult(result: TestAdapter, props: {status: TestStatus} & Partial): PreparedTestResult { + const { + browserId, suite, sessionId, description, history, + imagesInfo = [], screenshot, multipleTabs, errorDetails + } = result; + + const {baseHost, saveErrorDetails} = this._pluginConfig; + const suiteUrl: string = suite.getUrl({baseHost}); + const metaInfo = _.merge(_.cloneDeep(result.meta), {url: suite.fullUrl, file: suite.file, sessionId}); + + const testResult: PreparedTestResult = Object.assign({ + suiteUrl, name: browserId, metaInfo, description, history, + imagesInfo, screenshot: Boolean(screenshot), multipleTabs + }, props); + + if (saveErrorDetails && errorDetails) { + testResult.errorDetails = _.pick(errorDetails, ['title', 'filePath']); + } + + return testResult; + } + + _writeTestResultToDb(testResult: PreparedTestResult, formattedResult: TestAdapter): void { + const {suite} = formattedResult; + const suiteName = formattedResult.state.name; + const suitePath = suite.path.concat(suiteName); + + this._sqliteAdapter.write({testResult, suitePath, suiteName}); + } + + _deleteTestResultFromDb(...args: Parameters): void { + this._sqliteAdapter.delete(...args); + } + + async finalize(): Promise { + this._sqliteAdapter.close(); + + const reportsSaver = this._hermione.htmlReporter.reportsSaver; + + if (reportsSaver) { + const reportDir = this._pluginConfig.path; + const src = path.join(reportDir, LOCAL_DATABASE_NAME); + const dbPath = await reportsSaver.saveReportData(src, {destPath: LOCAL_DATABASE_NAME, reportDir: reportDir}); + await writeDatabaseUrlsFile(reportDir, [dbPath]); + await fs.remove(src); + } + } +} diff --git a/lib/server-utils.ts b/lib/server-utils.ts index c7fb02073..fccc4d9b3 100644 --- a/lib/server-utils.ts +++ b/lib/server-utils.ts @@ -5,7 +5,7 @@ import _ from 'lodash'; import fs from 'fs-extra'; import {logger} from './common-utils'; import {UPDATED, RUNNING, IDLE, SKIPPED, IMAGES_PATH, TestStatus} from './constants'; -import type HtmlReporter from './plugin-api'; +import type {HtmlReporter} from './plugin-api'; import type {TestAdapter} from './test-adapter'; import {CustomGuiItem, HtmlReporterApi, ReporterConfig} from './types'; import type Hermione from 'hermione'; diff --git a/lib/sqlite-adapter.ts b/lib/sqlite-adapter.ts index 7ec859500..0d0502a2c 100644 --- a/lib/sqlite-adapter.ts +++ b/lib/sqlite-adapter.ts @@ -9,9 +9,9 @@ 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 {createTablesQuery} from './db-utils/server'; +import {createTablesQuery} from './db-utils/common'; import {DbNotInitializedError} from './errors/db-not-initialized-error'; -import type {HtmlReporterApi, ImageInfoFull} from './types'; +import type {ErrorDetails, HtmlReporterApi, ImageInfoFull} from './types'; const debug = makeDebug('html-reporter:sqlite-adapter'); @@ -37,13 +37,14 @@ export interface PreparedTestResult { metaInfo: Record; history: string[]; description: unknown; - error: Error; + error?: {message?: string; stack?: string; stateName?: string}; skipReason?: string; imagesInfo: ImageInfoFull[]; screenshot: boolean; multipleTabs: boolean; status: TestStatus; timestamp?: number; + errorDetails?: ErrorDetails; } interface ParseTestResultParams { diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index 73f1b2491..817afeaf4 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -1,7 +1,7 @@ import axios from 'axios'; import {isEmpty, difference} from 'lodash'; import {notify, dismissNotification as dismissNotify, POSITIONS} from 'reapop'; -import StaticTestsTreeBuilder from '../../tests-tree-builder/static'; +import {StaticTestsTreeBuilder} from '../../tests-tree-builder/static'; import actionNames from './action-names'; import {types as modalTypes} from '../components/modals'; import {QUEUED} from '../../constants/test-statuses'; diff --git a/lib/static/modules/custom-queries.js b/lib/static/modules/custom-queries.js index 1d996a50f..f31ee33bf 100644 --- a/lib/static/modules/custom-queries.js +++ b/lib/static/modules/custom-queries.js @@ -3,7 +3,7 @@ import {parseQuery, decodeBrowsers} from './query-params'; import {pick, has} from 'lodash'; import {ViewMode} from '../../constants/view-modes'; -import {config} from '../../constants/defaults'; +import {configDefaults} from '../../constants/defaults'; const allowedViewModes = new Set(Object.values(ViewMode)); @@ -13,7 +13,7 @@ export function getViewQuery(queryString) { query.filteredBrowsers = decodeBrowsers(query.filteredBrowsers); if (has(query, 'viewMode') && !allowedViewModes.has(query.viewMode)) { - query.viewMode = config.defaultView; + query.viewMode = configDefaults.defaultView; } return pick(query, [ diff --git a/lib/static/modules/default-state.js b/lib/static/modules/default-state.js index 2ac31c190..59d073bbc 100644 --- a/lib/static/modules/default-state.js +++ b/lib/static/modules/default-state.js @@ -1,12 +1,12 @@ 'use strict'; -const defaults = require('../../constants/defaults'); +const {configDefaults} = require('../../constants/defaults'); const {ViewMode} = require('../../constants/view-modes'); const {DiffModes} = require('../../constants/diff-modes'); const {EXPAND_ERRORS} = require('../../constants/expand-modes'); const {RESULT_KEYS} = require('../../constants/group-tests'); -export default Object.assign(defaults, { +export default Object.assign({config: configDefaults}, { gui: true, running: false, processing: false, diff --git a/lib/static/modules/reducers/browsers.js b/lib/static/modules/reducers/browsers.js index 83f1ce14f..924c1ed1f 100644 --- a/lib/static/modules/reducers/browsers.js +++ b/lib/static/modules/reducers/browsers.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import actionNames from '../action-names'; -import {versions as BrowserVersions} from '../../../constants/browser'; +import {BrowserVersions} from '../../../constants/browser'; export default (state, action) => { switch (action.type) { diff --git a/lib/suite-adapter.js b/lib/suite-adapter.js deleted file mode 100644 index fb386b170..000000000 --- a/lib/suite-adapter.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const path = require('path'); -const url = require('url'); -const Uri = require('urijs'); - -const {getSuitePath} = require('./plugin-utils'); - -const wrapLinkByTag = (text) => { - return text.replace(/https?:\/\/[^\s]*/g, (url) => { - return `${url}`; - }); -}; - -function getSkipComment(suite) { - return suite.skipReason || suite.parent && getSkipComment(suite.parent); -} - -module.exports = class SuiteAdapter { - static create(suite = {}, config = {}) { - return new this(suite, config); - } - - constructor(suite, config) { - this._suite = suite; - this._config = config; - } - - _wrapSkipComment(skipComment) { - return skipComment ? wrapLinkByTag(skipComment) : 'Unknown reason'; - } - - _configureUrl(url, baseHost) { - return _.isEmpty(baseHost) - ? url - : Uri(baseHost).resource(url).href(); - } - - get skipComment() { - const skipComment = getSkipComment(this._suite); - - return this._wrapSkipComment(skipComment); - } - - get fullName() { - return this._suite.fullTitle(); - } - - get path() { - return getSuitePath(this._suite.parent); - } - - get file() { - return path.relative(process.cwd(), this._suite.file); - } - - getUrl(opts = {}) { - const url = _.get(this, '_suite.meta.url', ''); - - return this._configureUrl(url, opts.baseHost); - } - - get fullUrl() { - const baseUrl = this.getUrl(); - - return baseUrl - ? url.parse(baseUrl).path - : ''; - } -}; diff --git a/lib/suite-adapter.ts b/lib/suite-adapter.ts new file mode 100644 index 000000000..f7feb2e81 --- /dev/null +++ b/lib/suite-adapter.ts @@ -0,0 +1,70 @@ +import _ from 'lodash'; +import path from 'path'; +import url from 'url'; +import Uri from 'urijs'; +import {getSuitePath} from './plugin-utils'; +import {Suite, TestResult} from './types'; + +const wrapLinkByTag = (text: string): string => { + return text.replace(/https?:\/\/[^\s]*/g, (url) => { + return `${url}`; + }); +}; + +function getSkipComment(suite: TestResult | Suite): string | null | undefined { + return suite.skipReason || suite.parent && getSkipComment(suite.parent); +} + +export class SuiteAdapter { + protected _suite: TestResult; + + static create(suite: TestResult): SuiteAdapter { + return new this(suite); + } + + constructor(suite: TestResult) { + this._suite = suite; + } + + protected _wrapSkipComment(skipComment: string | null | undefined): string { + return skipComment ? wrapLinkByTag(skipComment) : 'Unknown reason'; + } + + protected _configureUrl(url: string, baseHost: string): string { + return _.isEmpty(baseHost) + ? url + : Uri(baseHost).resource(url).href(); + } + + get skipComment(): string { + const skipComment = getSkipComment(this._suite); + + return this._wrapSkipComment(skipComment); + } + + get fullName(): string { + return this._suite.fullTitle(); + } + + get path(): string[] { + return getSuitePath(this._suite.parent); + } + + get file(): string { + return path.relative(process.cwd(), this._suite.file); + } + + getUrl(opts: { baseHost?: string } = {}): string { + const url = _.get(this, '_suite.meta.url', '') as string; + + return this._configureUrl(url, opts.baseHost || ''); + } + + get fullUrl(): string { + const baseUrl = this.getUrl(); + + return baseUrl + ? url.parse(baseUrl).path || '' + : ''; + } +} diff --git a/lib/test-adapter.ts b/lib/test-adapter.ts index f8e030610..ccda49982 100644 --- a/lib/test-adapter.ts +++ b/lib/test-adapter.ts @@ -5,7 +5,7 @@ import tmp from 'tmp'; import crypto from 'crypto'; import type {default as Hermione} from 'hermione'; -import SuiteAdapter from './suite-adapter'; +import {SuiteAdapter} from './suite-adapter'; import {DB_COLUMNS} from './constants/database'; import {getSuitePath} from './plugin-utils'; import {getCommandsHistory} from './history-utils'; @@ -18,7 +18,7 @@ import { HtmlReporterApi, ImageInfo, ImagesSaver, - SuitesRow, + LabeledSuitesRow, TestResult, ImageData, ImageInfoFull, ImageDiffError, AssertViewResult, ImageInfoError, @@ -26,7 +26,7 @@ import { } from './types'; import type {SqliteAdapter} from './sqlite-adapter'; import EventEmitter2 from 'eventemitter2'; -import type HtmlReporter from './plugin-api'; +import type {HtmlReporter} from './plugin-api'; import type * as Workers from './workers/worker'; interface PrepareTestResultData { @@ -91,6 +91,8 @@ export class TestAdapter { this._attempt = testsAttempts.get(this._testId) || 0; } + image?: boolean; + get suite(): SuiteAdapter { return this._suite; } @@ -121,14 +123,14 @@ export class TestAdapter { const suitePath = getSuitePath(this._testResult); const suitePathString = JSON.stringify(suitePath); - const imagesInfoResult = this._sqliteAdapter.query | undefined>({ + const imagesInfoResult = this._sqliteAdapter.query | undefined>({ select: DB_COLUMNS.IMAGES_INFO, where: `${DB_COLUMNS.SUITE_PATH} = ? AND ${DB_COLUMNS.NAME} = ?`, orderBy: DB_COLUMNS.TIMESTAMP, orderDescending: true }, suitePathString, browserName); - const imagesInfo: ImageInfoFull[] = imagesInfoResult && JSON.parse(imagesInfoResult[DB_COLUMNS.IMAGES_INFO as keyof SuitesRow]) || []; + const imagesInfo: ImageInfoFull[] = imagesInfoResult && JSON.parse(imagesInfoResult[DB_COLUMNS.IMAGES_INFO as keyof Pick]) || []; return imagesInfo.find(info => info.stateName === stateName); } diff --git a/lib/tests-tree-builder/base.js b/lib/tests-tree-builder/base.ts similarity index 50% rename from lib/tests-tree-builder/base.js rename to lib/tests-tree-builder/base.ts index 621ff31f4..42fd78afe 100644 --- a/lib/tests-tree-builder/base.js +++ b/lib/tests-tree-builder/base.ts @@ -1,11 +1,82 @@ -'use strict'; - -const _ = require('lodash'); -const {determineStatus} = require('../common-utils'); -const {versions: browserVersions} = require('../constants/browser'); - -module.exports = class ResultsTreeBuilder { - static create() { +import _ from 'lodash'; +import {determineStatus} from '../common-utils'; +import {TestStatus, BrowserVersions} from '../constants'; +import {TestAdapter} from '../test-adapter'; +import {ImageInfoFull, ParsedSuitesRow} from '../types'; + +type TreeResult = { + id: string; + parentId: string; + status: TestStatus; + imageIds: string[]; +} & Omit; + +interface TreeBrowser { + id: string; + name: string; + parentId: string; + resultIds: string[]; + version: string; +} + +interface TreeSuite { + status?: TestStatus; + id: string; + parentId: string | null; + name: string; + suitePath: string[]; + root: boolean; + suiteIds?: string[]; + browserIds?: string[]; +} + +type TreeImages = { + id: string; + parentId: string; +} & ImageInfoFull; + +export interface Tree { + suites: { + byId: Record, + allIds: string[], + allRootIds: string[] + }, + browsers: { + byId: Record, + allIds: string[] + }, + results: { + byId: Record, + allIds: string[] + }, + images: { + byId: Record, + allIds: string[] + } +} + +interface ResultPayload { + id: string; + parentId: string; + result: ParsedSuitesRow; +} + +interface BrowserPayload { + id: string; + name: string; + parentId: string; + version: string; +} + +interface ImagesPayload { + imagesInfo: ImageInfoFull[]; + parentId: string; +} + +export class BaseTestsTreeBuilder { + protected _tree: Tree; + + static create(this: new () => T): T { return new this(); } @@ -18,12 +89,12 @@ module.exports = class ResultsTreeBuilder { }; } - get tree() { + get tree(): Tree { return this._tree; } - sortTree() { - const sortChildSuites = (suiteId) => { + sortTree(): void { + const sortChildSuites = (suiteId: string): void => { const childSuite = this._tree.suites.byId[suiteId]; if (childSuite.suiteIds) { @@ -38,16 +109,16 @@ module.exports = class ResultsTreeBuilder { this._tree.suites.allRootIds.sort().forEach(sortChildSuites); } - addTestResult(testResult, formattedResult) { + addTestResult(testResult: ParsedSuitesRow, formattedResult: Pick): void { const {testPath, browserId: browserName, attempt} = formattedResult; const {imagesInfo} = testResult; - const {browserVersion = browserVersions.UNKNOWN} = testResult.metaInfo; + const {browserVersion = BrowserVersions.UNKNOWN} = testResult.metaInfo as {browserVersion: string}; const suiteId = this._buildId(testPath); const browserId = this._buildId(suiteId, browserName); - const testResultId = this._buildId(browserId, attempt); + const testResultId = this._buildId(browserId, attempt.toString()); const imageIds = imagesInfo - .map((image, i) => this._buildId(testResultId, image.stateName || `${image.status}_${i}`)); + .map((image: ImageInfoFull, i: number) => this._buildId(testResultId, image.stateName || `${image.status}_${i}`)); this._addSuites(testPath, browserId); this._addBrowser({id: browserId, parentId: suiteId, name: browserName, version: browserVersion}, testResultId, attempt); @@ -57,11 +128,11 @@ module.exports = class ResultsTreeBuilder { this._setStatusForBranch(testPath); } - _buildId(parentId = [], name = []) { - return [].concat(parentId, name).join(' '); + protected _buildId(parentId: string | string[] = [], name: string | string[] = []): string { + return ([] as string[]).concat(parentId, name).join(' '); } - _addSuites(testPath, browserId) { + protected _addSuites(testPath: string[], browserId: string): void { testPath.reduce((suites, name, ind, arr) => { const isRoot = ind === 0; const suitePath = isRoot ? [name] : arr.slice(0, ind + 1); @@ -69,7 +140,7 @@ module.exports = class ResultsTreeBuilder { if (!suites.byId[id]) { const parentId = isRoot ? null : this._buildId(suitePath.slice(0, -1)); - const suite = {id, parentId, name, suitePath, root: isRoot}; + const suite: TreeSuite = {id, parentId, name, suitePath, root: isRoot}; this._addSuite(suite); } @@ -85,7 +156,7 @@ module.exports = class ResultsTreeBuilder { }, this._tree.suites); } - _addSuite(suite) { + protected _addSuite(suite: TreeSuite): void { const {suites} = this._tree; suites.byId[suite.id] = suite; @@ -96,7 +167,7 @@ module.exports = class ResultsTreeBuilder { } } - _addNodeId(parentSuiteId, nodeId, {fieldName}) { + protected _addNodeId(parentSuiteId: string, nodeId: string, {fieldName}: {fieldName: 'browserIds' | 'suiteIds'}): void { const {suites} = this._tree; if (!suites.byId[parentSuiteId][fieldName]) { @@ -105,15 +176,15 @@ module.exports = class ResultsTreeBuilder { } if (!this._isNodeIdExists(parentSuiteId, nodeId, {fieldName})) { - suites.byId[parentSuiteId][fieldName].push(nodeId); + suites.byId[parentSuiteId][fieldName]?.push(nodeId); } } - _isNodeIdExists(parentSuiteId, nodeId, {fieldName}) { + protected _isNodeIdExists(parentSuiteId: string, nodeId: string, {fieldName}: {fieldName: 'browserIds' | 'suiteIds'}): boolean { return _.includes(this._tree.suites.byId[parentSuiteId][fieldName], nodeId); } - _addBrowser({id, parentId, name, version}, testResultId, attempt) { + protected _addBrowser({id, parentId, name, version}: BrowserPayload, testResultId: string, attempt: number): void { const {browsers} = this._tree; if (!browsers.byId[id]) { @@ -124,11 +195,11 @@ module.exports = class ResultsTreeBuilder { this._addResultIdToBrowser(id, testResultId, attempt); } - _addResultIdToBrowser(browserId, testResultId, attempt) { + protected _addResultIdToBrowser(browserId: string, testResultId: string, attempt: number): void { this._tree.browsers.byId[browserId].resultIds[attempt] = testResultId; } - _addResult({id, parentId, result}, imageIds) { + protected _addResult({id, parentId, result}: ResultPayload, imageIds: string[]): void { const resultWithoutImagesInfo = _.omit(result, 'imagesInfo'); if (!this._tree.results.byId[id]) { @@ -138,14 +209,14 @@ module.exports = class ResultsTreeBuilder { this._tree.results.byId[id] = {id, parentId, ...resultWithoutImagesInfo, imageIds}; } - _addImages(imageIds, {imagesInfo, parentId}) { + protected _addImages(imageIds: string[], {imagesInfo, parentId}: ImagesPayload): void { imageIds.forEach((id, ind) => { this._tree.images.byId[id] = {...imagesInfo[ind], id, parentId}; this._tree.images.allIds.push(id); }); } - _setStatusForBranch(testPath = []) { + protected _setStatusForBranch(testPath: string[] = []): void { const suiteId = this._buildId(testPath); if (!suiteId) { @@ -154,25 +225,25 @@ module.exports = class ResultsTreeBuilder { const suite = this._tree.suites.byId[suiteId]; - const resultStatuses = _.compact([].concat(suite.browserIds)) - .map((browserId) => { + const resultStatuses = _.compact(([] as (string | undefined)[]).concat(suite.browserIds)) + .map((browserId: string) => { const browser = this._tree.browsers.byId[browserId]; - const lastResultId = _.last(browser.resultIds); + const lastResultId = _.last(browser.resultIds) as string; return this._tree.results.byId[lastResultId].status; }); - const childrenSuiteStatuses = _.compact([].concat(suite.suiteIds)) - .map((childSuiteId) => this._tree.suites.byId[childSuiteId].status); + const childrenSuiteStatuses = _.compact(([] as (string | undefined)[]).concat(suite.suiteIds)) + .map((childSuiteId: string) => this._tree.suites.byId[childSuiteId].status); - const status = determineStatus([...resultStatuses, ...childrenSuiteStatuses]); + const status = determineStatus(_.compact([...resultStatuses, ...childrenSuiteStatuses])); // if newly determined status is the same as current status, do nothing if (suite.status === status) { return; } - suite.status = status; + suite.status = status || undefined; this._setStatusForBranch(testPath.slice(0, -1)); } -}; +} diff --git a/lib/tests-tree-builder/gui.js b/lib/tests-tree-builder/gui.js index 32423f361..41caace3b 100644 --- a/lib/tests-tree-builder/gui.js +++ b/lib/tests-tree-builder/gui.js @@ -1,7 +1,7 @@ 'use strict'; const _ = require('lodash'); -const BaseTestsTreeBuilder = require('./base'); +const {BaseTestsTreeBuilder} = require('./base'); const {UPDATED} = require('../constants/test-statuses'); const {isUpdatedStatus} = require('../common-utils'); diff --git a/lib/tests-tree-builder/static.js b/lib/tests-tree-builder/static.ts similarity index 61% rename from lib/tests-tree-builder/static.js rename to lib/tests-tree-builder/static.ts index 55dddccc0..24bb64e94 100644 --- a/lib/tests-tree-builder/static.js +++ b/lib/tests-tree-builder/static.ts @@ -1,12 +1,41 @@ -'use strict'; +import _ from 'lodash'; +import {BaseTestsTreeBuilder, Tree} from './base'; +import {BrowserVersions, DB_COLUMN_INDEXES, TestStatus} from '../constants'; +import {Attempt, ParsedSuitesRow, RawSuitesRow} from '../types'; + +interface Stats { + total: number; + passed: number; + failed: number; + skipped: number; + retries: number; +} + +type FinalStats = Stats & { + perBrowser: { + [browserName: string]: { + [browserVersion: string]: Stats + } + } +} -const _ = require('lodash'); -const BaseTestsTreeBuilder = require('./base'); -const testStatus = require('../constants/test-statuses'); -const {versions: browserVersions} = require('../constants/browser'); -const {DB_COLUMN_INDEXES} = require('../constants/database'); +interface SkipItem { + browser: string; + suite: string; + comment: string; +} + +interface BrowserItem { + id: string; + versions: string[]; +} + +export class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { + protected _stats: FinalStats; + protected _skips: SkipItem[]; + protected _failedBrowserIds: { [key: string]: boolean }; + protected _passedBrowserIds: { [key: string]: boolean }; -module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { constructor() { super(); @@ -19,20 +48,19 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { this._passedBrowserIds = {}; } - build(rows = []) { - // in order to sync attempts between gui tree and static tree - const attemptsMap = new Map(); - const browsers = {}; + build(rows: RawSuitesRow[] = []): { tree: Tree; stats: FinalStats; skips: SkipItem[]; browsers: BrowserItem[] } { + const attemptsMap = new Map(); + const browsers: Record> = {}; for (const row of rows) { - const testPath = JSON.parse(row[DB_COLUMN_INDEXES.suitePath]); - const browserName = row[DB_COLUMN_INDEXES.name]; + const testPath: string[] = JSON.parse(row[DB_COLUMN_INDEXES.suitePath] as string); + const browserName = row[DB_COLUMN_INDEXES.name] as string; const testId = this._buildId(testPath); const browserId = this._buildId(testId, browserName); - attemptsMap.set(browserId, attemptsMap.has(browserId) ? attemptsMap.get(browserId) + 1 : 0); - const attempt = attemptsMap.get(browserId); + attemptsMap.set(browserId, attemptsMap.has(browserId) ? attemptsMap.get(browserId) as number + 1 : 0); + const attempt = attemptsMap.get(browserId) as number; const testResult = mkTestResult(row, {attempt}); const formattedResult = {browserId: browserName, testPath, attempt}; @@ -53,14 +81,14 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { }; } - _addResultIdToBrowser(browserId, testResultId) { + protected _addResultIdToBrowser(browserId: string, testResultId: string): void { this._tree.browsers.byId[browserId].resultIds.push(testResultId); } - _calcStats(testResult, {testId, browserId, browserName}) { + protected _calcStats(testResult: ParsedSuitesRow, {testId, browserId, browserName}: { testId: string; browserId: string; browserName: string }): void { const {status} = testResult; const {browserVersion} = testResult.metaInfo; - const version = browserVersion || browserVersions.UNKNOWN; + const version = browserVersion || BrowserVersions.UNKNOWN; if (!this._stats.perBrowser[browserName]) { this._stats.perBrowser[browserName] = {}; @@ -71,8 +99,8 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { } switch (status) { - case testStatus.FAIL: - case testStatus.ERROR: { + case TestStatus.FAIL: + case TestStatus.ERROR: { if (this._failedBrowserIds[browserId]) { this._stats.retries++; this._stats.perBrowser[browserName][version].retries++; @@ -87,7 +115,7 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { return; } - case testStatus.SUCCESS: { + case TestStatus.SUCCESS: { if (this._passedBrowserIds[browserId]) { this._stats.retries++; this._stats.perBrowser[browserName][version].retries++; @@ -115,7 +143,7 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { return; } - case testStatus.SKIPPED: { + case TestStatus.SKIPPED: { this._skips.push({ browser: browserName, suite: testId, @@ -137,9 +165,9 @@ module.exports = class StaticTestsTreeBuilder extends BaseTestsTreeBuilder { } } } -}; +} -function initStats() { +function initStats(): Stats { return { total: 0, passed: 0, @@ -149,30 +177,30 @@ function initStats() { }; } -function mkTestResult(row, data = {}) { +function mkTestResult(row: RawSuitesRow, data: {attempt: number}): ParsedSuitesRow & Attempt { return { - description: row[DB_COLUMN_INDEXES.description], - imagesInfo: JSON.parse(row[DB_COLUMN_INDEXES.imagesInfo]), - metaInfo: JSON.parse(row[DB_COLUMN_INDEXES.metaInfo]), - history: JSON.parse(row[DB_COLUMN_INDEXES.history]), + description: row[DB_COLUMN_INDEXES.description] as string | null, + imagesInfo: JSON.parse(row[DB_COLUMN_INDEXES.imagesInfo] as string), + metaInfo: JSON.parse(row[DB_COLUMN_INDEXES.metaInfo] as string), + history: JSON.parse(row[DB_COLUMN_INDEXES.history] as string), multipleTabs: Boolean(row[DB_COLUMN_INDEXES.multipleTabs]), - name: row[DB_COLUMN_INDEXES.name], + name: row[DB_COLUMN_INDEXES.name] as string, screenshot: Boolean(row[DB_COLUMN_INDEXES.screenshot]), - status: row[DB_COLUMN_INDEXES.status], - suiteUrl: row[DB_COLUMN_INDEXES.suiteUrl], - skipReason: row[DB_COLUMN_INDEXES.skipReason], - error: JSON.parse(row[DB_COLUMN_INDEXES.error]), + status: row[DB_COLUMN_INDEXES.status] as TestStatus, + 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), ...data }; } -function addBrowserVersion(browsers, testResult) { +function addBrowserVersion(browsers: Record>, testResult: ParsedSuitesRow): void { const browserId = testResult.name; if (!browsers[browserId]) { browsers[browserId] = new Set(); } - const {browserVersion = browserVersions.UNKNOWN} = testResult.metaInfo; + const {browserVersion = BrowserVersions.UNKNOWN} = testResult.metaInfo; browsers[browserId].add(browserVersion); } diff --git a/lib/types.ts b/lib/types.ts index f2c33bcce..53f3ce38a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,13 +1,21 @@ import type {LooksSameOptions, CoordBounds} from 'looks-same'; import type {default as Hermione} from 'hermione'; -import {DiffMode, TestStatus, ViewMode} from './constants'; -import type HtmlReporter from './plugin-api'; +import {DiffModeId, SaveFormat, TestStatus, ViewMode} from './constants'; +import type {HtmlReporter} from './plugin-api'; declare module 'tmp' { export const tmpdir: string; } -export interface Suite { +interface ConfigurableTestObject { + browserId: string; + browserVersion?: string; + id: string; + file: string; + skipReason: string; +} + +export interface Suite extends ConfigurableTestObject { readonly root: boolean; readonly title: string; parent: Suite | null; @@ -17,9 +25,13 @@ export interface ImagesSaver { saveImg: (localFilePath: string, options: {destPath: string; reportDir: string}) => string | Promise; } +export interface ReportsSaver { + saveReportData: (localDbPath: string, options: {destPath: string; reportDir: string}) => string | Promise; +} + export interface ErrorDetails { title: string; - data: unknown; + data?: unknown; filePath: string; } @@ -89,7 +101,7 @@ export interface ImageDiffError { export type AssertViewResult = ImageDiffError; -export interface TestResult { +export interface TestResult extends ConfigurableTestObject { assertViewResults: AssertViewResult[]; description?: string; err?: { @@ -99,11 +111,8 @@ export interface TestResult { details: ErrorDetails }; fullTitle(): string; - id: string; title: string; meta: Record - browserId: string; - browserVersion?: string; sessionId: string; timestamp: number; imagesInfo: ImageInfoFull[]; @@ -112,16 +121,52 @@ export interface TestResult { parent: Suite; } -export interface SuitesRow { +export interface LabeledSuitesRow { imagesInfo: string; + timestamp: number; +} + +export type RawSuitesRow = LabeledSuitesRow[keyof LabeledSuitesRow][]; + +export interface ParsedSuitesRow { + description: string | null; + error: { + message: string; + stack: string; + }; + history: unknown; + imagesInfo: ImageInfoFull[]; + metaInfo: { + browserVersion?: string; + [key: string]: unknown; + }; + multipleTabs: boolean; + name: string; + screenshot: boolean; + skipReason: string; + status: TestStatus; + suiteUrl: string; +} + +export interface Attempt { + attempt: number; } export interface HtmlReporterApi { htmlReporter: HtmlReporter; } +export interface ErrorPattern { + name: string; + pattern: string; +} + export interface PluginDescription { name: string; + component: string; + point?: string; + position?: 'after' | 'before' | 'wrap'; + config?: Record; } export interface CustomGuiItem { @@ -133,14 +178,26 @@ export interface CustomGuiItem { export interface ReporterConfig { baseHost: string; - defaultView: ViewMode; + commandsWithShortHistory: string[]; customGui: Record; - customScripts: object[]; - diffMode: DiffMode; - errorPatterns: object[]; + customScripts: (() => void)[]; + defaultView: ViewMode; + diffMode: DiffModeId; + enabled: boolean; + errorPatterns: ErrorPattern[]; + lazyLoadOffset: number | null; metaInfoBaseUrls: Record; path: string; plugins: PluginDescription[]; pluginsEnabled: boolean; + saveErrorDetails: boolean; + saveFormat: SaveFormat; yandexMetrika: { counterNumber: null | number }; } + +export type ReporterOptions = Omit & {errorPatterns: (string | ErrorPattern)[]}; + +export interface DbUrlsJsonData { + dbUrls: string[]; + jsonUrls: string[]; +} diff --git a/lib/workers/create-workers.js b/lib/workers/create-workers.js deleted file mode 100644 index dff0bc7b0..000000000 --- a/lib/workers/create-workers.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = (runner) => { - const workerFilepath = require.resolve('./worker'); - - return runner.registerWorkers(workerFilepath, ['saveDiffTo']); -}; diff --git a/lib/workers/create-workers.ts b/lib/workers/create-workers.ts new file mode 100644 index 000000000..5407ee888 --- /dev/null +++ b/lib/workers/create-workers.ts @@ -0,0 +1,15 @@ +import type {EventEmitter} from 'events'; + +type MapOfMethods> = { + [K in T[number]]: (...args: Array) => Promise | unknown; +}; + +type RegisterWorkers> = EventEmitter & MapOfMethods; + +export const createWorkers = ( + runner: {registerWorkers: (workerFilePath: string, exportedMethods: string[]) => RegisterWorkers<['saveDiffTo']>} +): RegisterWorkers<['saveDiffTo']> => { + const workerFilepath = require.resolve('./worker'); + + return runner.registerWorkers(workerFilepath, ['saveDiffTo']); +}; diff --git a/package-lock.json b/package-lock.json index 4a6e2c62c..0d3a60c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "html-reporter", - "version": "9.10.3", + "version": "9.10.3-hello", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "html-reporter", - "version": "9.10.3", + "version": "9.10.3-hello", "license": "MIT", "dependencies": { "@babel/runtime": "^7.22.5", @@ -54,6 +54,7 @@ "@types/nested-error-stacks": "^2.1.0", "@types/opener": "^1.4.0", "@types/tmp": "^0.1.0", + "@types/urijs": "^1.19.19", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "app-module-path": "^2.2.0", @@ -4623,6 +4624,12 @@ "node": ">=0.10.0" } }, + "node_modules/@types/urijs": { + "version": "1.19.19", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.19.tgz", + "integrity": "sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==", + "dev": true + }, "node_modules/@types/webpack": { "version": "4.41.33", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", @@ -35371,6 +35378,12 @@ } } }, + "@types/urijs": { + "version": "1.19.19", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.19.tgz", + "integrity": "sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==", + "dev": true + }, "@types/webpack": { "version": "4.41.33", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", diff --git a/package.json b/package.json index 94a6e52ff..eca5127bb 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@types/nested-error-stacks": "^2.1.0", "@types/opener": "^1.4.0", "@types/tmp": "^0.1.0", + "@types/urijs": "^1.19.19", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "app-module-path": "^2.2.0", diff --git a/test/unit/hermione.js b/test/unit/hermione.js index 156133a8a..409013fc8 100644 --- a/test/unit/hermione.js +++ b/test/unit/hermione.js @@ -35,15 +35,16 @@ describe('lib/hermione', () => { './server-utils': utils }); - const StaticReportBuilder = proxyquire('lib/report-builder/static', { + const {StaticReportBuilder} = proxyquire('lib/report-builder/static', { + 'fs-extra': fs, '../server-utils': utils, '../sqlite-adapter': {SqliteAdapter}, '../test-adapter': {TestAdapter} }); - const PluginAdapter = proxyquire('lib/plugin-adapter', { + const {PluginAdapter} = proxyquire('lib/plugin-adapter', { './server-utils': utils, - './report-builder/static': StaticReportBuilder, + './report-builder/static': {StaticReportBuilder}, './plugin-api': proxyquire('lib/plugin-api', { './local-images-saver': proxyquire('lib/local-images-saver', { './server-utils': utils @@ -52,7 +53,7 @@ describe('lib/hermione', () => { }); const HermioneReporter = proxyquire('../../hermione', { - './lib/plugin-adapter': PluginAdapter + './lib/plugin-adapter': {PluginAdapter} }); const events = { diff --git a/test/unit/lib/config/index.js b/test/unit/lib/config/index.js index 82e980b73..17370f3b9 100644 --- a/test/unit/lib/config/index.js +++ b/test/unit/lib/config/index.js @@ -1,11 +1,11 @@ 'use strict'; const {isEmpty} = require('lodash'); -const parseConfig = require('lib/config'); -const {config: configDefaults} = require('lib/constants/defaults'); +const {parseConfig} = require('lib/config'); +const {configDefaults} = require('lib/constants/defaults'); const {ViewMode} = require('lib/constants/view-modes'); const {DiffModes} = require('lib/constants/diff-modes'); -const saveFormats = require('lib/constants/save-formats'); +const {SaveFormat} = require('lib/constants/save-formats'); const SUPPORTED_CONTROL_TYPES = Object.values(require('lib/gui/constants/custom-gui-control-types')); const {logger} = require('lib/common-utils'); @@ -82,8 +82,8 @@ describe('config', () => { }); describe('"saveFormat" option', () => { - it(`should be ${saveFormats.SQLITE} by default`, () => { - assert.equal(parseConfig({}).saveFormat, saveFormats.SQLITE); + it(`should be ${SaveFormat.SQLITE} by default`, () => { + assert.equal(parseConfig({}).saveFormat, SaveFormat.SQLITE); }); }); diff --git a/test/unit/lib/local-images-saver.js b/test/unit/lib/local-images-saver.js index 9988a949c..6d25e6dd6 100644 --- a/test/unit/lib/local-images-saver.js +++ b/test/unit/lib/local-images-saver.js @@ -14,7 +14,7 @@ describe('local-images-saver', () => { imagesSaver = proxyquire('lib/local-images-saver', { './server-utils': utils - }); + }).LocalImagesSaver; }); afterEach(() => sandbox.restore()); diff --git a/test/unit/lib/plugin-adapter.js b/test/unit/lib/plugin-adapter.js index e09205752..a2dd57201 100644 --- a/test/unit/lib/plugin-adapter.js +++ b/test/unit/lib/plugin-adapter.js @@ -4,10 +4,10 @@ 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 PluginApi = require('lib/plugin-api'); +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'); +const {GUI, MERGE_REPORTS, REMOVE_UNUSED_SCREENS} = require('lib/cli-commands').cliCommands; describe('lib/plugin-adapter', () => { const sandbox = sinon.createSandbox(); @@ -76,11 +76,11 @@ describe('lib/plugin-adapter', () => { cliCommands[REMOVE_UNUSED_SCREENS] = sandbox.stub(); toolReporter = proxyquire('lib/plugin-adapter', { - './config': parseConfig, + './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()); @@ -133,7 +133,7 @@ describe('lib/plugin-adapter', () => { const plugin = toolReporter.create(tool, opts); assert.deepEqual(plugin.addApi(), plugin); - assert.instanceOf(tool.htmlReporter, PluginApi); + assert.instanceOf(tool.htmlReporter, HtmlReporter); }); it(`should not register command if hermione called via API`, () => { diff --git a/test/unit/lib/plugin-api.js b/test/unit/lib/plugin-api.js index 114cca00f..6d303aa81 100644 --- a/test/unit/lib/plugin-api.js +++ b/test/unit/lib/plugin-api.js @@ -1,11 +1,11 @@ 'use strict'; -const PluginApi = require('lib/plugin-api'); -const PluginEvents = require('lib/constants/plugin-events'); +const {HtmlReporter} = require('lib/plugin-api'); +const {PluginEvents} = require('lib/constants/plugin-events'); describe('plugin api', () => { it('should store extra items', () => { - const pluginApi = PluginApi.create(); + const pluginApi = HtmlReporter.create(); pluginApi.addExtraItem('some', 'item'); @@ -13,7 +13,7 @@ describe('plugin api', () => { }); it('should store meta info extenders', () => { - const pluginApi = PluginApi.create(); + const pluginApi = HtmlReporter.create(); pluginApi.addMetaInfoExtender('name', 'value'); @@ -21,7 +21,7 @@ describe('plugin api', () => { }); it('should return all stored values', () => { - const pluginApi = PluginApi.create(); + const pluginApi = HtmlReporter.create(); pluginApi.addExtraItem('key1', 'value1'); pluginApi.addMetaInfoExtender('key2', 'value2'); @@ -38,14 +38,14 @@ describe('plugin api', () => { describe('should provide access to', () => { it('plugin events', () => { - const pluginApi = PluginApi.create(); + const pluginApi = HtmlReporter.create(); assert.deepEqual(pluginApi.events, PluginEvents); }); it('plugin config', () => { const pluginConfig = {path: 'some-path'}; - const pluginApi = PluginApi.create(pluginConfig); + const pluginApi = HtmlReporter.create(pluginConfig); assert.deepEqual(pluginApi.config, pluginConfig); }); diff --git a/test/unit/lib/report-builder/gui.js b/test/unit/lib/report-builder/gui.js index bda41d216..58d6a4ad5 100644 --- a/test/unit/lib/report-builder/gui.js +++ b/test/unit/lib/report-builder/gui.js @@ -7,7 +7,7 @@ const serverUtils = require('lib/server-utils'); const {TestAdapter} = require('lib/test-adapter'); const {SqliteAdapter} = require('lib/sqlite-adapter'); const GuiTestsTreeBuilder = require('lib/tests-tree-builder/gui'); -const PluginApi = require('lib/plugin-api'); +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 {mkFormattedTest} = require('../../utils'); @@ -26,7 +26,7 @@ describe('GuiReportBuilder', () => { const browserConfigStub = {getAbsoluteUrl: toolConfig.getAbsoluteUrl}; const hermione = { forBrowser: sandbox.stub().returns(browserConfigStub), - htmlReporter: PluginApi.create() + htmlReporter: HtmlReporter.create() }; TestAdapter.create = (obj) => obj; diff --git a/test/unit/lib/report-builder/static.js b/test/unit/lib/report-builder/static.js index 37856f936..04e98f13d 100644 --- a/test/unit/lib/report-builder/static.js +++ b/test/unit/lib/report-builder/static.js @@ -1,10 +1,10 @@ 'use strict'; -const fs = require('fs-extra'); +const fsOriginal = require('fs-extra'); const _ = require('lodash'); const Database = require('better-sqlite3'); const proxyquire = require('proxyquire'); -const PluginApi = require('lib/plugin-api'); +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 {mkFormattedTest} = require('../../utils'); @@ -14,7 +14,14 @@ const TEST_DB_PATH = `${TEST_REPORT_PATH}/${LOCAL_DATABASE_NAME}`; describe('StaticReportBuilder', () => { const sandbox = sinon.sandbox.create(); - let hasImage, StaticReportBuilder, hermione; + let StaticReportBuilder, hermione; + + const fs = _.clone(fsOriginal); + + const originalUtils = proxyquire('lib/server-utils', { + 'fs-extra': fs + }); + const utils = _.clone(originalUtils); const mkStaticReportBuilder_ = async ({toolConfig = {}, pluginConfig} = {}) => { toolConfig = _.defaults(toolConfig, {getAbsoluteUrl: _.noop}); @@ -24,7 +31,7 @@ describe('StaticReportBuilder', () => { hermione = { forBrowser: sandbox.stub().returns(browserConfigStub), on: sandbox.spy(), - htmlReporter: _.extend(PluginApi.create(), { + htmlReporter: _.extend(HtmlReporter.create(), { reportsSaver: { saveReportData: sandbox.stub() } @@ -59,11 +66,12 @@ describe('StaticReportBuilder', () => { }; beforeEach(() => { - hasImage = sandbox.stub().returns(true); + sandbox.stub(utils, 'hasImage').returns(true); StaticReportBuilder = proxyquire('lib/report-builder/static', { - '../server-utils': {hasImage} - }); + 'fs-extra': fs, + '../server-utils': utils + }).StaticReportBuilder; }); afterEach(() => { diff --git a/test/unit/lib/sqlite-adapter.js b/test/unit/lib/sqlite-adapter.js index 2df7e1c82..3bf15e344 100644 --- a/test/unit/lib/sqlite-adapter.js +++ b/test/unit/lib/sqlite-adapter.js @@ -5,7 +5,7 @@ const proxyquire = require('proxyquire'); const Database = require('better-sqlite3'); const {SqliteAdapter} = require('lib/sqlite-adapter'); -const PluginApi = require('lib/plugin-api'); +const {HtmlReporter} = require('lib/plugin-api'); describe('lib/sqlite-adapter', () => { const sandbox = sinon.createSandbox(); @@ -18,7 +18,7 @@ describe('lib/sqlite-adapter', () => { }; beforeEach(() => { - hermione = {htmlReporter: PluginApi.create()}; + hermione = {htmlReporter: HtmlReporter.create()}; }); afterEach(() => { @@ -78,7 +78,7 @@ describe('lib/sqlite-adapter', () => { getStub = sandbox.stub(); prepareStub = sandbox.stub(Database.prototype, 'prepare').returns({get: getStub}); sqliteAdapter = proxyquire('lib/sqlite-adapter', { - './db-utils/server': {createTablesQuery: () => []} + './db-utils/common': {createTablesQuery: () => []} }).SqliteAdapter.create({hermione, reportPath: 'test'}); await sqliteAdapter.init(); @@ -146,7 +146,7 @@ describe('lib/sqlite-adapter', () => { runStub = sandbox.stub(); prepareStub = sandbox.stub(Database.prototype, 'prepare').returns({run: runStub}); sqliteAdapter = proxyquire('lib/sqlite-adapter', { - './db-utils/server': {createTablesQuery: () => []} + './db-utils/common': {createTablesQuery: () => []} }).SqliteAdapter.create({hermione, reportPath: 'test'}); await sqliteAdapter.init(); diff --git a/test/unit/lib/static/modules/actions.js b/test/unit/lib/static/modules/actions.js index 3e7f76281..b9a6526d0 100644 --- a/test/unit/lib/static/modules/actions.js +++ b/test/unit/lib/static/modules/actions.js @@ -3,7 +3,7 @@ import proxyquire from 'proxyquire'; import {POSITIONS} from 'reapop'; import {acceptOpened, undoAcceptImages, retryTest, runFailedTests} from 'lib/static/modules/actions'; import actionNames from 'lib/static/modules/action-names'; -import StaticTestsTreeBuilder from 'lib/tests-tree-builder/static'; +import {StaticTestsTreeBuilder} from 'lib/tests-tree-builder/static'; import {LOCAL_DATABASE_NAME} from 'lib/constants/database'; import {DiffModes} from 'lib/constants/diff-modes'; diff --git a/test/unit/lib/suite-adapter.js b/test/unit/lib/suite-adapter.js index a5843b6b8..0b1138dab 100644 --- a/test/unit/lib/suite-adapter.js +++ b/test/unit/lib/suite-adapter.js @@ -1,6 +1,6 @@ 'use strict'; -const SuiteAdapter = require('lib/suite-adapter'); +const {SuiteAdapter} = require('lib/suite-adapter'); describe('suite adapter', () => { it('should return suite skip reason', () => { diff --git a/test/unit/lib/tests-tree-builder/base.js b/test/unit/lib/tests-tree-builder/base.js index 5dd11291b..ac6fa30d3 100644 --- a/test/unit/lib/tests-tree-builder/base.js +++ b/test/unit/lib/tests-tree-builder/base.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const proxyquire = require('proxyquire'); const {FAIL, ERROR, SUCCESS} = require('lib/constants/test-statuses'); -const {versions: browserVersions} = require('lib/constants/browser'); +const {BrowserVersions} = require('lib/constants/browser'); describe('ResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); @@ -25,7 +25,7 @@ describe('ResultsTreeBuilder', () => { determineStatus = sandbox.stub().returns(SUCCESS); ResultsTreeBuilder = proxyquire('lib/tests-tree-builder/base', { '../common-utils': {determineStatus} - }); + }).BaseTestsTreeBuilder; builder = ResultsTreeBuilder.create(); }); @@ -132,7 +132,7 @@ describe('ResultsTreeBuilder', () => { name: 'b1', parentId: 's1', resultIds: ['s1 b1 0'], - version: browserVersions.UNKNOWN + version: BrowserVersions.UNKNOWN } ); }); diff --git a/test/unit/lib/tests-tree-builder/static.js b/test/unit/lib/tests-tree-builder/static.js index ccc6b84b7..d80978ad3 100644 --- a/test/unit/lib/tests-tree-builder/static.js +++ b/test/unit/lib/tests-tree-builder/static.js @@ -1,9 +1,9 @@ 'use strict'; const _ = require('lodash'); -const StaticResultsTreeBuilder = require('lib/tests-tree-builder/static'); +const {StaticTestsTreeBuilder} = require('lib/tests-tree-builder/static'); const {SUCCESS} = require('lib/constants/test-statuses'); -const {versions: browserVersions} = require('lib/constants/browser'); +const {BrowserVersions} = require('lib/constants/browser'); describe('StaticResultsTreeBuilder', () => { const sandbox = sinon.sandbox.create(); @@ -65,10 +65,10 @@ describe('StaticResultsTreeBuilder', () => { }; beforeEach(() => { - sandbox.stub(StaticResultsTreeBuilder.prototype, 'addTestResult'); - sandbox.stub(StaticResultsTreeBuilder.prototype, 'sortTree'); + sandbox.stub(StaticTestsTreeBuilder.prototype, 'addTestResult'); + sandbox.stub(StaticTestsTreeBuilder.prototype, 'sortTree'); - builder = StaticResultsTreeBuilder.create(); + builder = StaticTestsTreeBuilder.create(); }); afterEach(() => sandbox.restore()); @@ -83,12 +83,12 @@ describe('StaticResultsTreeBuilder', () => { builder.build(rows); assert.calledWith( - StaticResultsTreeBuilder.prototype.addTestResult.firstCall, + StaticTestsTreeBuilder.prototype.addTestResult.firstCall, formatToTestResult(dataFromDb1, {attempt: 0}), {browserId: 'yabro', testPath: ['s1'], attempt: 0} ); assert.calledWith( - StaticResultsTreeBuilder.prototype.addTestResult.secondCall, + StaticTestsTreeBuilder.prototype.addTestResult.secondCall, formatToTestResult(dataFromDb2, {attempt: 0}), {browserId: 'yabro', testPath: ['s2'], attempt: 0} ); @@ -102,12 +102,12 @@ describe('StaticResultsTreeBuilder', () => { builder.build(rows); assert.calledWith( - StaticResultsTreeBuilder.prototype.addTestResult.firstCall, + StaticTestsTreeBuilder.prototype.addTestResult.firstCall, formatToTestResult(dataFromDb1, {attempt: 0}), {browserId: 'yabro', testPath: ['s1'], attempt: 0} ); assert.calledWith( - StaticResultsTreeBuilder.prototype.addTestResult.secondCall, + StaticTestsTreeBuilder.prototype.addTestResult.secondCall, formatToTestResult(dataFromDb1, {attempt: 1}), {browserId: 'yabro', testPath: ['s1'], attempt: 1} ); @@ -121,8 +121,8 @@ describe('StaticResultsTreeBuilder', () => { builder.build(rows); assert.callOrder( - StaticResultsTreeBuilder.prototype.addTestResult, - StaticResultsTreeBuilder.prototype.sortTree + StaticTestsTreeBuilder.prototype.addTestResult, + StaticTestsTreeBuilder.prototype.sortTree ); }); @@ -131,12 +131,12 @@ describe('StaticResultsTreeBuilder', () => { builder.build(rows); - assert.calledOnce(StaticResultsTreeBuilder.prototype.sortTree); + assert.calledOnce(StaticTestsTreeBuilder.prototype.sortTree); }); }); it('should return tests tree', () => { - sandbox.stub(StaticResultsTreeBuilder.prototype, 'tree').get(() => 'tree'); + sandbox.stub(StaticTestsTreeBuilder.prototype, 'tree').get(() => 'tree'); const {tree} = builder.build([]); @@ -150,7 +150,7 @@ describe('StaticResultsTreeBuilder', () => { const {browsers} = builder.build(rows); - assert.deepEqual(browsers, [{id: 'yabro', versions: [browserVersions.UNKNOWN]}]); + assert.deepEqual(browsers, [{id: 'yabro', versions: [BrowserVersions.UNKNOWN]}]); }); it('with a few versions for the same browser', () => {