Skip to content

Commit

Permalink
feature: add experimental support of steps option (related to #94)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Apr 9, 2024
1 parent 692e783 commit a626caa
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## dev
* feature: add experimental support of `steps` option (related to [#94](https://github.com/vitalets/playwright-bdd/issues/94))

## 6.1.1
* fix: support stacktrace for Cucumber 10.4

Expand Down
14 changes: 13 additions & 1 deletion docs/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,16 @@ Verbose output.
- Type: `boolean`
- Default: `undefined`

If this option is enabled, playwright-bdd will add special attachments with BDD data, required for Cucumber reports. It gets enabled automatically, when you use `cucmberReporter()` helper. But for scenarios with [reports merging](reporters/cucumber.md#merge-reports), you need to manually set `enrichReporterData: true` when generating **blob** report.
If this option is enabled, playwright-bdd will add special attachments with BDD data, required for Cucumber reports. It gets enabled automatically, when you use `cucmberReporter()` helper. But for scenarios with [reports merging](reporters/cucumber.md#merge-reports), you need to manually set `enrichReporterData: true` when generating **blob** report.

## steps

?> Experimental

- Type: `string | string[]`
- Default: `undefined`

Paths to search for step definitions. Can be directory or glob pattern.
Example: `steps/**/*.ts`.
If you don't specify file extension, by default the following extensions are appended to the pattern: `*.{js,mjs,cjs,ts,mts,cts}`.
This option will eventually replace Cucumber options `require`, `requireModule`, and `import`.
2 changes: 1 addition & 1 deletion lint-staged.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function test(changedFiles) {
const testHelpersChanged = testDirFiles.length !== testFiles.length;
const srcFiles = micromatch(changedFiles, 'src/**/*.{js,mjs,ts}');
const packageFiles = micromatch(changedFiles, 'package*.json');
const testDirs = testDirFiles.map((file) => file.split(path.sep).slice(0, 2));
const testDirs = testDirFiles.map((file) => file.split(path.sep).slice(0, 2).join(path.sep));
// if changes only in test/* -> run only these tests
// if changes in src|package.json -> run tests on test dirs + smoke test dirs
if (testHelpersChanged || srcFiles.length || packageFiles.length) {
Expand Down
12 changes: 12 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ type OwnConfig = {
featuresRoot?: string;
/** Add special BDD attachments for Cucumber reports */
enrichReporterData?: boolean;
/**
* Step definition patterns, e.g. 'steps/*.{ts,js}'.
* Always use forward slash.
* Will replace Cucumber's 'require' / 'import'
* @experimental
*/
steps?: string | string[];
};

export const defaults: Required<
Expand Down Expand Up @@ -68,6 +75,10 @@ function getConfig(configDir: string, inputConfig?: BDDInputConfig): BDDConfig {
? path.resolve(configDir, config.featuresRoot)
: configDir;

if (config.steps && (config.require || config.import)) {
throw new Error(`Config option 'steps' can't be used together with 'require' or 'import'`);
}

return {
...config,
// important to resolve outputDir as it is used as unique key for input configs
Expand All @@ -90,6 +101,7 @@ export function extractCucumberConfig(config: BDDConfig): CucumberConfig {
tags: true,
featuresRoot: true,
enrichReporterData: true,
steps: true,
};
const keys = Object.keys(omitProps) as (keyof OwnConfig)[];
const cucumberConfig = { ...config };
Expand Down
32 changes: 32 additions & 0 deletions src/cucumber/loadStepsOwn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Load steps using Playwright's import instead of Cucumber's tryRequire.
* Based on: https://github.com/cucumber/cucumber-js/blob/main/src/api/support.ts
*/
import { IdGenerator } from '@cucumber/messages';
import { supportCodeLibraryBuilder } from '@cucumber/cucumber';
import { ISupportCodeLibrary } from './types';
import { resolveFiles } from '../utils/paths';
import { requireTransform } from '../playwright/transform';
import { toArray } from '../utils';

const newId = IdGenerator.uuid();

export async function loadStepsOwn(
cwd: string,
stepPaths: string | string[],
): Promise<ISupportCodeLibrary> {
supportCodeLibraryBuilder.reset(cwd, newId, {
requireModules: [],
requirePaths: [],
importPaths: [],
});

const stepFiles = await resolveFiles(cwd, toArray(stepPaths), '{js,mjs,cjs,ts,mts,cts}');
const { requireOrImport } = requireTransform();

for (const file of stepFiles) {
await requireOrImport(file);
}

return supportCodeLibraryBuilder.finalize();
}
2 changes: 1 addition & 1 deletion src/gen/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { jsStringWrap } from '../utils/jsStringWrap';
import { TestNode } from './testNode';
import { BddWorldFixtures } from '../run/bddWorld';
import { TestMetaBuilder } from './testMeta';
import { toPosixPath } from '../utils';
import { playwrightVersion } from '../playwright/utils';
import { DescribeConfigureOptions } from '../playwright/types';
import { toPosixPath } from '../utils/paths';

const supportsTags = playwrightVersion >= '1.42.0';

Expand Down
5 changes: 4 additions & 1 deletion src/gen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { exit, withExitHandler } from '../utils/exit';
import { hasCustomTest } from '../steps/createBdd';
import { ISupportCodeLibrary } from '../cucumber/types';
import { resovleFeaturePaths } from '../cucumber/resolveFeaturePaths';
import { loadStepsOwn } from '../cucumber/loadStepsOwn';

/* eslint-disable @typescript-eslint/no-non-null-assertion */

Expand Down Expand Up @@ -95,7 +96,9 @@ export class TestFilesGenerator {
const { requirePaths, importPaths } = this.runConfiguration.support;
this.logger.log(`Loading steps from: ${requirePaths.concat(importPaths).join(', ')}`);
const environment = { cwd: getPlaywrightConfigDir() };
this.supportCodeLibrary = await loadSteps(this.runConfiguration, environment);
this.supportCodeLibrary = this.config.steps
? await loadStepsOwn(environment.cwd, this.config.steps)
: await loadSteps(this.runConfiguration, environment);
await this.loadDecoratorSteps();
this.logger.log(`Loaded steps: ${this.supportCodeLibrary.stepDefinitions.length}`);
}
Expand Down
6 changes: 5 additions & 1 deletion src/run/bddFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TestMeta, TestMetaMap, getTestMeta } from '../gen/testMeta';
import { logger } from '../utils/logger';
import { getEnrichReporterData } from '../config/enrichReporterData';
import { BddDataManager } from './bddData';
import { loadStepsOwn } from '../cucumber/loadStepsOwn';

// BDD fixtures prefixed with '$' to avoid collision with user's fixtures.

Expand Down Expand Up @@ -67,8 +68,11 @@ export const test = base.extend<BddFixtures, BddFixturesWorker>({
environment,
);

const supportCodeLibrary = await loadSteps(runConfiguration, environment);
const supportCodeLibrary = config.steps
? await loadStepsOwn(environment.cwd, config.steps)
: await loadSteps(runConfiguration, environment);
appendDecoratorSteps(supportCodeLibrary);

const World = getWorldConstructor(supportCodeLibrary);

await use({ runConfiguration, supportCodeLibrary, World, config });
Expand Down
8 changes: 2 additions & 6 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ export function omit<T extends object, K extends keyof T>(obj: T, key: K) {
return res as Omit<T, K>;
}

/**
* Returns path with "/" separator on all platforms.
* See: https://stackoverflow.com/questions/53799385/how-can-i-convert-a-windows-path-to-posix-path-using-node-path
*/
export function toPosixPath(somePath: string) {
return somePath.split(path.sep).join(path.posix.sep);
export function toArray<T>(value: T | T[]) {
return Array.isArray(value) ? value : [value];
}
42 changes: 42 additions & 0 deletions src/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from 'node:path';
import fg from 'fast-glob';

/**
* Returns path with "/" separator on all platforms.
* See: https://stackoverflow.com/questions/53799385/how-can-i-convert-a-windows-path-to-posix-path-using-node-path
*/
export function toPosixPath(somePath: string) {
return somePath.split(path.sep).join(path.posix.sep);
}

/**
* Resolves patterns to list of files.
* Extension can be a list: {js,ts}
* See: https://github.com/cucumber/cucumber-js/blob/main/src/paths/paths.ts
*/
export async function resolveFiles(cwd: string, patterns: string[], extension: string) {
const finalPatterns = patterns.map((pattern) => finalizePattern(pattern, extension));
return fg.glob(finalPatterns, { cwd, absolute: true, dot: true });
}

/**
* Appends file extension(s) to pattern.
* Example: 'path/to/dir' -> 'path/to/dir/** /*.{js,ts}'
*/
export function finalizePattern(pattern: string, extension: string) {
// On Windows convert path to forward slash.
// Note: pattern must always use forward slash "/",
// but directory can be resolved dynamically via path.xxx methods
// that return backslash on Windows.
if (path.sep === '\\') pattern = toPosixPath(pattern);
switch (true) {
case pattern.endsWith('**'):
return `${pattern}/*.${extension}`;
case pattern.endsWith('*'):
return `${pattern}.${extension}`;
case path.extname(pattern) === '':
return `${pattern.replace(/\/+$/, '')}/**/*.${extension}`;
default:
return pattern;
}
}
4 changes: 4 additions & 0 deletions test/load-steps/features/sample.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Feature: load steps

Scenario: create todos
When I create todo
3 changes: 3 additions & 0 deletions test/load-steps/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"description": "This file is required for Playwright to consider this dir as a <package-json dir>. It ensures to load 'playwright-bdd' from './test/node_modules/playwright-bdd' and output './test-results' here to avoid conficts."
}
11 changes: 11 additions & 0 deletions test/load-steps/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from '@playwright/test';
import { defineBddConfig } from 'playwright-bdd';

const testDir = defineBddConfig({
paths: ['features/*.feature'],
steps: 'steps/*',
});

export default defineConfig({
testDir,
});
5 changes: 5 additions & 0 deletions test/load-steps/steps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createBdd } from 'playwright-bdd';

const { When } = createBdd();

When('I create todo', async () => {});
7 changes: 7 additions & 0 deletions test/load-steps/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test, TestDir, execPlaywrightTest } from '../_helpers/index.mjs';

const testDir = new TestDir(import.meta);

test(testDir.name, () => {
execPlaywrightTest(testDir.name);
});
23 changes: 23 additions & 0 deletions test/unit/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import path from 'node:path';
import { test, expect, TestDir } from '../_helpers/index.mjs';

const testDir = new TestDir(import.meta);

test(`${testDir.name} (finalizePattern)`, async () => {
const modulePath = path.resolve('dist/utils/paths.js');
const { finalizePattern } = await import(modulePath);

expect(finalizePattern('a/steps', 'js')).toEqual('a/steps/**/*.js');
expect(finalizePattern('a/steps/', 'js')).toEqual('a/steps/**/*.js');
expect(finalizePattern('a/steps//', 'js')).toEqual('a/steps/**/*.js');
expect(finalizePattern('a/steps/**', 'js')).toEqual('a/steps/**/*.js');
expect(finalizePattern('a/steps/*', 'js')).toEqual('a/steps/*.js');
expect(finalizePattern('a/steps/*.js', 'js')).toEqual('a/steps/*.js');
expect(finalizePattern('a/steps/**/*.js', 'js')).toEqual('a/steps/**/*.js');

// backslash (on win)
if (path.sep === '\\') {
expect(finalizePattern('a\\steps', 'js')).toEqual('a/steps/**/*.js');
expect(finalizePattern('a\\steps\\**', 'js')).toEqual('a/steps/**/*.js');
}
});

0 comments on commit a626caa

Please sign in to comment.