diff --git a/packages/core/spec/io/Path.spec.ts b/packages/core/spec/io/Path.spec.ts index 032cc3edb1e..d68c0216fd8 100644 --- a/packages/core/spec/io/Path.spec.ts +++ b/packages/core/spec/io/Path.spec.ts @@ -1,7 +1,7 @@ import { describe, it } from 'mocha'; import { given } from 'mocha-testdata'; -import { Path } from '../../src/io'; +import { Path } from '../../src/io/Path'; import { expect } from '../expect'; describe ('Path', () => { @@ -44,9 +44,10 @@ describe ('Path', () => { }); given([ - { path: Path.from('../home'), expected: '../home' }, - { path: Path.from('./home'), expected: 'home' }, - { path: Path.from('home'), expected: 'home' }, + { path: Path.from('../home'), expected: '../home' }, + { path: Path.from('./home'), expected: 'home' }, + { path: Path.from('home'), expected: 'home' }, + { path: Path.from('file:///home'), expected: '/home' }, ]). it('can be instantiated from a path segment', ({ path, expected }) => { expect(path.value).to.equal(expected); @@ -114,24 +115,38 @@ describe ('Path', () => { }); given( - { description: 'localhost', uri: 'file://localhost/etc/fstab', expected: '/etc/fstab' }, - { description: 'no host', uri: 'file:///etc/fstab', expected: '/etc/fstab' }, - { description: 'Windows, no host', uri: 'file:///c:/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, - { description: 'Windows, localhost, pipe instead of colon', uri: 'file://localhost/c|/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, - { description: 'Windows, no host, pipe instead of colon', uri: 'file:///c|/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, - { description: 'Windows, localhost', uri: 'file://localhost/c:/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, - { description: 'spaces in file name', uri: 'file:///hostname/path/to/the%20file.txt', expected: '/hostname/path/to/the file.txt' }, - { description: 'Windows, spaces in file name', uri: 'file:///c:/path/to/the%20file.txt', expected: 'c:/path/to/the file.txt' }, - { description: 'Windows, spaces in directory name', uri: 'file:///C:/Documents%20and%20Settings/user/FileSchemeURIs.doc', expected: 'C:/Documents and Settings/user/FileSchemeURIs.doc' }, - { description: 'Windows, special characters', uri: 'file:///C:/caf%C3%A9/%C3%A5r/d%C3%BCnn/%E7%89%9B%E9%93%83/Ph%E1%BB%9F/%F0%9F%98%B5.exe', expected: 'C:/café/år/dünn/牛铃/Phở/😵.exe' }, + { description: 'localhost', url: 'file://localhost/etc/fstab', expected: '/etc/fstab' }, + { description: 'no host', url: 'file:///etc/fstab', expected: '/etc/fstab' }, + { description: 'Windows, no host', url: 'file:///c:/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, + { description: 'Windows, localhost, pipe instead of colon', url: 'file://localhost/c|/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, + { description: 'Windows, no host, pipe instead of colon', url: 'file:///c|/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, + { description: 'Windows, localhost', url: 'file://localhost/c:/WINDOWS/clock.avi', expected: 'c:/WINDOWS/clock.avi' }, + { description: 'spaces in file name', url: 'file:///hostname/path/to/the%20file.txt', expected: '/hostname/path/to/the file.txt' }, + { description: 'Windows, spaces in file name', url: 'file:///c:/path/to/the%20file.txt', expected: 'c:/path/to/the file.txt' }, + { description: 'Windows, spaces in directory name', url: 'file:///C:/Documents%20and%20Settings/user/FileSchemeURIs.doc', expected: 'C:/Documents and Settings/user/FileSchemeURIs.doc' }, + { description: 'Windows, special characters', url: 'file:///C:/caf%C3%A9/%C3%A5r/d%C3%BCnn/%E7%89%9B%E9%93%83/Ph%E1%BB%9F/%F0%9F%98%B5.exe', expected: 'C:/café/år/dünn/牛铃/Phở/😵.exe' }, ). - it('can be instantiated from a file:// URI', ({ uri, expected }) => { - expect(Path.fromURI(uri).value).to.equal(expected); + it('can be instantiated from a file:// URI', ({ url, expected }) => { + expect(Path.fromFileURL(new URL(url)).value).to.equal(expected); }); + given( + { description: 'no host', path: '/etc/fstab', expected: 'file:///etc/fstab' }, + { description: 'Windows, no host', path: 'c:/WINDOWS/clock.avi', expected: 'file:///c:/WINDOWS/clock.avi' }, + { description: 'spaces in file name', path: '/hostname/path/to/the file.txt', expected: 'file:///hostname/path/to/the%20file.txt' }, + { description: 'Windows, backslashes', path: 'c:\\path\\to\\file.txt', expected: 'file:///c:/path/to/file.txt' }, + { description: 'Windows, slashes', path: 'c:/path/to/file.txt', expected: 'file:///c:/path/to/file.txt' }, + { description: 'Windows, spaces in file name', path: 'c:/path/to/the file.txt', expected: 'file:///c:/path/to/the%20file.txt' }, + { description: 'Windows, spaces in directory name', path: 'C:/Documents and Settings/user/FileSchemeURIs.doc', expected: 'file:///C:/Documents%20and%20Settings/user/FileSchemeURIs.doc' }, + { description: 'Windows, special characters', path: 'C:/café/år/dünn/牛铃/Phở/😵.exe', expected: 'file:///C:/caf%C3%A9/%C3%A5r/d%C3%BCnn/%E7%89%9B%E9%93%83/Ph%E1%BB%9F/%F0%9F%98%B5.exe' }, + ). + it('can be converted to a file:// URI', ({ path, expected }) => { + expect(Path.from(path).toFileURL().toString()).to.equal(expected); + }) + it('complains when instantiated from URI with a non-file URI', () => { expect(() => { - Path.fromURI('https://serenity-js.org/index.html'); - }).to.throw(TypeError, `A Path can be created only from URIs that start with 'file://'. Received: https://serenity-js.org/index.html`) + Path.fromFileURL(new URL('https://serenity-js.org/index.html')); + }).to.throw(TypeError, `A Path can be created only from URLs that start with 'file://'. Received: https://serenity-js.org/index.html`) }) -}); +}); \ No newline at end of file diff --git a/packages/core/src/io/Path.ts b/packages/core/src/io/Path.ts index f185ddab45b..c903c0ea8d3 100644 --- a/packages/core/src/io/Path.ts +++ b/packages/core/src/io/Path.ts @@ -10,19 +10,16 @@ export class Path extends TinyType { return new Path(v); } - static fromURI(uri: string): Path { + static fromFileURL(fileUrl: URL): Path { // inspired by https://github.com/TooTallNate/file-uri-to-path - if ( - typeof uri !== 'string' || - uri.length <= 7 || - uri.slice(0, 7) !== 'file://' - ) { + if (fileUrl.protocol !== 'file:') { throw new TypeError( - `A Path can be created only from URIs that start with 'file://'. Received: ${ uri }` + `A Path can be created only from URLs that start with 'file://'. Received: ${ fileUrl }` ); } - const rest = decodeURI(uri.slice(7)); + const url = fileUrl.toString(); + const rest = decodeURI(url.slice(7)); const firstSlash = rest.indexOf('/'); let host = rest.slice(0, Math.max(0, firstSlash)); @@ -51,8 +48,7 @@ export class Path extends TinyType { // for Windows, we need to invert the path separators from what a URI uses if (sep === '\\') { - throw new Error('that used?') - // path = path.replace(/\//g, '\\'); + path = path.replaceAll('/', '\\'); } if (! (/^.+:/.test(path))) { @@ -64,6 +60,9 @@ export class Path extends TinyType { } static from(...segments: string[]): Path { + if (segments.length === 1 && segments[0].startsWith('file://')) { + return Path.fromFileURL(new URL(segments[0])); + } return new Path(path.joinSafe(...segments)); } @@ -122,4 +121,10 @@ export class Path extends TinyType { root(): Path { return new Path(path.parse(this.value).root); } -} + + toFileURL(): URL { + return new URL( + encodeURI(`file://${this.value}`).replaceAll(/[#?]/g, encodeURIComponent) + ); + } +} \ No newline at end of file diff --git a/packages/cucumber/spec/adapter/CucumberOptions.spec.ts b/packages/cucumber/spec/adapter/CucumberOptions.spec.ts index 7bc2ac5753b..22a144fe41c 100644 --- a/packages/cucumber/spec/adapter/CucumberOptions.spec.ts +++ b/packages/cucumber/spec/adapter/CucumberOptions.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable unicorn/no-null */ import { expect } from '@integration/testing-tools'; -import type { FileSystem, Path} from '@serenity-js/core/lib/io'; -import { Version } from '@serenity-js/core/lib/io'; +import type { FileSystem } from '@serenity-js/core/lib/io'; +import { FileFinder, Path, Version } from '@serenity-js/core/lib/io'; import { describe, it } from 'mocha'; import { given } from 'mocha-testdata'; @@ -9,6 +9,8 @@ import { CucumberOptions } from '../../src/adapter/CucumberOptions'; describe('CucumberOptions', () => { + const finder = new FileFinder(Path.from(__dirname)); + describe('strict mode', () => { given([ @@ -19,7 +21,7 @@ describe('CucumberOptions', () => { new Version('5.0.0'), ]). it('is strict by default', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), { }); + const options = new CucumberOptions(finder, dummyFS(), { }); expect(options.isStrict()).to.equal(true); @@ -34,7 +36,7 @@ describe('CucumberOptions', () => { new Version('5.0.0'), ]). it('can be explicitly enabled', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), { strict: true }); + const options = new CucumberOptions(finder, dummyFS(), { strict: true }); expect(options.isStrict()).to.equal(true); @@ -49,7 +51,7 @@ describe('CucumberOptions', () => { new Version('5.0.0'), ]). it('can be disabled', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), { strict: false }); + const options = new CucumberOptions(finder, dummyFS(), { strict: false }); expect(options.isStrict()).to.equal(false); @@ -64,7 +66,7 @@ describe('CucumberOptions', () => { new Version('5.0.0'), ]). it('can be disabled via cucumberOpts.noStrict', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), { noStrict: true } as any); + const options = new CucumberOptions(finder, dummyFS(), { noStrict: true } as any); expect(options.isStrict()).to.equal(false); @@ -83,18 +85,18 @@ describe('CucumberOptions', () => { new Version('5.0.0'), ]). it('returns no additional arguments when the config is empty', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), {}); + const options = new CucumberOptions(finder, dummyFS(), {}); expect(options.asArgumentsForCucumber(majorVersion)).to.deep.equal(['node', 'cucumber-js']); }); /** * @see https://github.com/cucumber/cucumber-js/blob/main/features/rerun_formatter.feature - */ + */ describe('rerun formatter', () => { it('adds the rerun formatter', () => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { format: 'rerun=@rerun.txt', }); @@ -104,7 +106,7 @@ describe('CucumberOptions', () => { }); it('appends the rerun file', () => { - const options = new CucumberOptions(fsWithRerunFile(true), { + const options = new CucumberOptions(finder, fsWithRerunFile(true), { format: 'rerun=@rerun.txt', rerun: '@rerun.txt' }); @@ -115,7 +117,7 @@ describe('CucumberOptions', () => { }); it('does not append the rerun file when it does not exist', () => { - const options = new CucumberOptions(fsWithRerunFile(false), { + const options = new CucumberOptions(finder, fsWithRerunFile(false), { format: 'rerun=@rerun.txt', rerun: '@rerun.txt' }); @@ -137,7 +139,7 @@ describe('CucumberOptions', () => { ]; given(emptyTags).it('ignores empty tags when generating tag expressions (>=2.x)', ({ tags }) => { - const options = new CucumberOptions(dummyFS(), { tags }); + const options = new CucumberOptions(finder, dummyFS(), { tags }); expect(options.asArgumentsForCucumber(new Version('2.0.0'))).to.deep.equal([ 'node', 'cucumber-js', @@ -145,7 +147,7 @@ describe('CucumberOptions', () => { }); given(emptyTags).it('ignores empty tags when working with Cucumber 1.x', ({ tags }) => { - const options = new CucumberOptions(dummyFS(), { tags }); + const options = new CucumberOptions(finder, dummyFS(), { tags }); expect(options.asArgumentsForCucumber(new Version('2.0.0'))).to.deep.equal([ 'node', 'cucumber-js', @@ -157,7 +159,7 @@ describe('CucumberOptions', () => { new Version('3.0.0'), ]). it('converts a list of tags into a Cucumber expression for Cucumber 2.x and newer', (majorVersion: Version) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { tags: [ '@smoke-test', '~@wip', @@ -171,7 +173,7 @@ describe('CucumberOptions', () => { }); it('passes the tags individually to Cucumber 1.x', () => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { tags: [ '@smoke-test', '~@wip', @@ -202,7 +204,7 @@ describe('CucumberOptions', () => { { description: 'colors on', option: 'colors', state: true, expected: '--colors' }, ]). it('correctly interprets boolean options', ({ option, state, expected }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: state, }); @@ -226,7 +228,7 @@ describe('CucumberOptions', () => { { description: 'colors on', option: 'no-colors', state: false, expected: '--colors' }, ]). it('correctly interprets negated boolean options', ({ option, state, expected }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: state, }); @@ -244,7 +246,7 @@ describe('CucumberOptions', () => { { description: 'name', option: 'name', value: [ 'checkout.*', 'smoke.*' ], expected: [ '--name', 'checkout.*', '--name', 'smoke.*' ] }, ]). it('includes any other options', ({ option, value, expected }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: value, }); @@ -263,7 +265,7 @@ describe('CucumberOptions', () => { { description: 'retryTagFilter', option: 'retryTagFilter', value: '@flaky', expected: [ '--retry-tag-filter', '@flaky' ] }, ]). it('converts camelCased options to kebab-case', ({ option, value, expected }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: value, }); @@ -281,7 +283,7 @@ describe('CucumberOptions', () => { { description: 'empty list', option: 'format', value: [], }, ]). it('ignores empty values', ({ option, value }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: value, }); @@ -299,7 +301,7 @@ describe('CucumberOptions', () => { { description: 'string', option: 'worldParameters', value: '{"baseUrl":"https://example.org"}' }, ]). it('ignores empty values', ({ option, value }) => { - const options = new CucumberOptions(dummyFS(), { + const options = new CucumberOptions(finder, dummyFS(), { [option]: value, }); @@ -309,6 +311,45 @@ describe('CucumberOptions', () => { }); }); }); + + describe('when used to produce configuration for Cucumber API', () => { + + it('resolves spec `paths` as absolute file paths', () => { + const options = new CucumberOptions(finder, dummyFS(), { + paths: [ './features/**/*.feature' ], + }); + + expect(options.asCucumberApiConfiguration().paths).to.deep.equal( + [ + Path.from(__dirname, 'features/passing_scenario.feature').value, + ] + ); + }); + + it('resolves `import` option as absolute file URLs', () => { + const options = new CucumberOptions(finder, dummyFS(), { + import: [ './features/step_definitions/**/*.ts' ], + }); + + expect(options.asCucumberApiConfiguration().import).to.deep.equal( + [ + Path.from(__dirname, 'features/step_definitions/steps.ts').toFileURL().href, + ] + ); + }); + + it('resolves `require` option as absolute file paths', () => { + const options = new CucumberOptions(finder, dummyFS(), { + require: [ './features/step_definitions/**/*.ts' ], + }); + + expect(options.asCucumberApiConfiguration().require).to.deep.equal( + [ + Path.from(__dirname, 'features/step_definitions/steps.ts').value, + ] + ); + }); + }); }); function dummyFS(): FileSystem { @@ -321,4 +362,4 @@ function fsWithRerunFile(hasFile: boolean): FileSystem { return hasFile } } as unknown as FileSystem; -} +} \ No newline at end of file diff --git a/packages/cucumber/src/adapter/CucumberCLIAdapter.ts b/packages/cucumber/src/adapter/CucumberCLIAdapter.ts index 776e4874cf9..4fafc7a4465 100644 --- a/packages/cucumber/src/adapter/CucumberCLIAdapter.ts +++ b/packages/cucumber/src/adapter/CucumberCLIAdapter.ts @@ -1,6 +1,6 @@ import type { TestRunnerAdapter } from '@serenity-js/core/lib/adapter'; -import type { FileSystem, ModuleLoader} from '@serenity-js/core/lib/io'; -import { Version } from '@serenity-js/core/lib/io'; +import type { FileSystem, ModuleLoader } from '@serenity-js/core/lib/io'; +import { FileFinder, Path, Version } from '@serenity-js/core/lib/io'; import type { Outcome } from '@serenity-js/core/lib/model'; import { ExecutionIgnored, ImplementationPending } from '@serenity-js/core/lib/model'; import * as path from 'path'; // eslint-disable-line unicorn/import-style @@ -29,7 +29,7 @@ export class CucumberCLIAdapter implements TestRunnerAdapter { fileSystem: FileSystem, private readonly output: SerenityFormatterOutput, ) { - this.options = new CucumberOptions(fileSystem, config); + this.options = new CucumberOptions(new FileFinder(Path.from(this.loader.cwd)), fileSystem, config); } /** @@ -92,7 +92,7 @@ export class CucumberCLIAdapter implements TestRunnerAdapter { private runScenarios(version: Version, serenityListener: string, pathsToScenarios: string[]): Promise { if (version.isAtLeast(new Version('10.0.0'))) { - return this.runWithCucumber8JavaScriptApi(serenityListener, pathsToScenarios); + return this.runWithCucumber10(serenityListener, pathsToScenarios); } if (version.isAtLeast(new Version('9.0.0'))) { @@ -124,11 +124,28 @@ export class CucumberCLIAdapter implements TestRunnerAdapter { return this.runWithCucumber0to1(argv, serenityListener, pathsToScenarios); } + private async runWithCucumber10(pathToSerenityListener: string, pathsToScenarios: string[]): Promise { + const output = this.output.get(); + const serenityListenerUrl = Path.from(pathToSerenityListener).toFileURL().href; + const outputUrl = output.value() ?? undefined; + + // https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md#ambiguous-colons-in-formats + // https://github.com/cucumber/cucumber-js/issues/2326#issuecomment-1711701382 + return await this.runWithCucumberApi([ + serenityListenerUrl, + outputUrl, + ], pathsToScenarios, output); + } + // https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md private async runWithCucumber8JavaScriptApi(pathToSerenityListener: string, pathsToScenarios: string[]): Promise { + const output = this.output.get(); + return await this.runWithCucumberApi(`${ pathToSerenityListener }:${ output.value() }`, pathsToScenarios, output); + } + + private async runWithCucumberApi(serenityFormatter: string | [string, string?], pathsToScenarios: string[], output: OutputDescriptor): Promise { const configuration = this.options.asCucumberApiConfiguration(); const { loadConfiguration, loadSupport, runCucumber } = this.loader.require('@cucumber/cucumber/api'); - const output = this.output.get(); // https://github.com/cucumber/cucumber-js/blob/main/src/api/environment.ts const environment = { @@ -139,16 +156,16 @@ export class CucumberCLIAdapter implements TestRunnerAdapter { debug: false, }; - configuration.format.push(`${ pathToSerenityListener }:${ output.value() }`) + configuration.format.push(serenityFormatter) configuration.paths = pathsToScenarios; // https://github.com/cucumber/cucumber-js/blob/main/src/configuration/types.ts const { runConfiguration } = await loadConfiguration({ provided: configuration }, environment); - // load the support code upfront - const support = await loadSupport(runConfiguration, environment) - try { + // load the support code upfront + const support = await loadSupport(runConfiguration, environment) + // run cucumber, using the support code we loaded already const { success } = await runCucumber({ ...runConfiguration, support }, environment) await output.cleanUp(); diff --git a/packages/cucumber/src/adapter/CucumberConfig.ts b/packages/cucumber/src/adapter/CucumberConfig.ts index 692dcd518c3..f382792ced1 100644 --- a/packages/cucumber/src/adapter/CucumberConfig.ts +++ b/packages/cucumber/src/adapter/CucumberConfig.ts @@ -7,6 +7,15 @@ */ export interface CucumberConfig { + /** + * Paths to where your feature files are. Note that you don't need to specify the paths when + * using Serenity/JS with WebdriverIO or Protractor, as their respective adapters will do it for you. + * + * #### Learn more + * - [Cucumber docs: configuration](https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md) + */ + paths?: string[]; + /** * Prepare a test run but don't run it * diff --git a/packages/cucumber/src/adapter/CucumberOptions.ts b/packages/cucumber/src/adapter/CucumberOptions.ts index f60f98094a2..a60d88160fc 100644 --- a/packages/cucumber/src/adapter/CucumberOptions.ts +++ b/packages/cucumber/src/adapter/CucumberOptions.ts @@ -1,5 +1,5 @@ import type { IConfiguration } from '@cucumber/cucumber/api'; -import type { FileSystem} from '@serenity-js/core/lib/io'; +import type { FileFinder, FileSystem } from '@serenity-js/core/lib/io'; import { Path, Version } from '@serenity-js/core/lib/io'; import type { CucumberConfig } from './CucumberConfig'; @@ -9,6 +9,7 @@ import type { CucumberConfig } from './CucumberConfig'; */ export class CucumberOptions { constructor( + private readonly finder: FileFinder, private readonly fileSystem: FileSystem, private readonly config: CucumberConfig, ) { @@ -55,20 +56,26 @@ export class CucumberOptions { failFast: this.config.failFast, format: this.asArray(this.config.format), formatOptions: this.config.formatOptions as any, - import: this.asArray(this.config.import), + + paths: this.asArray(this.config.paths) + .flatMap(glob => this.finder.filesMatching(glob).map(path => path.value)), + import: this.asArray(this.config.import) + .flatMap(glob => this.finder.filesMatching(glob).map(path => path.toFileURL().href)), + require: this.asArray(this.config.require) + .flatMap(glob => this.finder.filesMatching(glob).map(path => path.value)), + requireModule: this.asArray(this.config.requireModule), + language: this.config.language, name: this.asArray(this.config.name), - // order: PickleOrder - // paths: string[], - // parallel: number, // this only works when Cucumber is the runner, in which scenario CucumberCLIAdapter is not used anyway publish: false, - require: this.asArray(this.config.require), - requireModule: this.asArray(this.config.requireModule), retry: this.config.retry, retryTagFilter: this.config.retryTagFilter, strict: this.config.strict, tags: this.asArray(this.config.tags).join(' and '), worldParameters: this.config.worldParameters, + + // order: PickleOrder + // parallel: number, // this only works when Cucumber is the runner, in which scenario CucumberCLIAdapter is not used anyway }; } @@ -124,8 +131,8 @@ export class CucumberOptions { private tagsToCucumberExpressions(tags: string[]): string { return tags.filter(tag => !! tag.replace) - .map(tag => tag.replaceAll('~', 'not ')) - .join(' and '); + .map(tag => tag.replaceAll('~', 'not ')) + .join(' and '); } private flagToArg(option: string, value: boolean): string { diff --git a/packages/protractor/spec/adapter/runner/TestRunnerLoader.spec.ts b/packages/protractor/spec/adapter/runner/TestRunnerLoader.spec.ts index 0bd97471daa..53b9ab80ebc 100644 --- a/packages/protractor/spec/adapter/runner/TestRunnerLoader.spec.ts +++ b/packages/protractor/spec/adapter/runner/TestRunnerLoader.spec.ts @@ -100,38 +100,6 @@ describe('TestRunnerLoader', () => { ); }); - it('resolves glob patterns in `require` to absolute paths', () => { - - // eslint-disable-next-line unicorn/consistent-function-scoping - function absolutePathTo(relativePath: string): string { - return Path.from(__dirname, relativePath).value; - } - - const testRunnerLoader = createTestRunnerLoader(exampleRunnerId); - - const CucumberCLIAdapter = sinon.spy(); - moduleLoader.require.withArgs('@serenity-js/cucumber/lib/adapter').returns({ CucumberCLIAdapter, CucumberFormat, StandardOutput, TempFileOutput }) - - const cucumberOpts = { // eslint-disable-line unicorn/prevent-abbreviations - require: [ - 'features/**/*.steps.ts', - ] - }; - - const runner_ = testRunnerLoader.forCucumber(cucumberOpts, { useStandardOutput: false, uniqueFormatterOutputs: false }); - - expect(CucumberCLIAdapter).to.have.been.calledWith( - { - require: [ - absolutePathTo('features/example.steps.ts'), - ] - }, - moduleLoader, - fileSystem, - sinon.match.instanceOf(TempFileOutput) - ); - }); - given([{ description: `doesn't change the output file name when uniqueFormatterOutputs are disabled`, adapterConfig: { useStandardOutput: false, uniqueFormatterOutputs: false }, diff --git a/packages/protractor/src/adapter/runner/TestRunnerLoader.ts b/packages/protractor/src/adapter/runner/TestRunnerLoader.ts index 3a4c9990550..6d8945036c3 100644 --- a/packages/protractor/src/adapter/runner/TestRunnerLoader.ts +++ b/packages/protractor/src/adapter/runner/TestRunnerLoader.ts @@ -65,9 +65,6 @@ export class TestRunnerLoader { const { CucumberCLIAdapter, CucumberFormat, StandardOutput, TempFileOutput } = this.moduleLoader.require('@serenity-js/cucumber/lib/adapter'); const config = new Config(cucumberOpts) - .where('require', requires => - this.finder.filesMatching(requires).map(p => p.value) - ) .whereIf(adapterConfig.uniqueFormatterOutputs, 'format', values => [].concat(values).map(value => { const format = new CucumberFormat(value); diff --git a/packages/webdriverio/src/adapter/TestRunnerLoader.ts b/packages/webdriverio/src/adapter/TestRunnerLoader.ts index 585c78ba7d0..8c2313bb52e 100644 --- a/packages/webdriverio/src/adapter/TestRunnerLoader.ts +++ b/packages/webdriverio/src/adapter/TestRunnerLoader.ts @@ -48,9 +48,6 @@ export class TestRunnerLoader { delete cucumberOptions?.parallel; // WebdriverIO handles that already const cleanedCucumberOptions = new Config(cucumberOptions) - .where('require', requires => - this.finder.filesMatching(requires).map(p => p.value) - ) .where('format', values => [].concat(values).map(value => { const format = new CucumberFormat(value); diff --git a/packages/webdriverio/src/adapter/WebdriverIOFrameworkAdapter.ts b/packages/webdriverio/src/adapter/WebdriverIOFrameworkAdapter.ts index 8bfeff0a4d0..d956b8992bf 100644 --- a/packages/webdriverio/src/adapter/WebdriverIOFrameworkAdapter.ts +++ b/packages/webdriverio/src/adapter/WebdriverIOFrameworkAdapter.ts @@ -102,7 +102,7 @@ export class WebdriverIOFrameworkAdapter { (this.reporter as any)._reporters.push(browserCapabilitiesReporter); // WebdriverIO v8 represents paths to specs as file URIs, so they need to be converted to absolute paths to avoid confusing the test runners like Cucumber - const absolutePaths = this.specs.map(pathToSpec => Path.fromURI(pathToSpec).value); + const absolutePaths = this.specs.map(pathToSpec => Path.from(pathToSpec).value); await this.adapter.load(absolutePaths);