Skip to content

Commit 3fb15d8

Browse files
authored
feat!: update cucumber format parsing (#284)
* feat: update cucumber format parsing * revise implementation and add unit test * support simple format strings * cleanup * rename example formatter in comments * redirect the output to sauce-test-report * fix test * add notes * revise notes * revise notes * oops * support absolute path * add more test cases * update comments * handle file based formatter implemenation * no file uri support in path * support formatOptions * update comment * update comments for sauce cucumber report setting * update test * add an invalid case * kill useless format options * give formatOptions more precise type * revise error msg
1 parent 3538b89 commit 3fb15d8

File tree

3 files changed

+157
-12
lines changed

3 files changed

+157
-12
lines changed

src/cucumber-runner.ts

+77-12
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,32 @@ import type { CucumberRunnerConfig } from './types';
77
import * as utils from './utils';
88
import { NodeContext } from 'sauce-testrunner-utils/lib/types';
99

10-
function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
10+
export function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
1111
const paths: string[] = [];
1212
runCfg.suite.options.paths.forEach((p) => {
1313
paths.push(path.join(runCfg.projectPath, p));
1414
});
1515
const procArgs = [
1616
cucumberBin,
1717
...paths,
18-
'--publish-quiet', // Deprecated in 9.4.0. Will be removed in 11.0.0 or later.
1918
'--force-exit',
2019
'--require-module',
2120
'ts-node/register',
21+
// NOTE: The Cucumber formatter (--format) option uses the "type":"path" format.
22+
// If the "path" is not specified, the output defaults to stdout.
23+
// Cucumber supports only one stdout formatter; if multiple are specified,
24+
// the last one listed takes precedence.
25+
//
26+
// To ensure the Sauce test report file is reliably generated and not overridden
27+
// by a user-specified stdout formatter, configure the following:
28+
// 1. In the --format option, set the output to a file (e.g., cucumber.log) to
29+
// avoid writing to stdout.
30+
// 2. Use the --format-options flag to explicitly specify the outputFile
31+
// (e.g., sauce-test-report.json).
32+
//
33+
// Both settings must be configured correctly to ensure the Sauce test report file is generated.
2234
'--format',
23-
'@saucelabs/cucumber-reporter',
35+
'"@saucelabs/cucumber-reporter":"cucumber.log"',
2436
'--format-options',
2537
JSON.stringify(buildFormatOption(runCfg)),
2638
];
@@ -50,15 +62,12 @@ function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
5062
procArgs.push('-t');
5163
procArgs.push(tag);
5264
});
65+
5366
runCfg.suite.options.format?.forEach((format) => {
5467
procArgs.push('--format');
55-
const opts = format.split(':');
56-
if (opts.length === 2) {
57-
procArgs.push(`${opts[0]}:${path.join(runCfg.assetsDir, opts[1])}`);
58-
} else {
59-
procArgs.push(format);
60-
}
68+
procArgs.push(normalizeFormat(format, runCfg.assetsDir));
6169
});
70+
6271
if (runCfg.suite.options.parallel) {
6372
procArgs.push('--parallel');
6473
procArgs.push(runCfg.suite.options.parallel.toString(10));
@@ -67,6 +76,64 @@ function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
6776
return procArgs;
6877
}
6978

