diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b02273e..20fe8ead 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/package-lock.json b/package-lock.json
index 44edb1fd..856f066b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,7 +36,7 @@
"uuid": "9.0.0"
},
"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",
@@ -495,9 +495,9 @@
"license": "MIT"
},
"node_modules/@cucumber/compatibility-kit": {
- "version": "23.0.0",
- "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-23.0.0.tgz",
- "integrity": "sha512-+XapEOpxPm2c5iOD+eiWgRQLAN6MMal22dOeicm6Z4Vewp1XpEoPaS/G5l0ODMcIxhKi9GrUlyCsQ1Hd84M45w==",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-25.0.0.tgz",
+ "integrity": "sha512-DmB6oOWVh+0L7VRa4kq/xHIccrBezdibSHt7RAVdVX/hv1grf5+yzt44a+JTliPg5IKVEqxusmQ556sB8Tlabg==",
"dev": true,
"license": "MIT"
},
@@ -17952,9 +17952,9 @@
}
},
"node_modules/vite-tsconfig-paths/node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
diff --git a/package.json b/package.json
index 1e4312ab..07d5678b 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/results/TestCaseOutcome.module.scss b/src/components/results/TestCaseOutcome.module.scss
index d0a59e13..47ee5b75 100644
--- a/src/components/results/TestCaseOutcome.module.scss
+++ b/src/components/results/TestCaseOutcome.module.scss
@@ -1,3 +1,5 @@
+@use '../../styles/theming';
+
.container {
> * + * {
margin-top: 0.25rem !important;
@@ -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;
+}
diff --git a/src/components/results/TestCaseOutcome.spec.tsx b/src/components/results/TestCaseOutcome.spec.tsx
new file mode 100644
index 00000000..7de65d61
--- /dev/null
+++ b/src/components/results/TestCaseOutcome.spec.tsx
@@ -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(
+
+
+
+ )
+
+ 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(
+
+
+
+ )
+
+ 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(
+
+
+
+ )
+
+ 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(
+
+
+
+ )
+
+ 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
+ })
+})
diff --git a/src/components/results/TestCaseOutcome.tsx b/src/components/results/TestCaseOutcome.tsx
index bfacc347..0ab99d66 100644
--- a/src/components/results/TestCaseOutcome.tsx
+++ b/src/components/results/TestCaseOutcome.tsx
@@ -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'
@@ -10,12 +17,15 @@ interface Props {
}
export const TestCaseOutcome: FC = ({ 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 (
- {steps.map(([testStepFinished, testStep]) => {
+ {filteredSteps.map(([testStepFinished, testStep]) => {
return (
= ({ testCaseStarted }) => {
)
})}
+ {hiddenSteps > 0 && (
+
+ )}
)
}
+
+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
+ })
+}