Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to run cli commands using html-reporter binary #551

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bin/html-reporter
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
'use strict';

(async () => {
await require('../build/lib/cli').run();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is async because I need to import cli commands in runtime and register them

})();
48 changes: 48 additions & 0 deletions lib/adapters/tool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {Config, TestCollection} from 'testplane';
import type {CommanderStatic} from '@gemini-testing/commander';

import {GuiApi} from '../../gui/api';
import {EventSource} from '../../gui/event-source';
import {GuiReportBuilder} from '../../report-builder/gui';
import {ToolName} from '../../constants';

import type {ReporterConfig, ImageFile} from '../../types';
import type {TestSpec} from './types';

export interface ToolAdapterOptionsFromCli {
toolName: ToolName;
configPath?: string;
}

export interface UpdateReferenceOpts {
refImg: ImageFile;
state: string;
}

export interface ToolAdapter {
readonly toolName: ToolName;
readonly config: Config;
readonly reporterConfig: ReporterConfig;
readonly guiApi?: GuiApi;

initGuiApi(): void;
readTests(paths: string[], cliTool: CommanderStatic): Promise<TestCollection>;
run(testCollection: TestCollection, tests: TestSpec[], cliTool: CommanderStatic): Promise<boolean>;

updateReference(opts: UpdateReferenceOpts): void;
handleTestResults(reportBuilder: GuiReportBuilder, eventSource: EventSource): void;

halt(err: Error, timeout: number): void;
}

export const makeToolAdapter = async (opts: ToolAdapterOptionsFromCli): Promise<ToolAdapter> => {
if (opts.toolName === ToolName.Testplane) {
const {TestplaneToolAdapter} = await import('./testplane');

return TestplaneToolAdapter.create(opts);
} else if (opts.toolName === ToolName.Playwright) {
throw new Error('Playwright is not supported yet');
} else {
throw new Error(`Tool adapter with name: "${opts.toolName}" is not supported`);
}
};
171 changes: 171 additions & 0 deletions lib/adapters/tool/testplane/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import _ from 'lodash';
import Testplane, {type Config, type TestCollection} from 'testplane';
import type {CommanderStatic} from '@gemini-testing/commander';

import {GuiApi} from '../../../gui/api';
import {parseConfig} from '../../../config';
import {HtmlReporter} from '../../../plugin-api';
import {ApiFacade} from '../../../gui/api/facade';
import {createTestRunner} from './runner';
import {EventSource} from '../../../gui/event-source';
import {GuiReportBuilder} from '../../../report-builder/gui';
import {handleTestResults} from './test-results-handler';
import {ToolName} from '../../../constants';

import type {ToolAdapter, ToolAdapterOptionsFromCli, UpdateReferenceOpts} from '../index';
import type {TestSpec, CustomGuiActionPayload} from '../types';
import type {ReporterConfig, CustomGuiItem} from '../../../types';

type HtmlReporterApi = {
gui: ApiFacade;
htmlReporter: HtmlReporter;
};
type TestplaneWithHtmlReporter = Testplane & HtmlReporterApi;

interface ReplModeOption {
enabled: boolean;
beforeTest: boolean;
onFail: boolean;
}

interface OptionsFromPlugin {
toolName: ToolName.Testplane;
tool: Testplane;
reporterConfig: ReporterConfig;
}

type Options = ToolAdapterOptionsFromCli | OptionsFromPlugin;

