Skip to content
Open
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]
### Changed
- Hide successful hooks by default ([#415](https://github.com/cucumber/react-components/pull/415))

## [24.0.1] - 2025-11-16
### Fixed
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"react-dom": "~18"
},
"devDependencies": {
"@cucumber/compatibility-kit": "^23.0.0",
"@cucumber/compatibility-kit": "^25.0.0",
"@cucumber/fake-cucumber": "^18.1.0",
"@cucumber/gherkin": "^35.1.0",
"@cucumber/gherkin-streams": "^6.0.0",
Expand Down
13 changes: 13 additions & 0 deletions src/components/results/TestCaseOutcome.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use '../../styles/theming';

.container {
> * + * {
margin-top: 0.25rem !important;
Expand All @@ -13,3 +15,14 @@
margin-top: 0.25rem !important;
}
}

.expandButton {
background-color: transparent;
color: theming.$exampleNumberColor;
font-size: inherit;
padding: 0;
border: none;
cursor: pointer;
display: flex;
gap: 0.25rem;
}
91 changes: 91 additions & 0 deletions src/components/results/TestCaseOutcome.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Query } from '@cucumber/query'
import { userEvent } from '@testing-library/user-event'
import { expect } from 'chai'
import React from 'react'

import hooksSample from '../../../acceptance/hooks/hooks.js'
import hooksConditionalSample from '../../../acceptance/hooks-conditional/hooks-conditional.js'
import hooksSkippedSample from '../../../acceptance/hooks-skipped/hooks-skipped.js'
import { render } from '../../../test-utils/index.js'
import { EnvelopesProvider } from '../app/index.js'
import { TestCaseOutcome } from './TestCaseOutcome.js'

describe('TestCaseOutcome', () => {
it('should hide successful hooks by default, then show them on request', async () => {
const cucumberQuery = new Query()
hooksSample.forEach((envelope) => cucumberQuery.update(envelope))
const [testCaseStarted] = cucumberQuery.findAllTestCaseStarted()

const { getByRole, getAllByRole, getByText, queryByRole, queryByText } = render(
<EnvelopesProvider envelopes={hooksSample}>
<TestCaseOutcome testCaseStarted={testCaseStarted} />
</EnvelopesProvider>
)

expect(getAllByRole('listitem')).to.have.lengthOf(1)
expect(queryByText('Before')).not.to.exist
expect(getByText('a step passes')).to.be.visible
expect(queryByText('After')).not.to.exist
expect(getByRole('button', { name: '2 hooks' })).to.be.visible

await userEvent.click(getByRole('button', { name: '2 hooks' }))

expect(getAllByRole('listitem')).to.have.lengthOf(3)
expect(getByText('Before')).to.be.visible
expect(getByText('a step passes')).to.be.visible
expect(getByText('After')).to.be.visible
expect(queryByRole('button', { name: /hooks/ })).not.to.exist
})

it('should always show failed hooks', () => {
const cucumberQuery = new Query()
hooksConditionalSample.forEach((envelope) => cucumberQuery.update(envelope))
const [testCaseStarted] = cucumberQuery.findAllTestCaseStarted()

const { getAllByRole, getByText, queryByRole } = render(
<EnvelopesProvider envelopes={hooksConditionalSample}>
<TestCaseOutcome testCaseStarted={testCaseStarted} />
</EnvelopesProvider>
)

expect(getAllByRole('listitem')).to.have.lengthOf(2)
expect(getByText('Before')).to.be.visible
expect(getByText('a step passes')).to.be.visible
expect(queryByRole('button', { name: /hooks/ })).not.to.exist
})

it('should hide skipped hooks by default when they are not the skipper', () => {
const cucumberQuery = new Query()
hooksSkippedSample.forEach((envelope) => cucumberQuery.update(envelope))
const [skipFromStep] = cucumberQuery.findAllTestCaseStarted()

const { getAllByRole, getByRole, getByText, queryByText } = render(
<EnvelopesProvider envelopes={hooksSkippedSample}>
<TestCaseOutcome testCaseStarted={skipFromStep} />
</EnvelopesProvider>
)

expect(getAllByRole('listitem')).to.have.lengthOf(1)
expect(queryByText('Before')).not.to.exist
expect(getByText('a step that skips')).to.be.visible
expect(queryByText('After')).not.to.exist
expect(getByRole('button', { name: '4 hooks' })).to.be.visible
})

it('should show skipped hooks by default when they are the skipper', () => {
const cucumberQuery = new Query()
hooksSkippedSample.forEach((envelope) => cucumberQuery.update(envelope))
const [, skipFromBefore] = cucumberQuery.findAllTestCaseStarted()

const { getAllByRole, getAllByText, getByRole, getByText } = render(
<EnvelopesProvider envelopes={hooksSkippedSample}>
<TestCaseOutcome testCaseStarted={skipFromBefore} />
</EnvelopesProvider>
)

expect(getAllByRole('listitem')).to.have.lengthOf(2)
expect(getAllByText('Before')).to.have.lengthOf(1)
expect(getByText('a normal step')).to.be.visible
expect(getByRole('button', { name: '4 hooks' })).to.be.visible
})
})
38 changes: 34 additions & 4 deletions src/components/results/TestCaseOutcome.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { TestCaseStarted } from '@cucumber/messages'
import React, { FC } from 'react'
import {
TestCaseStarted,
TestStep,
TestStepFinished,
TestStepResultStatus,
} from '@cucumber/messages'
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { FC, useState } from 'react'

import { useQueries } from '../../hooks/index.js'
import styles from './TestCaseOutcome.module.scss'
Expand All @@ -10,12 +17,15 @@ interface Props {
}

export const TestCaseOutcome: FC<Props> = ({ testCaseStarted }) => {
const [showAllSteps, setShowAllSteps] = useState(false)
const { cucumberQuery } = useQueries()
const steps = cucumberQuery.findTestStepFinishedAndTestStepBy(testCaseStarted)
const allSteps = cucumberQuery.findTestStepFinishedAndTestStepBy(testCaseStarted)
const filteredSteps = showAllSteps ? allSteps : filterSteps(allSteps)
const hiddenSteps = allSteps.length - filteredSteps.length
return (
<article className={styles.container}>
<ol className={styles.steps}>
{steps.map(([testStepFinished, testStep]) => {
{filteredSteps.map(([testStepFinished, testStep]) => {
return (
<TestStepOutcome
key={testStep.id}
Expand All @@ -25,6 +35,26 @@ export const TestCaseOutcome: FC<Props> = ({ testCaseStarted }) => {
)
})}
</ol>
{hiddenSteps > 0 && (
<button className={styles.expandButton} onClick={() => setShowAllSteps(true)}>
<FontAwesomeIcon aria-hidden="true" icon={faCirclePlus} />
{hiddenSteps} hooks
</button>
)}
</article>
)
}

function filterSteps(allSteps: ReadonlyArray<[TestStepFinished, TestStep]>) {
const statuses = allSteps.map(([testStepFinished]) => testStepFinished.testStepResult.status)
return allSteps.filter(([testStepFinished, testStep], index) => {
if (!testStep.hookId) {
return true
}
if (testStepFinished.testStepResult.status === TestStepResultStatus.SKIPPED) {
const previousStatus = statuses[index - 1] ?? TestStepResultStatus.PASSED
return previousStatus === TestStepResultStatus.PASSED
}
return testStepFinished.testStepResult.status !== TestStepResultStatus.PASSED
})
}