79+
/**
80+
* Normalizes a Cucumber-js format string.
81+
*
82+
* This function handles structured inputs in the format `key:value`, `"key:value"`,
83+
* or `"key":"value"` and returns a normalized string in the form `"key":"value"`.
84+
* For simple inputs (e.g., `usage`) or unstructured formats, the function returns the
85+
* input unchanged.
86+
*
87+
* If the input starts with `file://`, an error is thrown to indicate an invalid format.
88+
*
89+
* @param {string} format - The input format string. Examples include:
90+
* - `"key:value"`
91+
* - `"key":"value"`
92+
* - `key:value`
93+
* - `usage`
94+
* - `"file://implementation":"output_file"`
95+
* @param {string} assetDir - The directory to prepend to the value for relative paths.
96+
* @returns {string} The normalized format string.
97+
*
98+
* Examples:
99+
* - Input: `"html:formatter/report.html"`, `"/project/assets"`
100+
* Output: `"html":"/project/assets/formatter/report.html"`
101+
* - Input: `"usage"`, `"/project/assets"`
102+
* Output: `"usage"`
103+
* - Input: `"file://implementation":"output_file"`, `"/project/assets"`
104+
* Output: `"file://implementation":"output_file"` (unchanged)
105+
*/
106+
export function normalizeFormat(format: string, assetDir: string): string {
107+
// Formats starting with file:// are not supported by the current implementation.
108+
// Restrict users from using this format.
109+
if (format.startsWith('file://')) {
110+
throw new Error(
111+
`Ambiguous colon usage detected. The provided format "${format}" is not allowed.`,
112+
);
113+
}
114+
// Try to match structured inputs in the format key:value, "key:value", or "key":"value".
115+
let match = format.match(/^"?([^:]+):"?([^"]+)"?$/);
116+
117+
if (!match) {
118+
if (!format.startsWith('"file://')) {
119+
return format;
120+
}
121+
122+
// Match file-based structured inputs like "file://implementation":"output_file".
123+
match = format.match(/^"([^"]+)":"([^"]+)"$/);
124+
}
125+
126+
if (!match) {
127+
return format;
128+
}
129+
130+
let [, key, value] = match;
131+
key = key.replaceAll('"', '');
132+
value = value.replaceAll('"', '');
133+
134+
return `"${key}":"${path.join(assetDir, value)}"`;
135+
}
136+
70137
export async function runCucumber(
71138
nodeBin: string,
72139
runCfg: CucumberRunnerConfig,
@@ -142,9 +209,7 @@ export async function runCucumber(
142209
function buildFormatOption(cfg: CucumberRunnerConfig) {
143210
return {
144211
upload: false,
145-
suiteName: cfg.suite.name,
146-
build: cfg.sauce.metadata?.build,
147-
tags: cfg.sauce.metadata?.tags,
148212
outputFile: path.join(cfg.assetsDir, 'sauce-test-report.json'),
213+
...cfg.suite.options.formatOptions,
149214
};
150215
}

src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface CucumberSuite {
101101
import?: string[];
102102
tags?: string[];
103103
format?: string[];
104+
formatOptions?: { [key: string]: string };
104105
parallel?: number;
105106
paths: string[];
106107
};
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const { buildArgs, normalizeFormat } = require('../../../src/cucumber-runner');
2+
3+
describe('buildArgs', () => {
4+
const cucumberBin = '/usr/local/bin/cucumber';
5+
6+
it('should build correct arguments with basic configuration', () => {
7+
const runCfg = {
8+
sauce: {
9+
metadata: {},
10+
},
11+
projectPath: '/project',
12+
assetsDir: '/project/assets',
13+
suite: {
14+
options: {
15+
paths: ['features/test.feature'],
16+
formatOptions: {
17+
myOption: 'test',
18+
},
19+
},
20+
},
21+
};
22+
23+
const result = buildArgs(runCfg, cucumberBin);
24+
25+
expect(result).toEqual([
26+
cucumberBin,
27+
'/project/features/test.feature',
28+
'--force-exit',
29+
'--require-module',
30+
'ts-node/register',
31+
'--format',
32+
'"@saucelabs/cucumber-reporter":"cucumber.log"',
33+
'--format-options',
34+
'{"upload":false,"outputFile":"/project/assets/sauce-test-report.json","myOption":"test"}',
35+
]);
36+
});
37+
});
38+
39+
describe('normalizeFormat', () => {
40+
const assetDir = '/project/assets';
41+
42+
it('should normalize format with both quoted format type and path', () => {
43+
expect(normalizeFormat(`"html":"formatter/report.html"`, assetDir)).toBe(
44+
`"html":"/project/assets/formatter/report.html"`,
45+
);
46+
});
47+
48+
it('should normalize format with only one pair of quote', () => {
49+
expect(normalizeFormat(`"html:formatter/report.html"`, assetDir)).toBe(
50+
`"html":"/project/assets/formatter/report.html"`,
51+
);
52+
});
53+
54+
it('should normalize format with no quotes', () => {
55+
expect(normalizeFormat(`html:formatter/report.html`, assetDir)).toBe(
56+
`"html":"/project/assets/formatter/report.html"`,
57+
);
58+
});
59+
60+
it('should normalize format with file path type', () => {
61+
expect(
62+
normalizeFormat(
63+
`"file://formatter/implementation":"report.json"`,
64+
assetDir,
65+
),
66+
).toBe(`"file://formatter/implementation":"/project/assets/report.json"`);
67+
});
68+
69+
it('should throw an error for an invalid file path type', () => {
70+
expect(() => {
71+
normalizeFormat(`file://formatter/implementation:report.json`, assetDir);
72+
}).toThrow('Ambiguous colon usage detected');
73+
});
74+
75+
it('should return simple strings as-is', () => {
76+
expect(normalizeFormat(`"usage"`, assetDir)).toBe('"usage"');
77+
expect(normalizeFormat(`usage`, assetDir)).toBe('usage');
78+
});
79+
});

0 commit comments

Comments
 (0)