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 + }) +}