Skip to content

Commit 7ef4d0e

Browse files
authored
Support externalising attachments in HTML formatter (#2413)
1 parent 551efa2 commit 7ef4d0e

21 files changed

+332
-41
lines changed

.gitignore

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ coverage/
88
lib/
99
node_modules
1010
tmp/
11-
reports/*.html
12-
reports/*.ndjson
13-
reports/*.txt
14-
reports/*.xml
11+
reports/
1512
yarn-error.log
1613
.vscode
1714
.DS_Store

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber.
99

1010
## [Unreleased]
11+
### Added
12+
- Support externalising attachments in HTML formatter (see [documentation](./docs/formatters.md#html)) ([#2413](https://github.com/cucumber/cucumber-js/pull/2413))
13+
- Support linking to external content via attachments (see [documentation](./docs/support_files/attachments.md#links)) ([#2413](https://github.com/cucumber/cucumber-js/pull/2413))
1114

1215
## [10.8.0] - 2024-05-26
1316
### Added

docs/formatters.md

+16
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ You can:
9090
- Filter to specific statuses
9191
- Search by keywords or tag expressions
9292

93+
#### Attachments
94+
95+
By default, the HTML report includes all attachments from your test run as embedded data. This is simple and convenient, with the file being completely standalone and portable. But it can make for a _very_ large file if you have a lot of large attachments like screenshots, videos and other media. You can optionally have attachments saved to external files instead, if that works better for you:
96+
97+
```json
98+
{
99+
"formatOptions": {
100+
"html": {
101+
"externalAttachments": true
102+
}
103+
}
104+
}
105+
```
106+
107+
This will cause attachments to be saved in the same directory as the report file, with filenames that look like `attachment-8e7c5d3d-1ef0-4be6-86e0-16362bad9531.png`. If you want to put the report file somewhere - say, a web server - to be viewed, you'll need to bring those files along with it.
108+
93109
### `message`
94110

95111
Outputs all the [Cucumber Messages](https://github.com/cucumber/messages) for the test run as newline-delimited JSON, which can then be consumed by other tools.

docs/support_files/attachments.md

+22
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,25 @@ After(function () {
127127
```
128128

129129
Anything you log will be attached as a string with a MIME type of `text/x.cucumber.log+plain`
130+
131+
## Links
132+
133+
You can attach one or more links from your support code with the `link` function:
134+
135+
```javascript
136+
var {Before, After} = require('@cucumber/cucumber');
137+
138+
Before(function () {
139+
this.link('https://cucumber.io');
140+
});
141+
142+
After(function () {
143+
this.link(
144+
'https://github.com/cucumber/cucumber-js',
145+
'https://github.com/cucumber/cucumber-jvm',
146+
'https://github.com/cucumber/cucumber-ruby'
147+
);
148+
});
149+
```
150+
151+
Links will be attached as a string with a MIME type of `text/uri-list`

docs/support_files/world.md

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ By default, the world is an instance of Cucumber's built-in `World` class. Cucum
6060

6161
* `this.attach`: a method for adding [attachments](./attachments.md) to hooks/steps
6262
* `this.log`: a method for [logging](./attachments.md#logging) information from hooks/steps
63+
* `this.link`: a method for [linking](./attachments.md#links) to URLs from hooks/steps
6364
* `this.parameters`: an object of parameters passed in via configuration (see below)
6465

6566
Your custom world will also receive these arguments, but it's up to you to decide what to do with them and they can be safely ignored.

exports/root/report.api.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ export interface IWorld<ParametersType = any> {
339339
// (undocumented)
340340
readonly attach: ICreateAttachment;
341341
// (undocumented)
342+
readonly link: ICreateLink;
343+
// (undocumented)
342344
readonly log: ICreateLog;
343345
// (undocumented)
344346
readonly parameters: ParametersType;
@@ -349,6 +351,8 @@ export interface IWorldOptions<ParametersType = any> {
349351
// (undocumented)
350352
attach: ICreateAttachment;
351353
// (undocumented)
354+
link: ICreateLink;
355+
// (undocumented)
352356
log: ICreateLog;
353357
// (undocumented)
354358
parameters: ParametersType;
@@ -532,10 +536,12 @@ export const When: IDefineStep_2;
532536

533537
// @public (undocumented)
534538
export class World<ParametersType = any> implements IWorld<ParametersType> {
535-
constructor({ attach, log, parameters }: IWorldOptions<ParametersType>);
539+
constructor({ attach, log, link, parameters, }: IWorldOptions<ParametersType>);
536540
// (undocumented)
537541
readonly attach: ICreateAttachment;
538542
// (undocumented)
543+
readonly link: ICreateLink;
544+
// (undocumented)
539545
readonly log: ICreateLog;
540546
// (undocumented)
541547
readonly parameters: ParametersType;

features/html_formatter.feature

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Feature: HTML formatter
2+
3+
Rule: Attachments except logs are externalised based on the externalAttachments option
4+
5+
Background:
6+
Given a file named "features/a.feature" with:
7+
"""
8+
Feature: a feature
9+
Scenario: a scenario
10+
Given a step
11+
"""
12+
And a file named "features/steps.js" with:
13+
"""
14+
const {Given, world} = require('@cucumber/cucumber')
15+
16+
Given('a step', () => {
17+
world.log('Logging some info')
18+
world.link('https://cucumber.io')
19+
world.attach(btoa('Base64 text'), 'base64:text/plain')
20+
world.attach('Plain text', 'text/plain')
21+
})
22+
"""
23+
24+
Scenario: Without externalAttachments option
25+
When I run cucumber-js with `--format html:html.out`
26+
Then it passes
27+
And the html formatter output is complete
28+
And the formatter has no externalised attachments
29+
30+
Scenario: With externalAttachments option
31+
When I run cucumber-js with `--format html:html.out --format-options '{"html":{"externalAttachments":true}}'`
32+
Then it passes
33+
And the html formatter output is complete
34+
And the formatter has these external attachments:
35+
| Base64 text |
36+
| Plain text |

features/step_definitions/formatter_steps.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
22
import { expect, use } from 'chai'
33
import chaiExclude from 'chai-exclude'
44
import fs from 'mz/fs'
5-
import { Then } from '../../'
5+
import { Then, DataTable } from '../../'
66
import {
77
ignorableKeys,
88
normalizeJsonOutput,
@@ -69,3 +69,27 @@ Then('the html formatter output is complete', async function (this: World) {
6969
expect(actual).to.contain('<html lang="en">')
7070
expect(actual).to.contain('</html>')
7171
})
72+
73+
Then(
74+
'the formatter has no externalised attachments',
75+
async function (this: World) {
76+
const actual = fs
77+
.readdirSync(this.tmpDir)
78+
.filter((filename) => filename.startsWith('attachment-')).length
79+
expect(actual).to.eq(0)
80+
}
81+
)
82+
83+
Then(
84+
'the formatter has these external attachments:',
85+
async function (this: World, table: DataTable) {
86+
const actual = fs
87+
.readdirSync(this.tmpDir)
88+
.filter((filename) => filename.startsWith('attachment-'))
89+
.map((filename) =>
90+
fs.readFileSync(path.join(this.tmpDir, filename), { encoding: 'utf-8' })
91+
)
92+
actual.sort()
93+
expect(actual).to.deep.eq(table.raw().map((row) => row[0]))
94+
}
95+
)

package-lock.json

+29-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"@cucumber/gherkin": "28.0.0",
219219
"@cucumber/gherkin-streams": "5.0.1",
220220
"@cucumber/gherkin-utils": "9.0.0",
221-
"@cucumber/html-formatter": "21.3.1",
221+
"@cucumber/html-formatter": "21.6.0",
222222
"@cucumber/message-streams": "4.0.1",
223223
"@cucumber/messages": "24.1.0",
224224
"@cucumber/tag-expressions": "6.1.0",
@@ -239,6 +239,7 @@
239239
"lodash.merge": "^4.6.2",
240240
"lodash.mergewith": "^4.6.2",
241241
"luxon": "3.2.1",
242+
"mime": "^3.0.0",
242243
"mkdirp": "^2.1.5",
243244
"mz": "^2.7.0",
244245
"progress": "^2.0.3",

src/api/formatters.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export async function initializeFormatters({
3939

4040
async function initializeFormatter(
4141
stream: IFormatterStream,
42+
directory: string | undefined,
4243
target: string,
4344
specifier: string
4445
): Promise<void> {
@@ -73,21 +74,25 @@ export async function initializeFormatters({
7374
await pluginManager.initFormatter(
7475
implementation,
7576
configuration.options,
76-
stream.write.bind(stream)
77+
logger,
78+
stream.write.bind(stream),
79+
directory
7780
)
7881
if (stream !== stdout) {
7982
cleanupFns.push(promisify<any>(stream.end.bind(stream)))
8083
}
8184
}
8285
}
8386

84-
await initializeFormatter(stdout, 'stdout', configuration.stdout)
87+
await initializeFormatter(stdout, undefined, 'stdout', configuration.stdout)
8588
for (const [target, specifier] of Object.entries(configuration.files)) {
86-
await initializeFormatter(
87-
await createStream(target, onStreamError, cwd, logger),
89+
const { stream, directory } = await createStream(
8890
target,
89-
specifier
91+
onStreamError,
92+
cwd,
93+
logger
9094
)
95+
await initializeFormatter(stream, directory, target, specifier)
9196
}
9297

9398
return async function () {

0 commit comments

Comments
 (0)