Skip to content

Commit 7c14692

Browse files
authored
Support text/uri-list attachments (#355)
Special support for attachments with MIME type text/uri-list, to render as hyperlinks that open in a new tab. See cucumber/common#2191
1 parent 198a5f0 commit 7c14692

10 files changed

+306
-127
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
9+
### Added
10+
- Support text/uri-list attachments ([#355](https://github.com/cucumber/react-components/pull/355))
911

1012
## [22.2.0] - 2024-06-21
1113
### Added

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ This is fine for simple use cases where results are not important.
3737
To render a `<GherkinDocument>` with results and highlighted [Cucumber Expression parameters](https://cucumber.io/docs/cucumber/cucumber-expressions/) parameters it must be nested inside a
3838
[`<Wrapper>`](src/components/app/Wrapper.tsx) component.
3939

40+
## Attachments
41+
42+
Attachments from test runs are shown with their corresponding steps. The baseline behaviour for attachments is a download button. However, we have some special handling for very common MIME types to make them more useful without leaving the report:
43+
44+
- `image/*` - images are rendered with an `<img/>` tag
45+
- `video/*` - videos are rendered with a `<video/` tag
46+
- `text/x.cucumber.log+plain` - logs (from calls to the `log` function in Cucumber) are rendered as monospace text, and support ANSI colors
47+
- `text/uri-list` - one or more URLs are rendered as links that open in a new tab
48+
- `application/json` - JSON is rendered as monospace text and prettified
49+
- `text/*` - other text types are rendered as monospace text
50+
4051
## Styling
4152

4253
The standard styling comes from wrapping your top-level usage with the `CucumberReact` component (sans-props). There are several ways you can apply different styling to the components.

package-lock.json

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

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
"pretty-quick-staged": "pretty-quick --staged"
2626
},
2727
"dependencies": {
28-
"@cucumber/gherkin-utils": "8.0.6",
29-
"@cucumber/messages": "24.0.1",
30-
"@cucumber/query": "12.0.1",
28+
"@cucumber/gherkin-utils": "9.0.0",
29+
"@cucumber/messages": "25.0.1",
30+
"@cucumber/query": "12.2.0",
3131
"@cucumber/tag-expressions": "6.1.0",
3232
"@fortawesome/fontawesome-svg-core": "6.2.1",
3333
"@fortawesome/free-solid-svg-icons": "6.2.1",
@@ -55,8 +55,8 @@
5555
"react-dom": "~18"
5656
},
5757
"devDependencies": {
58-
"@cucumber/compatibility-kit": "15.0.0",
59-
"@cucumber/fake-cucumber": "16.4.0",
58+
"@cucumber/compatibility-kit": "16.1.0",
59+
"@cucumber/fake-cucumber": "16.5.0",
6060
"@cucumber/gherkin": "28.0.0",
6161
"@cucumber/gherkin-streams": "5.0.1",
6262
"@cucumber/message-streams": "4.0.1",

src/components/customise/customRendering.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface AttachmentProps {
3131
attachment: messages.Attachment
3232
}
3333

34-
export type AttachmentClasses = Styles<'text' | 'log' | 'icon' | 'image'>
34+
export type AttachmentClasses = Styles<'text' | 'log' | 'icon' | 'image' | 'links'>
3535

3636
export interface BackgroundProps {
3737
background: messages.Background

src/components/gherkin/attachment/Attachment.module.scss

+18
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,21 @@
3737
max-width: 100%;
3838
margin-top: 0.5em;
3939
}
40+
41+
.links {
42+
list-style: none;
43+
display: flex;
44+
flex-direction: column;
45+
align-items: flex-start;
46+
padding: 0;
47+
margin: 0;
48+
49+
a {
50+
text-decoration: none;
51+
color: $anchorColor;
52+
}
53+
54+
svg {
55+
margin-right: 0.5em;
56+
}
57+
}

src/components/gherkin/attachment/Attachment.spec.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,41 @@ describe('<Attachment>', () => {
166166
'<span style="color:#000">black<span style="color:#AAA">white</span></span>'
167167
)
168168
})
169+
170+
it('renders a link', () => {
171+
const attachment: messages.Attachment = {
172+
mediaType: 'text/uri-list',
173+
contentEncoding: AttachmentContentEncoding.IDENTITY,
174+
body: 'https://cucumber.io',
175+
}
176+
177+
render(<Attachment attachment={attachment} />)
178+
179+
expect(screen.getByRole('link', { name: 'https://cucumber.io' })).to.be.visible
180+
expect(screen.getByRole('link', { name: 'https://cucumber.io' })).to.have.attr(
181+
'href',
182+
'https://cucumber.io'
183+
)
184+
})
185+
186+
it('renders multiple links and ignores blank lines', () => {
187+
const attachment: messages.Attachment = {
188+
mediaType: 'text/uri-list',
189+
contentEncoding: AttachmentContentEncoding.IDENTITY,
190+
body: `https://github.com/cucumber/cucumber-js
191+
https://github.com/cucumber/cucumber-jvm
192+
https://github.com/cucumber/cucumber-ruby
193+
`,
194+
}
195+
196+
render(<Attachment attachment={attachment} />)
197+
198+
expect(screen.getAllByRole('link').length).to.eq(3)
199+
expect(screen.getByRole('link', { name: 'https://github.com/cucumber/cucumber-js' })).to.be
200+
.visible
201+
expect(screen.getByRole('link', { name: 'https://github.com/cucumber/cucumber-jvm' })).to.be
202+
.visible
203+
expect(screen.getByRole('link', { name: 'https://github.com/cucumber/cucumber-ruby' })).to.be
204+
.visible
205+
})
169206
})

src/components/gherkin/attachment/Attachment.stories.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ Log.args = {
3333
},
3434
} satisfies AttachmentProps
3535

