Skip to content

Commit 6dec552

Browse files
authored
feat: support TestStepResult.exception (#345)
1 parent af15595 commit 6dec552

12 files changed

+571
-114
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
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 `TestStepResult.exception` in results ([#345](https://github.com/cucumber/react-components/pull/345))
911

1012
## [22.0.0] - 2023-12-09
1113
### Changed

package-lock.json

Lines changed: 393 additions & 89 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
"pretty-quick-staged": "pretty-quick --staged"
2727
},
2828
"dependencies": {
29-
"@cucumber/gherkin-utils": "^8.0.0",
30-
"@cucumber/messages": "^21.0.0",
31-
"@cucumber/query": "^12.0.0",
32-
"@cucumber/tag-expressions": "^5.0.0",
29+
"@cucumber/gherkin-utils": "8.0.6",
30+
"@cucumber/messages": "24.0.1",
31+
"@cucumber/query": "12.0.1",
32+
"@cucumber/tag-expressions": "6.1.0",
3333
"@fortawesome/fontawesome-svg-core": "6.2.1",
3434
"@fortawesome/free-solid-svg-icons": "6.2.1",
3535
"@fortawesome/react-fontawesome": "0.2.0",
@@ -55,11 +55,11 @@
5555
"react-dom": "~18"
5656
},
5757
"devDependencies": {
58-
"@cucumber/compatibility-kit": "^12.0.0",
59-
"@cucumber/fake-cucumber": "^16.0.0",
60-
"@cucumber/gherkin": "^26.0.0",
61-
"@cucumber/gherkin-streams": "^5.0.1",
62-
"@cucumber/message-streams": "^4.0.1",
58+
"@cucumber/compatibility-kit": "15.0.0",
59+
"@cucumber/fake-cucumber": "16.4.0",
60+
"@cucumber/gherkin": "28.0.0",
61+
"@cucumber/gherkin-streams": "5.0.1",
62+
"@cucumber/message-streams": "4.0.1",
6363
"@ladle/react": "^2.4.5",
6464
"@testing-library/react": "14.1.2",
6565
"@testing-library/user-event": "14.5.1",
@@ -108,7 +108,6 @@
108108
"typescript": "4.9.4"
109109
},
110110
"overrides": {
111-
"@cucumber/messages": "^21.0.0",
112111
"uuid": "9.0.0"
113112
},
114113
"bugs": {

src/components/customise/customRendering.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export interface DocStringProps {
6262
export type DocStringClasses = Styles<'docString'>
6363

6464
export interface ErrorMessageProps {
65-
message: string
65+
message?: string
66+
children?: ReactNode
6667
}
6768

6869
export type ErrorMessageClasses = Styles<'message'>
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { FC } from 'react'
22

33
import {
44
DefaultComponent,
@@ -10,16 +10,17 @@ import defaultStyles from './ErrorMessage.module.scss'
1010

1111
const DefaultRenderer: DefaultComponent<ErrorMessageProps, ErrorMessageClasses> = ({
1212
message,
13+
children,
1314
styles,
1415
}) => {
15-
return <pre className={styles.message}>{message}</pre>
16+
return <pre className={styles.message}>{message ?? children}</pre>
1617
}
1718

18-
export const ErrorMessage: React.FunctionComponent<ErrorMessageProps> = (props) => {
19+
export const ErrorMessage: FC<ErrorMessageProps> = ({ children, ...props }) => {
1920
const ResolvedRenderer = useCustomRendering<ErrorMessageProps, ErrorMessageClasses>(
2021
'ErrorMessage',
2122
defaultStyles,
2223
DefaultRenderer
2324
)
24-
return <ResolvedRenderer {...props} />
25+
return <ResolvedRenderer {...props}>{children}</ResolvedRenderer>
2526
}

src/components/gherkin/GherkinStep.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import CucumberQueryContext from '../../CucumberQueryContext.js'
1313
import GherkinQueryContext from '../../GherkinQueryContext.js'
1414
import { HighLight } from '../app/HighLight.js'
1515
import { DefaultComponent, GherkinStepProps, useCustomRendering } from '../customise/index.js'
16+
import { TestStepResultDetails } from '../results/index.js'
1617
import { Attachment } from './Attachment.js'
1718
import { DataTable as DataTableComponent } from './DataTable.js'
1819
import { DocString as DocStringComponent } from './DocString.js'
19-
import { ErrorMessage } from './ErrorMessage.js'
2020
import { Keyword } from './Keyword.js'
2121
import { Parameter } from './Parameter.js'
2222
import { StepItem } from './StepItem.js'
@@ -116,7 +116,7 @@ const DefaultRenderer: DefaultComponent<GherkinStepProps> = ({
116116
</Title>
117117
{step.dataTable && <DataTableComponent dataTable={step.dataTable} />}
118118
{step.docString && <DocStringComponent docString={step.docString} />}
119-
{!hasExamples && testStepResult.message && <ErrorMessage message={testStepResult.message} />}
119+
{!hasExamples && <TestStepResultDetails {...testStepResult} />}
120120
{!hasExamples &&
121121
attachments.map((attachment, i) => <Attachment key={i} attachment={attachment} />)}
122122
</StepItem>

src/components/gherkin/HookStep.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import React from 'react'
44

55
import CucumberQueryContext from '../../CucumberQueryContext.js'
66
import { HookStepProps, useCustomRendering } from '../customise/index.js'
7+
import { TestStepResultDetails } from '../results/index.js'
78
import { Attachment } from './Attachment.js'
8-
import { ErrorMessage } from './ErrorMessage.js'
99
import { StepItem } from './StepItem.js'
1010
import { Title } from './Title.js'
1111

@@ -31,7 +31,7 @@ const DefaultRenderer: React.FunctionComponent<HookStepProps> = ({ step }) => {
3131
<Title header="h3" id={step.id}>
3232
{hook?.name ? `Hook "${hook.name}"` : 'Hook'} failed: {location}
3333
</Title>
34-
{stepResult.message && <ErrorMessage message={stepResult.message} />}
34+
<TestStepResultDetails {...stepResult} />
3535
{attachments.map((attachment, i) => (
3636
<Attachment key={i} attachment={attachment} />
3737
))}

src/components/gherkin/Scenario.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('<Scenario/>', () => {
3232
})
3333

3434
it('should render the outline with worst result for each step', () => {
35-
expect(screen.getByRole('heading', { name: 'Scenario Outline: eating cucumbers' })).to.be
35+
expect(screen.getByRole('heading', { name: 'Scenario Outline: Eating cucumbers' })).to.be
3636
.visible
3737
expect(screen.getByRole('heading', { name: 'Given there are <start> cucumbers' })).to.be
3838
.visible
@@ -49,7 +49,7 @@ describe('<Scenario/>', () => {
4949
await userEvent.click(within(screen.getAllByRole('table')[0]).getAllByRole('row')[1])
5050

5151
expect(screen.getByText('@passing')).to.be.visible
52-
expect(screen.getByRole('heading', { name: 'Example: eating cucumbers' })).to.be.visible
52+
expect(screen.getByRole('heading', { name: 'Example: Eating cucumbers' })).to.be.visible
5353
expect(screen.getByRole('heading', { name: 'Given there are 12 cucumbers' })).to.be.visible
5454
expect(screen.getByRole('heading', { name: 'When I eat 5 cucumbers' })).to.be.visible
5555
expect(screen.getByRole('heading', { name: 'Then I should have 7 cucumbers' })).to.be.visible
@@ -65,7 +65,7 @@ describe('<Scenario/>', () => {
6565
await userEvent.click(within(screen.getAllByRole('table')[1]).getAllByRole('row')[1])
6666

6767
expect(screen.getByText('@failing')).to.be.visible
68-
expect(screen.getByRole('heading', { name: 'Example: eating cucumbers' })).to.be.visible
68+
expect(screen.getByRole('heading', { name: 'Example: Eating cucumbers' })).to.be.visible
6969
expect(screen.getByRole('heading', { name: 'Given there are 12 cucumbers' })).to.be.visible
7070
expect(screen.getByRole('heading', { name: 'When I eat 20 cucumbers' })).to.be.visible
7171
expect(screen.getByRole('heading', { name: 'Then I should have 0 cucumbers' })).to.be.visible
@@ -82,7 +82,7 @@ describe('<Scenario/>', () => {
8282
await userEvent.click(within(screen.getAllByRole('table')[2]).getAllByRole('row')[1])
8383

8484
expect(screen.getByText('@undefined')).to.be.visible
85-
expect(screen.getByRole('heading', { name: 'Example: eating cucumbers' })).to.be.visible
85+
expect(screen.getByRole('heading', { name: 'Example: Eating cucumbers' })).to.be.visible
8686
expect(screen.getByRole('heading', { name: 'Given there are 12 cucumbers' })).to.be.visible
8787
expect(screen.getByRole('heading', { name: 'When I eat banana cucumbers' })).to.be.visible
8888
expect(screen.getByRole('heading', { name: 'Then I should have 12 cucumbers' })).to.be.visible
@@ -96,12 +96,12 @@ describe('<Scenario/>', () => {
9696

9797
it('should allow returning to the outline from an example detail', async () => {
9898
await userEvent.click(within(screen.getAllByRole('table')[0]).getAllByRole('row')[1])
99-
expect(screen.getByRole('heading', { name: 'Example: eating cucumbers' })).to.be.visible
99+
expect(screen.getByRole('heading', { name: 'Example: Eating cucumbers' })).to.be.visible
100100

101101
await userEvent.click(
102102
screen.getByRole('button', { name: 'Back to outline and all 6 examples' })
103103
)
104-
expect(screen.getByRole('heading', { name: 'Scenario Outline: eating cucumbers' })).to.be
104+
expect(screen.getByRole('heading', { name: 'Scenario Outline: Eating cucumbers' })).to.be
105105
.visible
106106
})
107107
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { TestStepResultStatus } from '@cucumber/messages'
2+
import { expect } from 'chai'
3+
import React from 'react'
4+
5+
import { render } from '../../../test-utils/index.js'
6+
import { TestStepResultDetails } from './TestStepResultDetails.js'
7+
8+
describe('TestStepResultDetails', () => {
9+
it('should render nothing if no message or exception', () => {
10+
const { container } = render(
11+
<TestStepResultDetails
12+
duration={{ seconds: 1, nanos: 0 }}
13+
status={TestStepResultStatus.PASSED}
14+
/>
15+
)
16+
17+
expect(container).to.be.empty
18+
})
19+
20+
it('should render the message for a legacy message', () => {
21+
const { container } = render(
22+
<TestStepResultDetails
23+
duration={{ seconds: 1, nanos: 0 }}
24+
status={TestStepResultStatus.FAILED}
25+
message="Oh no a bad thing happened"
26+
/>
27+
)
28+
29+
expect(container).to.include.text('Oh no a bad thing happened')
30+
})
31+
32+
it('should render the message for a typed exception', () => {
33+
const { container } = render(
34+
<TestStepResultDetails
35+
duration={{ seconds: 1, nanos: 0 }}
36+
status={TestStepResultStatus.FAILED}
37+
message="Dont use the legacy field"
38+
exception={{
39+
type: 'Whoopsie',
40+
message: 'Bad things happened',
41+
}}
42+
/>
43+
)
44+
45+
expect(container).to.include.text('Whoopsie Bad things happened')
46+
expect(container).not.to.include.text('Dont use the legacy field')
47+
})
48+
49+
it('should render a stack trace where present', () => {
50+
const { container } = render(
51+
<TestStepResultDetails
52+
duration={{ seconds: 1, nanos: 0 }}
53+
status={TestStepResultStatus.FAILED}
54+
message="Dont use the legacy field"
55+
exception={{
56+
type: 'Whoopsie',
57+
message: 'Bad things happened',
58+
stackTrace: 'at /some/file.js:1:2',
59+
}}
60+
/>
61+
)
62+
63+
expect(container).to.include.text('at /some/file.js:1:2')
64+
})
65+
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { TestStepResult, TestStepResultStatus } from '@cucumber/messages'
2+
import { Story } from '@ladle/react'
3+
import React from 'react'
4+
5+
import { CucumberReact } from '../CucumberReact.js'
6+
import { TestStepResultDetails } from './TestStepResultDetails.js'
7+
8+
export default {
9+
title: 'Results/TestStepResultDetails',
10+
}
11+
12+
type TemplateArgs = {
13+
result: TestStepResult
14+
}
15+
16+
const Template: Story<TemplateArgs> = ({ result }) => {
17+
return (
18+
<CucumberReact>
19+
<TestStepResultDetails {...result} />
20+
</CucumberReact>
21+
)
22+
}
23+
24+
export const Legacy = Template.bind({})
25+
Legacy.args = {
26+
result: {
27+
status: TestStepResultStatus.FAILED,
28+
message: 'Oh no a bad thing happened!',
29+
},
30+
}
31+
32+
export const NothingToSee = Template.bind({})
33+
NothingToSee.args = {
34+
result: {
35+
status: TestStepResultStatus.PASSED,
36+
},
37+
}
38+
39+
export const TypedException = Template.bind({})
40+
TypedException.args = {
41+
result: {
42+
status: TestStepResultStatus.FAILED,
43+
message:
44+
"TypeError: Cannot read properties of null (reading 'type')\n at TodosPage.addItem (/Users/somebody/Projects/my-project/support/pages/TodosPage.ts:39:21)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\n at CustomWorld.<anonymous> (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)",
45+
exception: {
46+
type: 'TypeError',
47+
message: "Cannot read properties of null (reading 'type')",
48+
},
49+
},
50+
}
51+
52+
export const WithStackTrace = Template.bind({})
53+
WithStackTrace.args = {
54+
result: {
55+
status: TestStepResultStatus.FAILED,
56+
message:
57+
"TypeError: Cannot read properties of null (reading 'type')\n at TodosPage.addItem (/Users/somebody/Projects/my-project/support/pages/TodosPage.ts:39:21)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\n at CustomWorld.<anonymous> (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)",
58+
exception: {
59+
type: 'TypeError',
60+
message: "Cannot read properties of null (reading 'type')",
61+
stackTrace:
62+
' at TodosPage.addItem (/Users/somebody/Projects/my-project/support/pages/TodosPage.ts:39:21)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\n at CustomWorld.<anonymous> (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)',
63+
},
64+
},
65+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { TestStepResult } from '@cucumber/messages'
2+
import React, { FC } from 'react'
3+
4+
import { ErrorMessage } from '../gherkin/index.js'
5+
6+
export const TestStepResultDetails: FC<TestStepResult> = ({ message, exception }) => {
7+
if (exception) {
8+
return (
9+
<ErrorMessage>
10+
<strong>{exception.type}</strong> {exception.message}
11+
{exception.stackTrace && <div>{exception.stackTrace}</div>}
12+
</ErrorMessage>
13+
)
14+
}
15+
if (message) {
16+
return <ErrorMessage>{message}</ErrorMessage>
17+
}
18+
return null
19+
}

src/components/results/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './TestStepResultDetails.js'

0 commit comments

Comments
 (0)