Skip to content

Commit 48148d4

Browse files
authored
add ability to download unsupported attachments (#333)
1 parent c27b94c commit 48148d4

12 files changed

+235
-111
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+
- Add ability to download unsupported attachments ([#333](https://github.com/cucumber/react-components/pull/333))
911

1012
## [21.0.1] - 2022-11-26
1113
### Fixed

package-lock.json

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

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"elasticlunr": "0.9.5",
4141
"hast-util-sanitize": "3.0.2",
4242
"highlight-words": "1.2.1",
43+
"mime-types": "^2.1.35",
4344
"react-accessible-accordion": "5.0.0",
4445
"react-markdown": "6.0.3",
4546
"rehype-raw": "5.1.0",
@@ -52,7 +53,7 @@
5253
"react-dom": "~18"
5354
},
5455
"devDependencies": {
55-
"@cucumber/compatibility-kit": "^11.0.0",
56+
"@cucumber/compatibility-kit": "^12.0.0",
5657
"@cucumber/fake-cucumber": "^16.0.0",
5758
"@cucumber/gherkin": "^26.0.0",
5859
"@cucumber/gherkin-streams": "^5.0.1",
@@ -65,6 +66,7 @@
6566
"@types/glob": "8.0.0",
6667
"@types/jest": "^29.0.0",
6768
"@types/jsdom": "20.0.1",
69+
"@types/mime-types": "^2.1.1",
6870
"@types/node": "18.11.18",
6971
"@types/react": "^18.0.26",
7072
"@types/react-dom": "^18.0.9",

src/components/app/GherkinDocumentList.tsx

+45-46
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as messages from '@cucumber/messages'
22
import { getWorstTestStepResult } from '@cucumber/messages'
33
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
44
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5-
import React from 'react'
5+
import React, { FunctionComponent, useContext, useMemo, useState } from 'react'
66
import {
77
Accordion,
88
AccordionItem,
@@ -14,9 +14,7 @@ import {
1414
import CucumberQueryContext from '../../CucumberQueryContext'
1515
import GherkinQueryContext from '../../GherkinQueryContext'
1616
import UriContext from '../../UriContext'
17-
import { GherkinDocument } from '../gherkin/GherkinDocument'
18-
import { MDG } from '../gherkin/MDG'
19-
import { StatusIcon } from '../gherkin/StatusIcon'
17+
import { GherkinDocument, MDG, StatusIcon } from '../gherkin'
2018
import styles from './GherkinDocumentList.module.scss'
2119

2220
interface IProps {
@@ -25,44 +23,43 @@ interface IProps {
2523
preExpand?: boolean
2624
}
2725

28-
export const GherkinDocumentList: React.FunctionComponent<IProps> = ({
29-
gherkinDocuments,
30-
preExpand,
31-
}) => {
32-
const gherkinQuery = React.useContext(GherkinQueryContext)
33-
const cucumberQuery = React.useContext(CucumberQueryContext)
34-
26+
export const GherkinDocumentList: FunctionComponent<IProps> = ({ gherkinDocuments, preExpand }) => {
27+
const gherkinQuery = useContext(GherkinQueryContext)
28+
const cucumberQuery = useContext(CucumberQueryContext)
3529
const gherkinDocs = gherkinDocuments || gherkinQuery.getGherkinDocuments()
36-
37-
const entries: Array<[string, messages.TestStepResultStatus]> = gherkinDocs.map(
38-
(gherkinDocument) => {
39-
if (!gherkinDocument.uri) throw new Error('No url for gherkinDocument')
40-
const gherkinDocumentStatus = gherkinDocument.feature
41-
? getWorstTestStepResult(
42-
cucumberQuery.getPickleTestStepResults(gherkinQuery.getPickleIds(gherkinDocument.uri))
43-
).status
44-
: messages.TestStepResultStatus.UNDEFINED
45-
return [gherkinDocument.uri, gherkinDocumentStatus]
46-
}
47-
)
48-
const gherkinDocumentStatusByUri = new Map(entries)
49-
50-
// Pre-expand any document that is *not* passed - assuming this is what people want to look at first
51-
const preExpanded = preExpand
52-
? (gherkinDocs
53-
.filter(
54-
(doc) =>
55-
doc.uri &&
56-
gherkinDocumentStatusByUri.get(doc.uri) !== messages.TestStepResultStatus.PASSED
57-
)
58-
.map((doc) => doc.uri) as string[])
59-
: []
30+
const gherkinDocumentStatusByUri = useMemo(() => {
31+
const entries: Array<[string, messages.TestStepResultStatus]> = gherkinDocs.map(
32+
(gherkinDocument) => {
33+
if (!gherkinDocument.uri) throw new Error('No url for gherkinDocument')
34+
const gherkinDocumentStatus = gherkinDocument.feature
35+
? getWorstTestStepResult(
36+
cucumberQuery.getPickleTestStepResults(gherkinQuery.getPickleIds(gherkinDocument.uri))
37+
).status
38+
: messages.TestStepResultStatus.UNDEFINED
39+
return [gherkinDocument.uri, gherkinDocumentStatus]
40+
}
41+
)
42+
return new Map(entries)
43+
}, [gherkinDocs, gherkinQuery, cucumberQuery])
44+
const [expanded, setExpanded] = useState<Array<string | number>>(() => {
45+
// Pre-expand any document that is *not* passed - assuming this is what people want to look at first
46+
return preExpand
47+
? (gherkinDocs
48+
.filter(
49+
(doc) =>
50+
doc.uri &&
51+
gherkinDocumentStatusByUri.get(doc.uri) !== messages.TestStepResultStatus.PASSED
52+
)
53+
.map((doc) => doc.uri) as string[])
54+
: []
55+
})
6056

6157
return (
6258
<Accordion
6359
allowMultipleExpanded={true}
6460
allowZeroExpanded={true}
65-
preExpanded={preExpanded}
61+
preExpanded={expanded}
62+
onChange={setExpanded}
6663
className={styles.accordion}
6764
>
6865
{gherkinDocs.map((doc) => {
@@ -73,7 +70,7 @@ export const GherkinDocumentList: React.FunctionComponent<IProps> = ({
7370
if (!source) throw new Error(`No source for ${doc.uri}`)
7471

7572
return (
76-
<AccordionItem key={doc.uri} className={styles.accordionItem}>
73+
<AccordionItem key={doc.uri} uuid={doc.uri} className={styles.accordionItem}>
7774
<AccordionItemHeading>
7875
<AccordionItemButton className={styles.accordionButton}>
7976
<FontAwesomeIcon
@@ -87,15 +84,17 @@ export const GherkinDocumentList: React.FunctionComponent<IProps> = ({
8784
<span>{doc.uri}</span>
8885
</AccordionItemButton>
8986
</AccordionItemHeading>
90-
<AccordionItemPanel className={styles.accordionPanel}>
91-
<UriContext.Provider value={doc.uri}>
92-
{source.mediaType === messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN ? (
93-
<GherkinDocument gherkinDocument={doc} source={source} />
94-
) : (
95-
<MDG uri={doc.uri}>{source.data}</MDG>
96-
)}
97-
</UriContext.Provider>
98-
</AccordionItemPanel>
87+
{expanded.includes(doc.uri) && (
88+
<AccordionItemPanel className={styles.accordionPanel}>
89+
<UriContext.Provider value={doc.uri}>
90+
{source.mediaType === messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN ? (
91+
<GherkinDocument gherkinDocument={doc} source={source} />
92+
) : (
93+
<MDG uri={doc.uri}>{source.data}</MDG>
94+
)}
95+
</UriContext.Provider>
96+
</AccordionItemPanel>
97+
)}
9998
</AccordionItem>
10099
)
101100
})}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@import '../../styles/theming';
2+
3+
.navigationButton {
4+
display: flex;
5+
align-items: center;
6+
gap: 4px;
7+
background-color: transparent;
8+
color: $anchorColor;
9+
font-family: inherit;
10+
font-size: inherit;
11+
padding: 0;
12+
border: 0;
13+
margin: 0 0 0.5em 0;
14+
cursor: pointer;
15+
16+
&:hover,
17+
&:focus {
18+
text-decoration: underline;
19+
}
20+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React, { FC, HTMLAttributes } from 'react'
2+
3+
import styles from './NavigationButton.module.scss'
4+
5+
export const NavigationButton: FC<HTMLAttributes<HTMLButtonElement>> = ({ children, ...props }) => {
6+
return (
7+
<button type="button" {...props} className={styles.navigationButton}>
8+
{children}
9+
</button>
10+
)
11+
}

src/components/gherkin/Attachment.spec.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import * as messages from '@cucumber/messages'
22
import React from 'react'
33

4-
import { render } from '../../../test-utils'
4+
import { render, screen } from '../../../test-utils'
55
import { Attachment } from './Attachment'
66

77
describe('<Attachment>', () => {
8+
it('renders a download button for a file that isnt video, image or text', () => {
9+
const attachment: messages.Attachment = {
10+
body: 'test content',
11+
mediaType: 'application/pdf',
12+
contentEncoding: messages.AttachmentContentEncoding.IDENTITY,
13+
fileName: 'document.pdf',
14+
}
15+
render(<Attachment attachment={attachment} />)
16+
17+
expect(screen.getByRole('button', { name: 'Download document.pdf' })).toBeVisible()
18+
})
19+
820
it('renders a video', () => {
9-
const binary = new Uint8Array(10)
10-
binary.fill(255, 0, binary.length)
1121
const attachment: messages.Attachment = {
1222
mediaType: 'video/mp4',
1323
body: 'fake-base64',
@@ -21,8 +31,6 @@ describe('<Attachment>', () => {
2131
})
2232

2333
it('renders a video with a name', () => {
24-
const binary = new Uint8Array(10)
25-
binary.fill(255, 0, binary.length)
2634
const attachment: messages.Attachment = {
2735
mediaType: 'video/mp4',
2836
fileName: 'the attachment name',
@@ -37,8 +45,6 @@ describe('<Attachment>', () => {
3745
})
3846

3947
it('renders an image', () => {
40-
const binary = new Uint8Array(10)
41-
binary.fill(255, 0, binary.length)
4248
const attachment: messages.Attachment = {
4349
mediaType: 'image/png',
4450
body: 'fake-base64',
@@ -52,8 +58,6 @@ describe('<Attachment>', () => {
5258
})
5359

5460
it('renders an image with a name', () => {
55-
const binary = new Uint8Array(10)
56-
binary.fill(255, 0, binary.length)
5761
const attachment: messages.Attachment = {
5862
mediaType: 'image/png',
5963
fileName: 'the attachment name',

0 commit comments

Comments
 (0)