36+
export const Link = Template.bind({})
37+
Link.args = {
38+
attachment: {
39+
mediaType: 'text/uri-list',
40+
contentEncoding: AttachmentContentEncoding.IDENTITY,
41+
body: 'https://cucumber.io',
42+
},
43+
} satisfies AttachmentProps
44+
45+
export const MultipleLinks = Template.bind({})
46+
MultipleLinks.args = {
47+
attachment: {
48+
mediaType: 'text/uri-list',
49+
contentEncoding: AttachmentContentEncoding.IDENTITY,
50+
body: `https://github.com/cucumber/cucumber-js
51+
https://github.com/cucumber/cucumber-jvm
52+
https://github.com/cucumber/cucumber-ruby`,
53+
},
54+
} satisfies AttachmentProps
55+
3656
export const ExternalisedImage = Template.bind({})
3757
ExternalisedImage.storyName = 'Externalised image'
3858
ExternalisedImage.args = {

src/components/gherkin/attachment/Attachment.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { ErrorMessage } from '../ErrorMessage.js'
1111
import defaultStyles from './Attachment.module.scss'
1212
import { Image } from './Image.js'
13+
import { Links } from './Links.js'
1314
import { Log } from './Log.js'
1415
import { Text } from './Text.js'
1516
import { Unknown } from './Unknown.js'
@@ -25,6 +26,8 @@ const DefaultRenderer: DefaultComponent<AttachmentProps, AttachmentClasses> = ({
2526
return <Video attachment={attachment} />
2627
} else if (attachment.mediaType == 'text/x.cucumber.log+plain') {
2728
return <Log attachment={attachment} classes={styles} />
29+
} else if (attachment.mediaType == 'text/uri-list') {
30+
return <Links attachment={attachment} classes={styles} />
2831
} else if (attachment.mediaType.match(/^text\//)) {
2932
return <Text attachment={attachment} classes={styles} />
3033
} else if (attachment.mediaType.match(/^application\/json/)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Attachment } from '@cucumber/messages'
2+
import { faLink } from '@fortawesome/free-solid-svg-icons'
3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4+
import React, { FC } from 'react'
5+
6+
import { AttachmentClasses } from '../../customise/index.js'
7+
import { useText } from './useText.js'
8+
9+
export const Links: FC<{
10+
attachment: Attachment
11+
classes: AttachmentClasses
12+
}> = ({ attachment, classes }) => {
13+
const { content } = useText(attachment)
14+
return (
15+
<ul className={classes.links}>
16+
{content
17+
.split('\n')
18+
.filter((line) => !!line)
19+
.map((line, index) => {
20+
return (
21+
<li key={index}>
22+
<a href={line} target="_blank" rel="noreferrer">
23+
<FontAwesomeIcon icon={faLink} />
24+
{line}
25+
</a>
26+
</li>
27+
)
28+
})}
29+
</ul>
30+
)
31+
}

0 commit comments

Comments
 (0)