export class TestplaneToolAdapter implements ToolAdapter {
private _toolName: ToolName;
private _tool: TestplaneWithHtmlReporter;
private _reporterConfig: ReporterConfig;
private _htmlReporter: HtmlReporter;
private _guiApi?: GuiApi;

static create<TestplaneToolAdapter>(
this: new (options: Options) => TestplaneToolAdapter,
options: Options
): TestplaneToolAdapter {
return new this(options);
}

constructor(opts: Options) {
if ('tool' in opts) {
this._tool = opts.tool as TestplaneWithHtmlReporter;
this._reporterConfig = opts.reporterConfig;
} else {
// in order to not use static report with gui simultaneously
process.env['html_reporter_enabled'] = false.toString();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to disable html-reporter plugin when it is called from html-reporter binary, because in this case I will have gui report and static report in the same time.

this._tool = Testplane.create(opts.configPath) as TestplaneWithHtmlReporter;

const pluginOpts = getPluginOptions(this._tool.config);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I need to get options of html-reporter from parsed testplane config.

this._reporterConfig = parseConfig(pluginOpts);
}

this._toolName = opts.toolName;
this._htmlReporter = HtmlReporter.create(this._reporterConfig, {toolName: ToolName.Testplane});

// in order to be able to use it from other plugins as an API
this._tool.htmlReporter = this._htmlReporter;
}

get toolName(): ToolName {
return this._toolName;
}

get config(): Config {
return this._tool.config;
}

get reporterConfig(): ReporterConfig {
return this._reporterConfig;
}

get htmlReporter(): HtmlReporter {
return this._htmlReporter;
}

get guiApi(): GuiApi | undefined {
return this._guiApi;
}

initGuiApi(): void {
this._guiApi = GuiApi.create();

// in order to be able to use it from other plugins as an API
this._tool.gui = this._guiApi.gui;
}

async readTests(paths: string[], cliTool: CommanderStatic): Promise<TestCollection> {
const {grep, set: sets, browser: browsers} = cliTool;
const replMode = getReplModeOption(cliTool);

return this._tool.readTests(paths, {grep, sets, browsers, replMode});
}

async run(testCollection: TestCollection, tests: TestSpec[] = [], cliTool: CommanderStatic): Promise<boolean> {
const {grep, set: sets, browser: browsers, devtools = false} = cliTool;
const replMode = getReplModeOption(cliTool);
const runner = createTestRunner(testCollection, tests);

return runner.run((collection) => this._tool.run(collection, {grep, sets, browsers, devtools, replMode}));
}

updateReference(opts: UpdateReferenceOpts): void {
this._tool.emit(this._tool.events.UPDATE_REFERENCE, opts);
}

handleTestResults(reportBuilder: GuiReportBuilder, eventSource: EventSource): void {
handleTestResults(this._tool, reportBuilder, eventSource);
}

halt(err: Error, timeout: number): void {
this._tool.halt(err, timeout);
}

async initGuiHandler(): Promise<void> {
const {customGui} = this._reporterConfig;

await Promise.all(
_(customGui)
.flatMap<CustomGuiItem>(_.identity)
.map((ctx) => ctx.initialize?.({testplane: this._tool, hermione: this._tool, ctx}))
.value()
);
}

async runCustomGuiAction(payload: CustomGuiActionPayload): Promise<void> {
const {customGui} = this._reporterConfig;

const {sectionName, groupIndex, controlIndex} = payload;
const ctx = customGui[sectionName][groupIndex];
const control = ctx.controls[controlIndex];

await ctx.action({testplane: this._tool, hermione: this._tool, control, ctx});
}
}

function getPluginOptions(config: Config): Partial<ReporterConfig> {
const defaultOpts = {};

for (const toolName of [ToolName.Testplane, 'hermione']) {
const opts = _.get(config.plugins, `html-reporter/${toolName}`, defaultOpts);

if (!_.isEmpty(opts)) {
return opts;
}
}

return defaultOpts;
}

