From 333fa65a2c7a03010e298f42e08818acc84d25e8 Mon Sep 17 00:00:00 2001 From: rocketraccoon Date: Thu, 27 Nov 2025 10:49:42 +0700 Subject: [PATCH 1/3] feat: add tags --- src/browser/existing-browser.ts | 13 +++++++ src/browser/history/index.ts | 4 ++- src/browser/types.ts | 2 ++ src/cli/commands/list-tests/index.ts | 3 +- src/cli/index.ts | 2 ++ src/index.ts | 1 + src/runner/test-runner/regular-test-runner.js | 6 +++- src/test-reader/index.ts | 4 +-- src/test-reader/mocha-reader/index.js | 36 +++++++++++++++++++ .../mocha-reader/tree-builder-decorator.js | 8 ++--- src/test-reader/test-object/suite.ts | 27 ++++++++++++-- src/test-reader/test-object/test.ts | 27 ++++++++++++-- src/test-reader/test-object/types.ts | 16 +++++++++ src/test-reader/test-parser.ts | 22 ++++++++++-- src/testplane.ts | 21 +++++++++-- src/types/index.ts | 2 +- src/utils/cli.ts | 36 ++++++++++++++++++- src/worker/runner/test-runner/index.js | 4 ++- src/worker/runner/test-runner/types.ts | 1 + src/worker/testplane.ts | 1 + test/src/test-reader/index.js | 2 ++ test/src/testplane.js | 2 ++ .../browser-env/runner/test-runner/index.ts | 2 +- 23 files changed, 219 insertions(+), 23 deletions(-) diff --git a/src/browser/existing-browser.ts b/src/browser/existing-browser.ts index b9b75ca1f..d4115485d 100644 --- a/src/browser/existing-browser.ts +++ b/src/browser/existing-browser.ts @@ -91,6 +91,7 @@ export class ExistingBrowser extends Browser { protected _calibration?: CalibrationResult; protected _clientBridge?: ClientBridge; protected _cdp: CDP | null = null; + protected _tag: Set = new Set(); constructor(config: Config, opts: BrowserOpts) { super(config, opts); @@ -328,6 +329,14 @@ export class ExistingBrowser extends Browser { protected _addMetaAccessCommands(session: WebdriverIO.Browser): void { session.addCommand("setMeta", (key, value) => (this._meta[key] = value)); session.addCommand("getMeta", key => (key ? this._meta[key] : this._meta)); + + session.addCommand("addTag", (tag: string | string[]) => { + if (Array.isArray(tag)) { + tag.forEach(element => this._tag?.add(element)); + } else { + this._tag?.add(tag); + } + }); } protected _decorateUrlMethod(session: WebdriverIO.Browser): void { @@ -584,6 +593,10 @@ export class ExistingBrowser extends Browser { return this._meta; } + get tag(): string[] { + return [...this._tag]; + } + get cdp(): CDP | null { return this._cdp; } diff --git a/src/browser/history/index.ts b/src/browser/history/index.ts index f9b3339b1..b4e801368 100644 --- a/src/browser/history/index.ts +++ b/src/browser/history/index.ts @@ -23,7 +23,9 @@ export interface PromiseRef { } const shouldNotWrapCommand = (commandName: string): boolean => - ["addCommand", "overwriteCommand", "extendOptions", "setMeta", "getMeta", "runStep"].includes(commandName); + ["addCommand", "overwriteCommand", "extendOptions", "addTag", "setMeta", "getMeta", "runStep"].includes( + commandName, + ); export const shouldPropagateFn = (parentNode: TestStep, currentNode: TestStep): boolean => isGroup(parentNode) || isGroup(currentNode); diff --git a/src/browser/types.ts b/src/browser/types.ts index 3de3ecde0..3760a96fe 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -75,6 +75,8 @@ declare global { setMeta(this: WebdriverIO.Browser, key: string, value: unknown): Promise; + addTag(this: WebdriverIO.Browser, tag: string | string[]): Promise; + extendOptions(this: WebdriverIO.Browser, opts: { [name: string]: unknown }): Promise; getConfig(this: WebdriverIO.Browser): Promise; diff --git a/src/cli/commands/list-tests/index.ts b/src/cli/commands/list-tests/index.ts index d91287555..c49afb099 100644 --- a/src/cli/commands/list-tests/index.ts +++ b/src/cli/commands/list-tests/index.ts @@ -30,7 +30,7 @@ export const registerCmd = (cliTool: ListTestsCmd, testplane: Testplane): void = .option("--formatter [name]", "return tests in specified format", String, Formatters.LIST) .arguments("[paths...]") .action(async (paths: string[], options: ListTestsCmdOpts) => { - const { grep, browser: browsers, set: sets } = cliTool; + const { grep, tag, browser: browsers, set: sets } = cliTool; const { ignore, silent, outputFile, formatter } = options; try { @@ -40,6 +40,7 @@ export const registerCmd = (cliTool: ListTestsCmd, testplane: Testplane): void = browsers, sets, grep, + tag, ignore, silent, runnableOpts: { diff --git a/src/cli/index.ts b/src/cli/index.ts index 73ac4220a..fad1a5de2 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -87,6 +87,7 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise => { browser: browsers, set: sets, grep, + tag, updateRefs, inspect, inspectBrk, @@ -104,6 +105,7 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise => { browsers, sets, grep, + tag, updateRefs, requireModules, inspectMode: (inspect || inspectBrk) && { inspect, inspectBrk }, diff --git a/src/index.ts b/src/index.ts index 42d0b2cb6..73ac35d28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ export type { TestFunctionCtx, ExecutionThreadCtx, Cookie, + TestTag, } from "./types"; export type { Config } from "./config"; export { TimeTravelMode } from "./config"; diff --git a/src/runner/test-runner/regular-test-runner.js b/src/runner/test-runner/regular-test-runner.js index 0e34cde1e..428b0af80 100644 --- a/src/runner/test-runner/regular-test-runner.js +++ b/src/runner/test-runner/regular-test-runner.js @@ -71,7 +71,7 @@ module.exports = class RegularTestRunner extends RunnableEmitter { }); } - _applyTestResults({ meta, testplaneCtx = {}, history = [] }) { + _applyTestResults({ tag, meta, testplaneCtx = {}, history = [] }) { testplaneCtx.assertViewResults = AssertViewResults.fromRawObject(testplaneCtx.assertViewResults || []); this._test.assertViewResults = testplaneCtx.assertViewResults.get(); @@ -80,6 +80,10 @@ module.exports = class RegularTestRunner extends RunnableEmitter { this._test.hermioneCtx = testplaneCtx; this._test.history = history; + if (tag) { + tag.forEach(tag => this._test.addTag(tag)); + } + this._test.duration = Date.now() - this._test.startTime; } diff --git a/src/test-reader/index.ts b/src/test-reader/index.ts index 2e7928d69..efa5dbbde 100644 --- a/src/test-reader/index.ts +++ b/src/test-reader/index.ts @@ -29,7 +29,7 @@ export class TestReader extends EventEmitter { } async read(options: TestReaderOpts): Promise> { - const { paths, browsers, ignore, sets, grep, runnableOpts } = options; + const { paths, browsers, ignore, sets, grep, tag, runnableOpts } = options; const { fileExtensions } = this.#config.system; const envSets = env.parseCommaSeparatedValue(["TESTPLANE_SETS", "HERMIONE_SETS"]).value; @@ -46,7 +46,7 @@ export class TestReader extends EventEmitter { const filesByBro = setCollection.groupByBrowser(); const testsByBro = _.mapValues(filesByBro, (files, browserId) => - parser.parse(files, { browserId, config: this.#config.forBrowser(browserId), grep }), + parser.parse(files, { browserId, config: this.#config.forBrowser(browserId), grep, tag }), ); validateTests(testsByBro, options, this.#config); diff --git a/src/test-reader/mocha-reader/index.js b/src/test-reader/mocha-reader/index.js index 6e5f10c31..486ffa670 100644 --- a/src/test-reader/mocha-reader/index.js +++ b/src/test-reader/mocha-reader/index.js @@ -10,8 +10,44 @@ const { MasterEvents } = require("../../events"); const { getMethodsByInterface } = require("./utils"); const { enableSourceMaps } = require("../../utils/typescript"); +function getTagParser(original) { + return function (title, paramsOrFn, fn) { + if (typeof paramsOrFn === "function") { + return original.call(this, title, paramsOrFn); + } else { + const test = original.call(this, title, fn); + + if (paramsOrFn?.tag) { + if (Array.isArray(paramsOrFn.tag)) { + test.tag = paramsOrFn.tag.map(title => ({ title, dynamic: false })); + } else { + test.tag = [{ title: paramsOrFn.tag, dynamic: false }]; + } + } + + return test; + } + }; +} + async function readFiles(files, { esmDecorator, config, eventBus, runnableOpts }) { const mocha = new Mocha(config); + + mocha.suite.on("pre-require", context => { + const originalDescribe = context.describe; + const originalIt = context.it; + + context.describe = context.context = getTagParser(originalDescribe); + + context.describe.only = originalDescribe.only; + context.describe.skip = originalDescribe.skip; + + context.it = context.specify = getTagParser(originalIt); + + context.it.only = originalIt.only; + context.it.skip = originalIt.skip; + }); + mocha.fullTrace(); initBuildContext(eventBus); diff --git a/src/test-reader/mocha-reader/tree-builder-decorator.js b/src/test-reader/mocha-reader/tree-builder-decorator.js index 1cdd14b67..2894fa4cf 100644 --- a/src/test-reader/mocha-reader/tree-builder-decorator.js +++ b/src/test-reader/mocha-reader/tree-builder-decorator.js @@ -18,12 +18,12 @@ class TreeBuilderDecorator { } addSuite(mochaSuite) { - const { id: mochaId } = mochaSuite; + const { id: mochaId, tag } = mochaSuite; const file = computeFile(mochaSuite) ?? "unknown-file"; const positionInFile = this.#suiteCounter.get(file) || 0; const id = mochaSuite.root ? mochaId : crypto.getShortMD5(file) + positionInFile; - const suite = this.#mkTestObject(Suite, mochaSuite, { id }); + const suite = this.#mkTestObject(Suite, mochaSuite, { id, tag }); this.#applyConfig(suite, mochaSuite); this.#treeBuilder.addSuite(suite, this.#getParent(mochaSuite, null)); @@ -34,9 +34,9 @@ class TreeBuilderDecorator { } addTest(mochaTest) { - const { fn } = mochaTest; + const { fn, tag } = mochaTest; const id = crypto.getShortMD5(mochaTest.fullTitle()); - const test = this.#mkTestObject(Test, mochaTest, { id, fn }); + const test = this.#mkTestObject(Test, mochaTest, { id, fn, tag }); this.#applyConfig(test, mochaTest); this.#treeBuilder.addTest(test, this.#getParent(mochaTest)); diff --git a/src/test-reader/test-object/suite.ts b/src/test-reader/test-object/suite.ts index 78b570bd9..36a636251 100644 --- a/src/test-reader/test-object/suite.ts +++ b/src/test-reader/test-object/suite.ts @@ -2,30 +2,51 @@ import _ from "lodash"; import { ConfigurableTestObject } from "./configurable-test-object"; import { Hook } from "./hook"; import { Test } from "./test"; -import type { TestObjectData, ConfigurableTestObjectData, TestFunction, TestFunctionCtx } from "./types"; +import type { TestObjectData, ConfigurableTestObjectData, TestFunction, TestFunctionCtx, TestTag } from "./types"; -type SuiteOpts = Pick & TestObjectData; +type SuiteOpts = Pick & TestObjectData & { tag: TestTag[] }; export class Suite extends ConfigurableTestObject { #suites: this[]; #tests: Test[]; #beforeEachHooks: Hook[]; #afterEachHooks: Hook[]; + public tag: Map; static create(this: new (opts: SuiteOpts) => T, opts: SuiteOpts): T { return new this(opts); } // used inside test - constructor({ title, file, id, location }: SuiteOpts = {} as SuiteOpts) { + constructor({ title, file, id, location, tag }: SuiteOpts = {} as SuiteOpts) { super({ title, file, id, location }); + this.tag = new Map(tag?.map(({ title, dynamic }) => [title, !!dynamic]) || []); this.#suites = []; this.#tests = []; this.#beforeEachHooks = []; this.#afterEachHooks = []; } + addTag(tag: string | string[], dynamic = false): void { + if (Array.isArray(tag)) { + tag.forEach(element => this.tag.set(element, dynamic)); + } else { + this.tag.set(tag, dynamic); + } + } + + hasTag(tag: string): boolean { + return this.tag.has(tag); + } + + getTag(): TestTag[] { + return Array.from(this.tag.keys()).map(title => ({ + title, + dynamic: this.tag.get(title), + })); + } + addSuite(suite: Suite): this { return this.#addChild(suite, this.#suites); } diff --git a/src/test-reader/test-object/test.ts b/src/test-reader/test-object/test.ts index 6641d4194..c9fb2b5ef 100644 --- a/src/test-reader/test-object/test.ts +++ b/src/test-reader/test-object/test.ts @@ -1,28 +1,51 @@ import { ConfigurableTestObject } from "./configurable-test-object"; -import type { TestObjectData, TestFunction, TestFunctionCtx } from "./types"; +import type { TestObjectData, TestFunction, TestFunctionCtx, TestTag } from "./types"; type TestOpts = TestObjectData & Pick & { fn: TestFunction; + tag?: TestTag[]; }; export class Test extends ConfigurableTestObject { public fn: TestFunction; + public tag: Map; public err?: Error; static create(this: new (opts: TestOpts) => T, opts: TestOpts): T { return new this(opts); } - constructor({ title, file, id, location, fn }: TestOpts) { + constructor({ title, file, id, location, fn, tag }: TestOpts) { super({ title, file, id, location }); this.fn = fn; + this.tag = new Map(tag?.map(({ title, dynamic }) => [title, !!dynamic]) || []); + } + + addTag(tag: string | string[]): void { + if (Array.isArray(tag)) { + tag.forEach(element => this.tag.set(element, true)); + } else { + this.tag.set(tag, true); + } + } + + hasTag(tag: string): boolean { + return this.tag.has(tag); + } + + getTag(): TestTag[] { + return Array.from(this.tag.keys()).map(title => ({ + title, + dynamic: this.tag.get(title), + })); } clone(): Test { return new Test({ title: this.title, + tag: this.getTag(), file: this.file, id: this.id, location: this.location, diff --git a/src/test-reader/test-object/types.ts b/src/test-reader/test-object/types.ts index 1c82f056a..7d15f7e28 100644 --- a/src/test-reader/test-object/types.ts +++ b/src/test-reader/test-object/types.ts @@ -35,8 +35,17 @@ export interface TestHookDefinition { = TestFunctionCtx>(fn?: TestFunction): Hook; } +export type DefinitionParams = { + tag?: string | string[]; +}; + export interface TestDefinition { = TestFunctionCtx>(title: string, fn?: TestFunction): Test; + = TestFunctionCtx>( + title: string, + params: DefinitionParams, + fn?: TestFunction, + ): Test; only: = TestFunctionCtx>(title: string, fn?: TestFunction) => Test; @@ -45,7 +54,14 @@ export interface TestDefinition { export interface SuiteDefinition { (title: string, fn: (this: Suite) => void): Suite; + (title: string, params: DefinitionParams, fn: (this: Suite) => void): Suite; only: (title: string, fn: (this: Suite) => void) => Suite; + skip: (title: string, fn: (this: Suite) => void) => Suite; } + +export type TestTag = { + title: string; + dynamic?: boolean; +}; diff --git a/src/test-reader/test-parser.ts b/src/test-reader/test-parser.ts index 822b452b9..54d1510cc 100644 --- a/src/test-reader/test-parser.ts +++ b/src/test-reader/test-parser.ts @@ -15,14 +15,16 @@ import path from "path"; import fs from "fs-extra"; import * as logger from "../utils/logger"; import { getShortMD5 } from "../utils/crypto"; -import { Test } from "./test-object"; +import { Suite, Test } from "./test-object"; import { Config } from "../config"; import { BrowserConfig } from "../config/browser-config"; import type { ReadTestsOpts } from "../testplane"; +import { TagFilter } from "../utils/cli"; export type TestParserParseOpts = { browserId: string; grep?: RegExp; + tag?: TagFilter; config: BrowserConfig; }; @@ -135,7 +137,7 @@ export class TestParser extends EventEmitter { }); } - parse(files: string[], { browserId, config, grep }: TestParserParseOpts): Test[] { + parse(files: string[], { browserId, config, grep, tag }: TestParserParseOpts): Test[] { const treeBuilder = new TreeBuilder(); this.#buildInstructions.exec(files, { treeBuilder, browserId, config }); @@ -144,6 +146,22 @@ export class TestParser extends EventEmitter { treeBuilder.addTestFilter((test: Test) => grep.test(test.fullTitle())); } + if (tag) { + treeBuilder.addTestFilter((test: Test) => { + let current: Test | Suite | null = test; + + while (current) { + if (tag(current.tag)) { + return true; + } else { + current = current.parent; + } + } + + return false; + }); + } + if (config.lastFailed?.only) { if (!this.#failedTests.size) { return []; diff --git a/src/testplane.ts b/src/testplane.ts index e5738e8cc..1d3554d83 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -20,11 +20,13 @@ import { ConfigInput } from "./config/types"; import { MasterEventHandler, Test, TestResult } from "./types"; import { preloadWebdriverIO } from "./utils/preload-utils"; import { updateSelectivityHashes } from "./browser/cdp/selectivity"; +import { TagFilter } from "./utils/cli"; interface RunOpts { browsers: string[]; sets: string[]; grep: RegExp; + tag: TagFilter; updateRefs: boolean; requireModules: string[]; inspectMode: { @@ -55,7 +57,8 @@ interface RunnableOpts { saveLocations?: boolean; } -export interface ReadTestsOpts extends Pick { +export interface ReadTestsOpts + extends Pick { silent: boolean; ignore: string | string[]; failed: FailedListItem[]; @@ -101,6 +104,7 @@ export class Testplane extends BaseTestplane { browsers, sets, grep, + tag, updateRefs, requireModules, inspectMode, @@ -163,7 +167,7 @@ export class Testplane extends BaseTestplane { const shouldDisableSelectivity = Boolean(hasTestFilter); await runner.run( - await this._readTests(testPaths, { browsers, sets, grep, replMode, keepBrowserMode }), + await this._readTests(testPaths, { browsers, sets, grep, tag, replMode, keepBrowserMode }), RunnerStats.create(this), { shouldDisableSelectivity }, ); @@ -196,7 +200,17 @@ export class Testplane extends BaseTestplane { async readTests( testPaths: string[], - { browsers, sets, grep, silent, ignore, replMode, keepBrowserMode, runnableOpts }: Partial = {}, + { + browsers, + sets, + grep, + tag, + silent, + ignore, + replMode, + keepBrowserMode, + runnableOpts, + }: Partial = {}, ): Promise { const testReader = TestReader.create(this._config); @@ -215,6 +229,7 @@ export class Testplane extends BaseTestplane { ignore, sets, grep, + tag, replMode, keepBrowserMode, runnableOpts, diff --git a/src/types/index.ts b/src/types/index.ts index 3f44ad774..8b1640f89 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,7 +20,7 @@ import type { NormalizedDependencies, SelectivityCompressionType } from "../brow export type { Test } from "../test-reader/test-object/test"; export type { Suite } from "../test-reader/test-object/suite"; -export type { TestFunction, TestFunctionCtx } from "../test-reader/test-object/types"; +export type { TestFunction, TestFunctionCtx, TestTag } from "../test-reader/test-object/types"; export type WdioBrowser = WebdriverIO.Browser; diff --git a/src/utils/cli.ts b/src/utils/cli.ts index 8bff3943b..ac21a5b9b 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -7,6 +7,39 @@ export const collectCliValues = (newValue: unknown, array = [] as unknown[]): un return array.concat(newValue); }; +export type TagFilter = (tags: Map) => boolean; + +export const compileTagFilter = (filter: string): TagFilter => { + const normalizedFilter = filter.replace(/\s+/g, "").toLowerCase(); + + function compileOrExpression(expr: string): string { + const parts = expr.split("|"); + if (parts.length === 1) { + return compileAndExpression(parts[0]); + } + return `(${parts.map(part => compileAndExpression(part)).join(" || ")})`; + } + + function compileAndExpression(expr: string): string { + const parts = expr.split("&"); + if (parts.length === 1) { + return compileSingleTag(parts[0]); + } + return `(${parts.map(part => compileSingleTag(part)).join(" && ")})`; + } + + function compileSingleTag(tag: string): string { + if (tag.startsWith("!")) { + return `!tags.has("${tag.substring(1)}")`; + } + return `tags.has("${tag}")`; + } + + const compiledCode = compileOrExpression(normalizedFilter); + + return new Function("tags", `return ${compiledCode};`) as (tags: Map) => boolean; +}; + export const compileGrep = (grep: string): RegExp => { try { return new RegExp(`(${grep})|(${_.escapeRegExp(grep)})`); @@ -41,5 +74,6 @@ export const withCommonCliOptions = ({ cmd, actionName = "run" }: { cmd: Command .option("-b, --browser ", `${actionName} tests only in specified browser`, collectCliValues) .option("-s, --set ", `${actionName} tests only in the specified set`, collectCliValues) .option("-r, --require ", "require module", collectCliValues) - .option("--grep ", `${actionName} only tests matching the pattern`, compileGrep); + .option("--grep ", `${actionName} only tests matching the pattern`, compileGrep) + .option("--tag ", "Tag filter", compileTagFilter); }; diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index e944be016..cfc879e6a 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -104,12 +104,14 @@ module.exports = class TestRunner { testplaneCtx.assertViewResults = assertViewResults ? assertViewResults.toRawObject() : []; - const { meta } = this._browser; + const { meta, tag } = this._browser; + const commandsHistory = callstackHistory ? callstackHistory.release() : []; const results = { testplaneCtx, hermioneCtx: testplaneCtx, meta, + tag, }; switch (this._browser.config.saveHistoryMode) { diff --git a/src/worker/runner/test-runner/types.ts b/src/worker/runner/test-runner/types.ts index 5bb7046c8..5805dee4d 100644 --- a/src/worker/runner/test-runner/types.ts +++ b/src/worker/runner/test-runner/types.ts @@ -23,4 +23,5 @@ export interface ExecutionThreadCtorOpts { hermioneCtx: WorkerRunTestTestplaneCtx; screenshooter: OneTimeScreenshooter; attempt: number; + tag?: string[]; } diff --git a/src/worker/testplane.ts b/src/worker/testplane.ts index 6f892579e..c80c8692a 100644 --- a/src/worker/testplane.ts +++ b/src/worker/testplane.ts @@ -32,6 +32,7 @@ export interface WorkerRunTestResult { * @deprecated Use `testplaneCtx` instead */ hermioneCtx: WorkerRunTestTestplaneCtx; + tag: string[]; } export interface Testplane { diff --git a/test/src/test-reader/index.js b/test/src/test-reader/index.js index d5f8f53be..1218e0a2d 100644 --- a/test/src/test-reader/index.js +++ b/test/src/test-reader/index.js @@ -196,11 +196,13 @@ describe("test-reader", () => { browserId: "bro1", config: bro1Config, grep, + tag: undefined, }); assert.calledWith(TestParser.prototype.parse, ["common/file", "file2"], { browserId: "bro2", config: bro2Config, grep, + tag: undefined, }); }); diff --git a/test/src/testplane.js b/test/src/testplane.js index 92d8a1f1e..a87923cf2 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -362,6 +362,7 @@ describe("testplane", () => { sets, replMode, keepBrowserMode, + tag: undefined, }); }); @@ -686,6 +687,7 @@ describe("testplane", () => { runnableOpts: { saveLocations: true, }, + tag: undefined, }); }); diff --git a/test/src/worker/browser-env/runner/test-runner/index.ts b/test/src/worker/browser-env/runner/test-runner/index.ts index 98beea41c..4b6855c9c 100644 --- a/test/src/worker/browser-env/runner/test-runner/index.ts +++ b/test/src/worker/browser-env/runner/test-runner/index.ts @@ -71,7 +71,7 @@ describe("worker/browser-env/runner/test-runner", () => { fn: sinon.stub(), location: undefined, }) as TestType; - test.parent = Suite.create({ id: "67890", title: "", file: test.file }); + test.parent = Suite.create({ id: "67890", title: "", file: test.file, tag: [] }); return test; }; From 3e2b66bff9436b7589c650b23497a564dfbdab9e Mon Sep 17 00:00:00 2001 From: rocketraccoon Date: Mon, 1 Dec 2025 16:15:40 +0700 Subject: [PATCH 2/3] feat: add tests --- src/utils/cli.ts | 2 +- test/src/cli/commands/list-tests/index.ts | 8 ++ test/src/cli/index.js | 16 ++++ test/src/test-reader/index.js | 7 +- test/src/test-reader/test-parser.js | 90 ++++++++++++++++++++++- test/src/testplane.js | 7 +- 6 files changed, 124 insertions(+), 6 deletions(-) diff --git a/src/utils/cli.ts b/src/utils/cli.ts index ac21a5b9b..4e297e1ba 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -75,5 +75,5 @@ export const withCommonCliOptions = ({ cmd, actionName = "run" }: { cmd: Command .option("-s, --set ", `${actionName} tests only in the specified set`, collectCliValues) .option("-r, --require ", "require module", collectCliValues) .option("--grep ", `${actionName} only tests matching the pattern`, compileGrep) - .option("--tag ", "Tag filter", compileTagFilter); + .option("--tag ", `${actionName} only tests matching the pattern`, compileTagFilter); }; diff --git a/test/src/cli/commands/list-tests/index.ts b/test/src/cli/commands/list-tests/index.ts index 6dd5d5fb1..80171e961 100644 --- a/test/src/cli/commands/list-tests/index.ts +++ b/test/src/cli/commands/list-tests/index.ts @@ -120,6 +120,14 @@ describe("cli/commands/list-tests", () => { }); }); + it("should use tag from cli", async () => { + await listTests_("--tag smoke"); + + assert.calledWithMatch(Testplane.prototype.readTests as SinonStub, sinon.match.any, { + tag: sinon.match.instanceOf(Function), + }); + }); + it("should use ignore paths from cli", async () => { await listTests_("--ignore first --ignore second"); diff --git a/test/src/cli/index.js b/test/src/cli/index.js index 82a1d49b4..8aac801d3 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -144,6 +144,22 @@ describe("cli", () => { assert.calledWithMatch(Testplane.prototype.run, any, { browsers: ["first", "second"] }); }); + describe("tag", () => { + it("should not pass any grep rule if it was not specified from cli", async () => { + await run_(); + + assert.calledWithMatch(Testplane.prototype.run, any, { tag: undefined }); + }); + + it("should use tag rule from cli", async () => { + await run_("--tag foo"); + + console.log("Testplane.prototype.run.firstCall.args", Testplane.prototype.run.firstCall.args); + + assert.instanceOf(Testplane.prototype.run.firstCall.args[1].tag, Function); + }); + }); + describe("grep", () => { it("should not pass any grep rule if it was not specified from cli", async () => { await run_(); diff --git a/test/src/test-reader/index.js b/test/src/test-reader/index.js index 1218e0a2d..cc602255a 100644 --- a/test/src/test-reader/index.js +++ b/test/src/test-reader/index.js @@ -18,6 +18,7 @@ describe("test-reader", () => { ignore: [], browsers: [], grep: undefined, + tag: undefined, }); config = config || makeConfigStub(); @@ -247,6 +248,7 @@ describe("test-reader", () => { { name: "ignore", value: "ignore1", expectedMsg: "- ignore: ignore1\n" }, { name: "sets", value: ["set1", "set2"], expectedMsg: "- sets: set1, set2\n" }, { name: "grep", value: "grep1", expectedMsg: "- grep: grep1\n" }, + { name: "tag", value: "tag_one", expectedMsg: "- tag: tag_one\n" }, ].forEach(({ name, value, expectedMsg }) => { it(`should correctly print passed option ${name}`, async () => { try { @@ -277,7 +279,10 @@ describe("test-reader", () => { }); it("should print supported options if none are specified", async () => { - await assert.isRejected(readTests_(), "Try to specify [paths, sets, ignore, browsers, grep] options"); + await assert.isRejected( + readTests_(), + "Try to specify [paths, sets, ignore, browsers, grep, tag] options", + ); }); it("should throw error if there are only silently skipped tests", async () => { diff --git a/test/src/test-reader/test-parser.js b/test/src/test-reader/test-parser.js index c08ad9603..2bd04586d 100644 --- a/test/src/test-reader/test-parser.js +++ b/test/src/test-reader/test-parser.js @@ -15,6 +15,7 @@ const path = require("path"); const { EventEmitter } = require("events"); const _ = require("lodash"); const fs = require("fs-extra"); +const { compileTagFilter } = require("src/utils/cli"); const { NEW_BUILD_INSTRUCTION } = TestReaderEvents; @@ -515,7 +516,7 @@ describe("test-reader/test-parser", () => { }); describe("parse", () => { - const parse_ = async ({ files, browserId, config, grep } = {}, loadFilesConfig) => { + const parse_ = async ({ files, browserId, config, grep, tag } = {}, loadFilesConfig) => { loadFilesConfig = loadFilesConfig || makeConfigStub(); config = _.defaults(config, { desiredCapabilities: {}, @@ -524,7 +525,7 @@ describe("test-reader/test-parser", () => { const parser = new TestParser(); await parser.loadFiles([], { config: loadFilesConfig }); - return parser.parse(files || [], { browserId, config, grep }); + return parser.parse(files || [], { browserId, config, grep, tag }); }; beforeEach(() => { @@ -616,6 +617,91 @@ describe("test-reader/test-parser", () => { }); }); + describe("tag", () => { + it("should not set test filter to tree builder if grep not set", async () => { + await parse_(); + + assert.notCalled(TreeBuilder.prototype.addTestFilter); + }); + + describe("if set", () => { + it("should set test filter to tree builder", async () => { + await parse_({ tag: compileTagFilter("smoke") }); + + assert.calledOnceWith(TreeBuilder.prototype.addTestFilter, sinon.match.func); + }); + + it("should set test filter to tree builder before applying filters", async () => { + await parse_({ tag: compileTagFilter("smoke") }); + + assert.callOrder(TreeBuilder.prototype.addTestFilter, TreeBuilder.prototype.applyFilters); + }); + + it("installed filter should accept matched test", async () => { + await parse_({ tag: compileTagFilter("smoke") }); + + const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; + const test = { fullTitle: () => "Some name", tag: new Map([["smoke", false]]) }; + + assert.isTrue(filter(test)); + }); + + it("installed filter should accept matched test with and operator for tags", async () => { + await parse_({ tag: compileTagFilter("smoke&slow") }); + + const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; + const test = { + fullTitle: () => "Some name", + tag: new Map([ + ["smoke", false], + ["slow", false], + ]), + }; + + assert.isTrue(filter(test)); + }); + + it("installed filter should accept matched test with not operator for tags", async () => { + await parse_({ tag: compileTagFilter("!smoke&!slow") }); + + const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; + const test = { + fullTitle: () => "Some name", + tag: new Map([ + ["smoke", false], + ["slow", false], + ]), + }; + + assert.isFalse(filter(test)); + }); + + it("installed filter should accept matched test with or operator for tags", async () => { + await parse_({ tag: compileTagFilter("smoke|slow") }); + + const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; + const test = { + fullTitle: () => "Some name", + tag: new Map([ + ["smoke", false], + ["fast", false], + ]), + }; + + assert.isTrue(filter(test)); + }); + + it("installed filter should ignore not matched test", async () => { + await parse_({ tag: compileTagFilter("desktop") }); + + const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; + const test = { fullTitle: () => "Some name", tag: new Map([["smoke", false]]) }; + + assert.isFalse(filter(test)); + }); + }); + }); + describe("grep", () => { it("should not set test filter to tree builder if grep not set", async () => { await parse_(); diff --git a/test/src/testplane.js b/test/src/testplane.js index a87923cf2..011ddadc7 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -342,6 +342,7 @@ describe("testplane", () => { const testPaths = ["foo/bar"]; const browsers = ["bro1", "bro2"]; const grep = "baz.*"; + const tag = "baz"; const sets = ["set1", "set2"]; const replMode = { enabled: false }; const keepBrowserMode = { enabled: false }; @@ -351,6 +352,7 @@ describe("testplane", () => { await runTestplane(testPaths, { browsers, grep, + tag, sets, replMode, keepBrowserMode, @@ -362,7 +364,7 @@ describe("testplane", () => { sets, replMode, keepBrowserMode, - tag: undefined, + tag, }); }); @@ -669,6 +671,7 @@ describe("testplane", () => { ignore: "baz/qux", sets: ["s1", "s2"], grep: "grep", + tag: "tag_one", replMode: { enabled: false }, keepBrowserMode: { enabled: false, onFail: false }, runnableOpts: { @@ -682,12 +685,12 @@ describe("testplane", () => { ignore: "baz/qux", sets: ["s1", "s2"], grep: "grep", + tag: "tag_one", replMode: { enabled: false }, keepBrowserMode: { enabled: false, onFail: false }, runnableOpts: { saveLocations: true, }, - tag: undefined, }); }); From f31b739d905834fb175a93c5a6b86d5a6eac8705 Mon Sep 17 00:00:00 2001 From: rocketraccoon Date: Wed, 3 Dec 2025 19:14:24 +0700 Subject: [PATCH 3/3] feat: review fixes --- src/browser/existing-browser.ts | 10 ++--- src/runner/test-runner/regular-test-runner.js | 6 +-- src/test-reader/mocha-reader/index.js | 10 +++-- .../mocha-reader/tree-builder-decorator.js | 8 ++-- src/test-reader/test-object/suite.ts | 20 ++++----- src/test-reader/test-object/test.ts | 22 +++++----- src/test-reader/test-parser.ts | 2 +- src/testplane.ts | 2 +- src/utils/cli.ts | 4 +- src/worker/runner/test-runner/index.js | 4 +- src/worker/runner/test-runner/types.ts | 2 +- src/worker/testplane.ts | 2 +- test/src/test-reader/test-parser.js | 10 ++--- test/src/utils/compileTagFilter.ts | 43 +++++++++++++++++++ .../browser-env/runner/test-runner/index.ts | 2 +- 15 files changed, 96 insertions(+), 51 deletions(-) create mode 100644 test/src/utils/compileTagFilter.ts diff --git a/src/browser/existing-browser.ts b/src/browser/existing-browser.ts index d4115485d..e74f9723c 100644 --- a/src/browser/existing-browser.ts +++ b/src/browser/existing-browser.ts @@ -91,7 +91,7 @@ export class ExistingBrowser extends Browser { protected _calibration?: CalibrationResult; protected _clientBridge?: ClientBridge; protected _cdp: CDP | null = null; - protected _tag: Set = new Set(); + protected _tags: Set = new Set(); constructor(config: Config, opts: BrowserOpts) { super(config, opts); @@ -332,9 +332,9 @@ export class ExistingBrowser extends Browser { session.addCommand("addTag", (tag: string | string[]) => { if (Array.isArray(tag)) { - tag.forEach(element => this._tag?.add(element)); + tag.forEach(element => this._tags?.add(element)); } else { - this._tag?.add(tag); + this._tags?.add(tag); } }); } @@ -593,8 +593,8 @@ export class ExistingBrowser extends Browser { return this._meta; } - get tag(): string[] { - return [...this._tag]; + get tags(): string[] { + return [...this._tags]; } get cdp(): CDP | null { diff --git a/src/runner/test-runner/regular-test-runner.js b/src/runner/test-runner/regular-test-runner.js index 428b0af80..dee04fc3b 100644 --- a/src/runner/test-runner/regular-test-runner.js +++ b/src/runner/test-runner/regular-test-runner.js @@ -71,7 +71,7 @@ module.exports = class RegularTestRunner extends RunnableEmitter { }); } - _applyTestResults({ tag, meta, testplaneCtx = {}, history = [] }) { + _applyTestResults({ tags, meta, testplaneCtx = {}, history = [] }) { testplaneCtx.assertViewResults = AssertViewResults.fromRawObject(testplaneCtx.assertViewResults || []); this._test.assertViewResults = testplaneCtx.assertViewResults.get(); @@ -80,8 +80,8 @@ module.exports = class RegularTestRunner extends RunnableEmitter { this._test.hermioneCtx = testplaneCtx; this._test.history = history; - if (tag) { - tag.forEach(tag => this._test.addTag(tag)); + if (tags) { + tags.forEach(tag => this._test.addTag(tag)); } this._test.duration = Date.now() - this._test.startTime; diff --git a/src/test-reader/mocha-reader/index.js b/src/test-reader/mocha-reader/index.js index 486ffa670..1a50858ff 100644 --- a/src/test-reader/mocha-reader/index.js +++ b/src/test-reader/mocha-reader/index.js @@ -19,9 +19,9 @@ function getTagParser(original) { if (paramsOrFn?.tag) { if (Array.isArray(paramsOrFn.tag)) { - test.tag = paramsOrFn.tag.map(title => ({ title, dynamic: false })); + test.tags = paramsOrFn.tag.map(title => ({ title, dynamic: false })); } else { - test.tag = [{ title: paramsOrFn.tag, dynamic: false }]; + test.tags = [{ title: paramsOrFn.tag, dynamic: false }]; } } @@ -37,12 +37,14 @@ async function readFiles(files, { esmDecorator, config, eventBus, runnableOpts } const originalDescribe = context.describe; const originalIt = context.it; - context.describe = context.context = getTagParser(originalDescribe); + context.describe = getTagParser(originalDescribe); + context.context = getTagParser(originalDescribe); context.describe.only = originalDescribe.only; context.describe.skip = originalDescribe.skip; - context.it = context.specify = getTagParser(originalIt); + context.it = getTagParser(originalIt); + context.specify = getTagParser(originalIt); context.it.only = originalIt.only; context.it.skip = originalIt.skip; diff --git a/src/test-reader/mocha-reader/tree-builder-decorator.js b/src/test-reader/mocha-reader/tree-builder-decorator.js index 2894fa4cf..89a8b14b7 100644 --- a/src/test-reader/mocha-reader/tree-builder-decorator.js +++ b/src/test-reader/mocha-reader/tree-builder-decorator.js @@ -18,12 +18,12 @@ class TreeBuilderDecorator { } addSuite(mochaSuite) { - const { id: mochaId, tag } = mochaSuite; + const { id: mochaId, tags } = mochaSuite; const file = computeFile(mochaSuite) ?? "unknown-file"; const positionInFile = this.#suiteCounter.get(file) || 0; const id = mochaSuite.root ? mochaId : crypto.getShortMD5(file) + positionInFile; - const suite = this.#mkTestObject(Suite, mochaSuite, { id, tag }); + const suite = this.#mkTestObject(Suite, mochaSuite, { id, tags }); this.#applyConfig(suite, mochaSuite); this.#treeBuilder.addSuite(suite, this.#getParent(mochaSuite, null)); @@ -34,9 +34,9 @@ class TreeBuilderDecorator { } addTest(mochaTest) { - const { fn, tag } = mochaTest; + const { fn, tags } = mochaTest; const id = crypto.getShortMD5(mochaTest.fullTitle()); - const test = this.#mkTestObject(Test, mochaTest, { id, fn, tag }); + const test = this.#mkTestObject(Test, mochaTest, { id, fn, tags }); this.#applyConfig(test, mochaTest); this.#treeBuilder.addTest(test, this.#getParent(mochaTest)); diff --git a/src/test-reader/test-object/suite.ts b/src/test-reader/test-object/suite.ts index 36a636251..035de8678 100644 --- a/src/test-reader/test-object/suite.ts +++ b/src/test-reader/test-object/suite.ts @@ -4,24 +4,24 @@ import { Hook } from "./hook"; import { Test } from "./test"; import type { TestObjectData, ConfigurableTestObjectData, TestFunction, TestFunctionCtx, TestTag } from "./types"; -type SuiteOpts = Pick & TestObjectData & { tag: TestTag[] }; +type SuiteOpts = Pick & TestObjectData & { tags: TestTag[] }; export class Suite extends ConfigurableTestObject { #suites: this[]; #tests: Test[]; #beforeEachHooks: Hook[]; #afterEachHooks: Hook[]; - public tag: Map; + public tags: Map; static create(this: new (opts: SuiteOpts) => T, opts: SuiteOpts): T { return new this(opts); } // used inside test - constructor({ title, file, id, location, tag }: SuiteOpts = {} as SuiteOpts) { + constructor({ title, file, id, location, tags }: SuiteOpts = {} as SuiteOpts) { super({ title, file, id, location }); - this.tag = new Map(tag?.map(({ title, dynamic }) => [title, !!dynamic]) || []); + this.tags = new Map(tags?.map(({ title, dynamic }) => [title, Boolean(dynamic)]) || []); this.#suites = []; this.#tests = []; this.#beforeEachHooks = []; @@ -30,20 +30,20 @@ export class Suite extends ConfigurableTestObject { addTag(tag: string | string[], dynamic = false): void { if (Array.isArray(tag)) { - tag.forEach(element => this.tag.set(element, dynamic)); + tag.forEach(element => this.tags.set(element, dynamic)); } else { - this.tag.set(tag, dynamic); + this.tags.set(tag, dynamic); } } hasTag(tag: string): boolean { - return this.tag.has(tag); + return this.tags.has(tag); } - getTag(): TestTag[] { - return Array.from(this.tag.keys()).map(title => ({ + getTags(): TestTag[] { + return Array.from(this.tags.keys()).map(title => ({ title, - dynamic: this.tag.get(title), + dynamic: this.tags.get(title), })); } diff --git a/src/test-reader/test-object/test.ts b/src/test-reader/test-object/test.ts index c9fb2b5ef..fea9e26dc 100644 --- a/src/test-reader/test-object/test.ts +++ b/src/test-reader/test-object/test.ts @@ -4,48 +4,48 @@ import type { TestObjectData, TestFunction, TestFunctionCtx, TestTag } from "./t type TestOpts = TestObjectData & Pick & { fn: TestFunction; - tag?: TestTag[]; + tags?: TestTag[]; }; export class Test extends ConfigurableTestObject { public fn: TestFunction; - public tag: Map; + public tags: Map; public err?: Error; static create(this: new (opts: TestOpts) => T, opts: TestOpts): T { return new this(opts); } - constructor({ title, file, id, location, fn, tag }: TestOpts) { + constructor({ title, file, id, location, fn, tags }: TestOpts) { super({ title, file, id, location }); this.fn = fn; - this.tag = new Map(tag?.map(({ title, dynamic }) => [title, !!dynamic]) || []); + this.tags = new Map(tags?.map(({ title, dynamic }) => [title, Boolean(dynamic)]) || []); } addTag(tag: string | string[]): void { if (Array.isArray(tag)) { - tag.forEach(element => this.tag.set(element, true)); + tag.forEach(element => this.tags.set(element, true)); } else { - this.tag.set(tag, true); + this.tags.set(tag, true); } } hasTag(tag: string): boolean { - return this.tag.has(tag); + return this.tags.has(tag); } - getTag(): TestTag[] { - return Array.from(this.tag.keys()).map(title => ({ + getTags(): TestTag[] { + return Array.from(this.tags.keys()).map(title => ({ title, - dynamic: this.tag.get(title), + dynamic: this.tags.get(title), })); } clone(): Test { return new Test({ title: this.title, - tag: this.getTag(), + tags: this.getTags(), file: this.file, id: this.id, location: this.location, diff --git a/src/test-reader/test-parser.ts b/src/test-reader/test-parser.ts index 54d1510cc..caea16dee 100644 --- a/src/test-reader/test-parser.ts +++ b/src/test-reader/test-parser.ts @@ -151,7 +151,7 @@ export class TestParser extends EventEmitter { let current: Test | Suite | null = test; while (current) { - if (tag(current.tag)) { + if (tag(current.tags)) { return true; } else { current = current.parent; diff --git a/src/testplane.ts b/src/testplane.ts index 1d3554d83..f29f82eb7 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -163,7 +163,7 @@ export class Testplane extends BaseTestplane { } const hasTestPathsFilter = _.isArray(testPaths) ? Boolean(testPaths.length) : true; - const hasTestFilter = hasTestPathsFilter || Boolean(sets?.length) || Boolean(grep); + const hasTestFilter = hasTestPathsFilter || Boolean(sets?.length) || Boolean(grep) || Boolean(tag); const shouldDisableSelectivity = Boolean(hasTestFilter); await runner.run( diff --git a/src/utils/cli.ts b/src/utils/cli.ts index 4e297e1ba..ad69b1995 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -10,7 +10,7 @@ export const collectCliValues = (newValue: unknown, array = [] as unknown[]): un export type TagFilter = (tags: Map) => boolean; export const compileTagFilter = (filter: string): TagFilter => { - const normalizedFilter = filter.replace(/\s+/g, "").toLowerCase(); + const normalizedFilter = filter.replace(/[\s'"`\\]/g, "").toLowerCase(); function compileOrExpression(expr: string): string { const parts = expr.split("|"); @@ -75,5 +75,5 @@ export const withCommonCliOptions = ({ cmd, actionName = "run" }: { cmd: Command .option("-s, --set ", `${actionName} tests only in the specified set`, collectCliValues) .option("-r, --require ", "require module", collectCliValues) .option("--grep ", `${actionName} only tests matching the pattern`, compileGrep) - .option("--tag ", `${actionName} only tests matching the pattern`, compileTagFilter); + .option("--tag ", `${actionName} only tests with specified tags`, compileTagFilter); }; diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index cfc879e6a..a32bb120f 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -104,14 +104,14 @@ module.exports = class TestRunner { testplaneCtx.assertViewResults = assertViewResults ? assertViewResults.toRawObject() : []; - const { meta, tag } = this._browser; + const { meta, tags } = this._browser; const commandsHistory = callstackHistory ? callstackHistory.release() : []; const results = { testplaneCtx, hermioneCtx: testplaneCtx, meta, - tag, + tags, }; switch (this._browser.config.saveHistoryMode) { diff --git a/src/worker/runner/test-runner/types.ts b/src/worker/runner/test-runner/types.ts index 5805dee4d..e33cbd3be 100644 --- a/src/worker/runner/test-runner/types.ts +++ b/src/worker/runner/test-runner/types.ts @@ -23,5 +23,5 @@ export interface ExecutionThreadCtorOpts { hermioneCtx: WorkerRunTestTestplaneCtx; screenshooter: OneTimeScreenshooter; attempt: number; - tag?: string[]; + tags?: string[]; } diff --git a/src/worker/testplane.ts b/src/worker/testplane.ts index c80c8692a..d55c3b867 100644 --- a/src/worker/testplane.ts +++ b/src/worker/testplane.ts @@ -32,7 +32,7 @@ export interface WorkerRunTestResult { * @deprecated Use `testplaneCtx` instead */ hermioneCtx: WorkerRunTestTestplaneCtx; - tag: string[]; + tags: string[]; } export interface Testplane { diff --git a/test/src/test-reader/test-parser.js b/test/src/test-reader/test-parser.js index 2bd04586d..4e1881f6d 100644 --- a/test/src/test-reader/test-parser.js +++ b/test/src/test-reader/test-parser.js @@ -641,7 +641,7 @@ describe("test-reader/test-parser", () => { await parse_({ tag: compileTagFilter("smoke") }); const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; - const test = { fullTitle: () => "Some name", tag: new Map([["smoke", false]]) }; + const test = { fullTitle: () => "Some name", tags: new Map([["smoke", false]]) }; assert.isTrue(filter(test)); }); @@ -652,7 +652,7 @@ describe("test-reader/test-parser", () => { const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; const test = { fullTitle: () => "Some name", - tag: new Map([ + tags: new Map([ ["smoke", false], ["slow", false], ]), @@ -667,7 +667,7 @@ describe("test-reader/test-parser", () => { const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; const test = { fullTitle: () => "Some name", - tag: new Map([ + tags: new Map([ ["smoke", false], ["slow", false], ]), @@ -682,7 +682,7 @@ describe("test-reader/test-parser", () => { const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; const test = { fullTitle: () => "Some name", - tag: new Map([ + tags: new Map([ ["smoke", false], ["fast", false], ]), @@ -695,7 +695,7 @@ describe("test-reader/test-parser", () => { await parse_({ tag: compileTagFilter("desktop") }); const filter = TreeBuilder.prototype.addTestFilter.lastCall.args[0]; - const test = { fullTitle: () => "Some name", tag: new Map([["smoke", false]]) }; + const test = { fullTitle: () => "Some name", tags: new Map([["smoke", false]]) }; assert.isFalse(filter(test)); }); diff --git a/test/src/utils/compileTagFilter.ts b/test/src/utils/compileTagFilter.ts new file mode 100644 index 000000000..f677e1320 --- /dev/null +++ b/test/src/utils/compileTagFilter.ts @@ -0,0 +1,43 @@ +import { compileTagFilter } from "../../../src/utils/cli"; + +describe("compileTagFilter", () => { + it("should work with correct expression string", () => { + const cases = [ + { + expression: "old&smoke", + okTags: ["smoke", "desktop", "old"], + badTags: ["old"], + }, + { + expression: "old&smoke|desktop", + okTags: ["smoke", "desktop", "old"], + badTags: ["smoke", "slow"], + }, + { + expression: "old", + okTags: ["smoke", "desktop", "old"], + badTags: ["smoke", "desktop"], + }, + { + expression: "!slow", + okTags: ["smoke", "desktop", "old"], + badTags: ["slow"], + }, + ]; + + cases.forEach(({ expression, okTags, badTags }) => { + const func = compileTagFilter(expression); + + assert.isTrue(func(new Map(okTags.map(tag => [tag, false])))); + + assert.isFalse(func(new Map(badTags.map(tag => [tag, false])))); + }); + }); + + it("check injection code", () => { + const injectionStr = '")+console.log("111111'; + const func = compileTagFilter(injectionStr); + + assert.isFalse(func(new Map())); + }); +}); diff --git a/test/src/worker/browser-env/runner/test-runner/index.ts b/test/src/worker/browser-env/runner/test-runner/index.ts index 4b6855c9c..21f68e3f9 100644 --- a/test/src/worker/browser-env/runner/test-runner/index.ts +++ b/test/src/worker/browser-env/runner/test-runner/index.ts @@ -71,7 +71,7 @@ describe("worker/browser-env/runner/test-runner", () => { fn: sinon.stub(), location: undefined, }) as TestType; - test.parent = Suite.create({ id: "67890", title: "", file: test.file, tag: [] }); + test.parent = Suite.create({ id: "67890", title: "", file: test.file, tags: [] }); return test; };