Skip to content

feat: support TestStepResult.exception #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Support `TestStepResult.exception` in results ([#345](https://github.com/cucumber/react-components/pull/345))

## [22.0.0] - 2023-12-09
### Changed
Expand Down
482 changes: 393 additions & 89 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 9 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
"pretty-quick-staged": "pretty-quick --staged"
},
"dependencies": {
"@cucumber/gherkin-utils": "^8.0.0",
"@cucumber/messages": "^21.0.0",
"@cucumber/query": "^12.0.0",
"@cucumber/tag-expressions": "^5.0.0",
"@cucumber/gherkin-utils": "8.0.6",
"@cucumber/messages": "24.0.1",
"@cucumber/query": "12.0.1",
"@cucumber/tag-expressions": "6.1.0",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0",
Expand All @@ -55,11 +55,11 @@
"react-dom": "~18"
},
"devDependencies": {
"@cucumber/compatibility-kit": "^12.0.0",
"@cucumber/fake-cucumber": "^16.0.0",
"@cucumber/gherkin": "^26.0.0",
"@cucumber/gherkin-streams": "^5.0.1",
"@cucumber/message-streams": "^4.0.1",
"@cucumber/compatibility-kit": "15.0.0",
"@cucumber/fake-cucumber": "16.4.0",
"@cucumber/gherkin": "28.0.0",
"@cucumber/gherkin-streams": "5.0.1",
"@cucumber/message-streams": "4.0.1",
"@ladle/react": "^2.4.5",
"@testing-library/react": "14.1.2",
"@testing-library/user-event": "14.5.1",
Expand Down Expand Up @@ -108,7 +108,6 @@
"typescript": "4.9.4"
},
"overrides": {
"@cucumber/messages": "^21.0.0",
"uuid": "9.0.0"
},
"bugs": {
Expand Down
3 changes: 2 additions & 1 deletion src/components/customise/customRendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export interface DocStringProps {
export type DocStringClasses = Styles<'docString'>

export interface ErrorMessageProps {
message: string
message?: string
children?: ReactNode
}

export type ErrorMessageClasses = Styles<'message'>
Expand Down
9 changes: 5 additions & 4 deletions src/components/gherkin/ErrorMessage.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in a backwards-compatible way to allow content as children instead of a string prop - we need this to use in our new component.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { FC } from 'react'

import {
DefaultComponent,
Expand All @@ -10,16 +10,17 @@ import defaultStyles from './ErrorMessage.module.scss'

const DefaultRenderer: DefaultComponent<ErrorMessageProps, ErrorMessageClasses> = ({
message,
children,
styles,
}) => {
return <pre className={styles.message}>{message}</pre>
return <pre className={styles.message}>{message ?? children}</pre>
}

export const ErrorMessage: React.FunctionComponent<ErrorMessageProps> = (props) => {
export const ErrorMessage: FC<ErrorMessageProps> = ({ children, ...props }) => {
const ResolvedRenderer = useCustomRendering<ErrorMessageProps, ErrorMessageClasses>(
'ErrorMessage',
defaultStyles,
DefaultRenderer
)
return <ResolvedRenderer {...props} />
return <ResolvedRenderer {...props}>{children}</ResolvedRenderer>
}
4 changes: 2 additions & 2 deletions src/components/gherkin/GherkinStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import CucumberQueryContext from '../../CucumberQueryContext.js'
import GherkinQueryContext from '../../GherkinQueryContext.js'
import { HighLight } from '../app/HighLight.js'
import { DefaultComponent, GherkinStepProps, useCustomRendering } from '../customise/index.js'
import { TestStepResultDetails } from '../results/index.js'
import { Attachment } from './Attachment.js'
import { DataTable as DataTableComponent } from './DataTable.js'
import { DocString as DocStringComponent } from './DocString.js'
import { ErrorMessage } from './ErrorMessage.js'
import { Keyword } from './Keyword.js'
import { Parameter } from './Parameter.js'
import { StepItem } from './StepItem.js'
Expand Down Expand Up @@ -116,7 +116,7 @@ const DefaultRenderer: DefaultComponent<GherkinStepProps> = ({
</Title>
{step.dataTable && <DataTableComponent dataTable={step.dataTable} />}
{step.docString && <DocStringComponent docString={step.docString} />}
{!hasExamples && testStepResult.message && <ErrorMessage message={testStepResult.message} />}
{!hasExamples && <TestStepResultDetails {...testStepResult} />}
{!hasExamples &&
attachments.map((attachment, i) => <Attachment key={i} attachment={attachment} />)}
</StepItem>
Expand Down
4 changes: 2 additions & 2 deletions src/components/gherkin/HookStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import React from 'react'

import CucumberQueryContext from '../../CucumberQueryContext.js'
import { HookStepProps, useCustomRendering } from '../customise/index.js'
import { TestStepResultDetails } from '../results/index.js'
import { Attachment } from './Attachment.js'
import { ErrorMessage } from './ErrorMessage.js'
import { StepItem } from './StepItem.js'
import { Title } from './Title.js'

Expand All @@ -31,7 +31,7 @@ const DefaultRenderer: React.FunctionComponent<HookStepProps> = ({ step }) => {
<Title header="h3" id={step.id}>
{hook?.name ? `Hook "${hook.name}"` : 'Hook'} failed: {location}
</Title>
{stepResult.message && <ErrorMessage message={stepResult.message} />}
<TestStepResultDetails {...stepResult} />
{attachments.map((attachment, i) => (
<Attachment key={i} attachment={attachment} />
))}
Expand Down
12 changes: 6 additions & 6 deletions src/components/gherkin/Scenario.spec.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This edits are due to upstream changes in the CCK sources.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('<Scenario/>', () => {
})

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

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

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

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

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

await userEvent.click(
screen.getByRole('button', { name: 'Back to outline and all 6 examples' })
)
expect(screen.getByRole('heading', { name: 'Scenario Outline: eating cucumbers' })).to.be
expect(screen.getByRole('heading', { name: 'Scenario Outline: Eating cucumbers' })).to.be
.visible
})
})
Expand Down
65 changes: 65 additions & 0 deletions src/components/results/TestStepResultDetails.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { TestStepResultStatus } from '@cucumber/messages'
import { expect } from 'chai'
import React from 'react'

import { render } from '../../../test-utils/index.js'
import { TestStepResultDetails } from './TestStepResultDetails.js'

describe('TestStepResultDetails', () => {
it('should render nothing if no message or exception', () => {
const { container } = render(
<TestStepResultDetails
duration={{ seconds: 1, nanos: 0 }}
status={TestStepResultStatus.PASSED}
/>
)

expect(container).to.be.empty
})

it('should render the message for a legacy message', () => {
const { container } = render(
<TestStepResultDetails
duration={{ seconds: 1, nanos: 0 }}
status={TestStepResultStatus.FAILED}
message="Oh no a bad thing happened"
/>
)

expect(container).to.include.text('Oh no a bad thing happened')
})

it('should render the message for a typed exception', () => {
const { container } = render(
<TestStepResultDetails
duration={{ seconds: 1, nanos: 0 }}
status={TestStepResultStatus.FAILED}
message="Dont use the legacy field"
exception={{
type: 'Whoopsie',
message: 'Bad things happened',
}}
/>
)

expect(container).to.include.text('Whoopsie Bad things happened')
expect(container).not.to.include.text('Dont use the legacy field')
})

it('should render a stack trace where present', () => {
const { container } = render(
<TestStepResultDetails
duration={{ seconds: 1, nanos: 0 }}
status={TestStepResultStatus.FAILED}
message="Dont use the legacy field"
exception={{
type: 'Whoopsie',
message: 'Bad things happened',
stackTrace: 'at /some/file.js:1:2',
}}
/>
)

expect(container).to.include.text('at /some/file.js:1:2')
})
})
65 changes: 65 additions & 0 deletions src/components/results/TestStepResultDetails.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { TestStepResult, TestStepResultStatus } from '@cucumber/messages'
import { Story } from '@ladle/react'
import React from 'react'

import { CucumberReact } from '../CucumberReact.js'
import { TestStepResultDetails } from './TestStepResultDetails.js'

export default {
title: 'Results/TestStepResultDetails',
}

type TemplateArgs = {
result: TestStepResult
}

const Template: Story<TemplateArgs> = ({ result }) => {
return (
<CucumberReact>
<TestStepResultDetails {...result} />
</CucumberReact>
)
}

export const Legacy = Template.bind({})
Legacy.args = {
result: {
status: TestStepResultStatus.FAILED,
message: 'Oh no a bad thing happened!',
},
}

export const NothingToSee = Template.bind({})
NothingToSee.args = {
result: {
status: TestStepResultStatus.PASSED,
},
}

export const TypedException = Template.bind({})
TypedException.args = {
result: {
status: TestStepResultStatus.FAILED,
message:
"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)",
exception: {
type: 'TypeError',
message: "Cannot read properties of null (reading 'type')",
},
},
}

export const WithStackTrace = Template.bind({})
WithStackTrace.args = {
result: {
status: TestStepResultStatus.FAILED,
message:
"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)",
exception: {
type: 'TypeError',
message: "Cannot read properties of null (reading 'type')",
stackTrace:
' 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)',
},
},
}
19 changes: 19 additions & 0 deletions src/components/results/TestStepResultDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TestStepResult } from '@cucumber/messages'
import React, { FC } from 'react'

import { ErrorMessage } from '../gherkin/index.js'

export const TestStepResultDetails: FC<TestStepResult> = ({ message, exception }) => {
if (exception) {
return (
<ErrorMessage>
<strong>{exception.type}</strong> {exception.message}
{exception.stackTrace && <div>{exception.stackTrace}</div>}
</ErrorMessage>
)
}
if (message) {
return <ErrorMessage>{message}</ErrorMessage>
}
return null
}
1 change: 1 addition & 0 deletions src/components/results/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TestStepResultDetails.js'