function getReplModeOption(cliTool: CommanderStatic): ReplModeOption {
const {repl = false, replBeforeTest = false, replOnFail = false} = cliTool;

return {
enabled: repl || replBeforeTest || replOnFail,
beforeTest: replBeforeTest,
onFail: replOnFail
};
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import _ from 'lodash';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All testplane runners moved to testplane folder. They cannot be used for pwt (for example)

import type {TestCollection} from 'testplane';

import {TestRunner, TestSpec} from './runner';
import {AllTestRunner} from './all-test-runner';
import {SpecificTestRunner} from './specific-test-runner';
import type {TestRunner} from './runner';
import type {TestSpec} from '../../types';

export const createTestRunner = (collection: TestCollection, tests: TestSpec[]): TestRunner => {
return _.isEmpty(tests)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ export interface TestRunner {
run<U>(handler: (testCollection: TestCollection) => U): U;
}

export interface TestSpec {
testName: string;
browserName: string;
}

export class BaseRunner implements TestRunner {
protected _collection: TestCollection;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {TestCollection} from 'testplane';
import {BaseRunner, TestSpec} from './runner';
import {BaseRunner} from './runner';
import type {TestSpec} from '../../types';

export class SpecificTestRunner extends BaseRunner {
private _tests: TestSpec[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import os from 'os';
import PQueue from 'p-queue';
import type Testplane from 'testplane';
import type {Test as TestplaneTest} from 'testplane';
import {ClientEvents} from '../constants';
import {getSuitePath} from '../../plugin-utils';
import {createWorkers, CreateWorkersRunner} from '../../workers/create-workers';
import {logError, formatTestResult} from '../../server-utils';
import {TestStatus} from '../../constants';
import {GuiReportBuilder} from '../../report-builder/gui';
import {EventSource} from '../event-source';
import {TestplaneTestResult} from '../../types';
import {getStatus} from '../../test-adapter/testplane';

export const subscribeOnToolEvents = (testplane: Testplane, reportBuilder: GuiReportBuilder, client: EventSource): void => {
import {ClientEvents} from '../../../gui/constants';
import {getSuitePath} from '../../../plugin-utils';
import {createWorkers, CreateWorkersRunner} from '../../../workers/create-workers';
import {logError, formatTestResult} from '../../../server-utils';
import {TestStatus} from '../../../constants';
import {GuiReportBuilder} from '../../../report-builder/gui';
import {EventSource} from '../../../gui/event-source';
import {TestplaneTestResult} from '../../../types';
import {getStatus} from '../../../test-adapter/testplane';

export const handleTestResults = (testplane: Testplane, reportBuilder: GuiReportBuilder, client: EventSource): void => {
const queue = new PQueue({concurrency: os.cpus().length});

testplane.on(testplane.events.RUNNER_START, (runner) => {
Expand Down
10 changes: 10 additions & 0 deletions lib/adapters/tool/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface TestSpec {
testName: string;
browserName: string;
}

export interface CustomGuiActionPayload {
sectionName: string;
groupIndex: number;
controlIndex: number;
}
5 changes: 0 additions & 5 deletions lib/cli-commands/index.ts

This file was deleted.

17 changes: 8 additions & 9 deletions lib/cli-commands/gui.js → lib/cli/commands/gui.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
'use strict';

const {cliCommands} = require('.');
const runGui = require('../gui').default;
const {Api} = require('../gui/api');
const {commands} = require('..');
const runGui = require('../../gui').default;

const {GUI: commandName} = cliCommands;
const {GUI: commandName} = commands;

module.exports = (program, pluginConfig, testplane) => {
// must be executed here because it adds `gui` field in `gemini`, `testplane` and `hermione tool`,
module.exports = (cliTool, toolAdapter) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All cli commands now works with toolAdapter instance.

// must be executed here because it adds `gui` field in tool instance,
// which is available to other plugins and is an API for interacting with the current plugin
const guiApi = Api.create(testplane);
toolAdapter.initGuiApi();

program
cliTool
.command(`${commandName} [paths...]`)
.allowUnknownOption()
.description('update the changed screenshots or gather them if they does not exist')
Expand All @@ -20,6 +19,6 @@ module.exports = (program, pluginConfig, testplane) => {
.option('-a, --auto-run', 'auto run immediately')
.option('-O, --no-open', 'not to open a browser window after starting the server')
.action((paths, options) => {
runGui({paths, testplane, guiApi, configs: {options, program, pluginConfig}});
runGui({paths, toolAdapter, cli: {options, tool: cliTool}});
});
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
'use strict';

const {cliCommands} = require('.');
const mergeReports = require('../merge-reports');
const {logError} = require('../server-utils');
const {commands} = require('..');
const mergeReports = require('../../merge-reports');
const {logError} = require('../../server-utils');

const {MERGE_REPORTS: commandName} = cliCommands;
const {MERGE_REPORTS: commandName} = commands;

module.exports = (program, pluginConfig, testplane) => {
module.exports = (program, toolAdapter) => {
program
.command(`${commandName} [paths...]`)
.allowUnknownOption()
.description('merge reports')
.option('-d, --destination <destination>', 'path to directory with merged report', pluginConfig.path)
.option('-d, --destination <destination>', 'path to directory with merged report', toolAdapter.reporterConfig.path)
.option('-h, --header <header>', 'http header for databaseUrls.json files from source paths', collect, [])
.action(async (paths, options) => {
try {
const {destination: destPath, header: headers} = options;

await mergeReports(pluginConfig, testplane, paths, {destPath, headers});
await mergeReports(toolAdapter, paths, {destPath, headers});
} catch (err) {
logError(err);
process.exit(1);
Expand Down
Loading
Loading