diff --git a/src/components/results/AmbiguousResult.module.scss b/src/components/results/AmbiguousResult.module.scss new file mode 100644 index 00000000..a760851d --- /dev/null +++ b/src/components/results/AmbiguousResult.module.scss @@ -0,0 +1,27 @@ +@use '../../styles/theming'; + +.note { + padding: 0; + margin: 0; +} + +.definitions { + padding: 0; + margin: 0; + list-style: none; + + > * + * { + margin-top: 0.25em; + } +} + +.definition { + > * + * { + margin-left: 0.5em; + } +} + +.pattern { + color: theming.$parameterColor; + font-size: inherit; +} diff --git a/src/components/results/AmbiguousResult.tsx b/src/components/results/AmbiguousResult.tsx new file mode 100644 index 00000000..f0f8564c --- /dev/null +++ b/src/components/results/AmbiguousResult.tsx @@ -0,0 +1,30 @@ +import { TestStep } from '@cucumber/messages' +import React, { FC } from 'react' + +import { useQueries } from '../../hooks/index.js' +import styles from './AmbiguousResult.module.scss' +import { SourceReference } from './SourceReference.js' + +interface Props { + testStep: TestStep +} + +export const AmbiguousResult: FC = ({ testStep }) => { + const { cucumberQuery } = useQueries() + const stepDefinitions = cucumberQuery.findStepDefinitionsBy(testStep) + return ( + <> +

+ Multiple matching step definitions found: +

+ + + ) +} diff --git a/src/components/results/FailedResult.spec.tsx b/src/components/results/FailedResult.spec.tsx new file mode 100644 index 00000000..2e24419d --- /dev/null +++ b/src/components/results/FailedResult.spec.tsx @@ -0,0 +1,74 @@ +import { TestStepResultStatus } from '@cucumber/messages' +import { render } from '@testing-library/react' +import { expect } from 'chai' +import React from 'react' + +import { FailedResult } from './FailedResult.js' + +describe('FailedResult', () => { + it('should render nothing if no message or exception', () => { + const { container } = render( + + ) + + expect(container).to.be.empty + }) + + it('should render the message for a legacy message', () => { + const { container } = render( + + ) + + expect(container).to.include.text('Oh no a bad thing happened') + }) + + it('should render the message for a typed exception', () => { + const { container } = render( + + ) + + expect(container).to.include.text('Whoopsie Bad things happened') + expect(container).not.to.include.text('Dont use the legacy field') + }) + + it('should prefer a full stack trace where present', () => { + const { container } = render( + + ) + + expect(container).to.include.text('Whoopsie: Bad things happened\n at /some/file.js:1:2') + expect(container).not.to.include.text('This bit is superfluous') + }) +}) diff --git a/src/components/results/TestStepResultDetails.stories.tsx b/src/components/results/FailedResult.stories.tsx similarity index 79% rename from src/components/results/TestStepResultDetails.stories.tsx rename to src/components/results/FailedResult.stories.tsx index 20152421..c2091242 100644 --- a/src/components/results/TestStepResultDetails.stories.tsx +++ b/src/components/results/FailedResult.stories.tsx @@ -2,10 +2,10 @@ import { TestStepResult, TestStepResultStatus } from '@cucumber/messages' import { Story } from '@ladle/react' import React from 'react' -import { TestStepResultDetails } from './TestStepResultDetails.js' +import { FailedResult } from './FailedResult.js' export default { - title: 'Results/TestStepResultDetails', + title: 'Results/FailedResult', } type TemplateArgs = { @@ -13,7 +13,7 @@ type TemplateArgs = { } const Template: Story = ({ result }) => { - return + return } export const Legacy = Template.bind({}) @@ -54,7 +54,7 @@ WithStackTrace.args = { 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. (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)', + "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. (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)", }, }, } diff --git a/src/components/results/TestStepResultDetails.tsx b/src/components/results/FailedResult.tsx similarity index 62% rename from src/components/results/TestStepResultDetails.tsx rename to src/components/results/FailedResult.tsx index 4fe1e290..2d0b4e10 100644 --- a/src/components/results/TestStepResultDetails.tsx +++ b/src/components/results/FailedResult.tsx @@ -3,12 +3,18 @@ import React, { FC } from 'react' import { ErrorMessage } from '../gherkin/index.js' -export const TestStepResultDetails: FC = ({ message, exception }) => { +interface Props { + result: TestStepResult +} + +export const FailedResult: FC = ({ result: { exception, message } }) => { + if (exception?.stackTrace) { + return {exception.stackTrace} + } if (exception) { return ( {exception.type} {exception.message} - {exception.stackTrace &&
{exception.stackTrace}
}
) } diff --git a/src/components/results/SourceReference.module.scss b/src/components/results/SourceReference.module.scss new file mode 100644 index 00000000..7357e120 --- /dev/null +++ b/src/components/results/SourceReference.module.scss @@ -0,0 +1,10 @@ +@use '../../styles/theming'; + +.sourceReference { + background-color: theming.$codeBackgroundColor; + color: theming.$codeTextColor; + font-size: 0.75em; + padding: 0.1em 0.2em; + border: 1px solid theming.$panelAccentColor; + border-radius: 0.25em; +} diff --git a/src/components/results/SourceReference.tsx b/src/components/results/SourceReference.tsx new file mode 100644 index 00000000..e5740421 --- /dev/null +++ b/src/components/results/SourceReference.tsx @@ -0,0 +1,20 @@ +import { SourceReference as MessagesSourceReference } from '@cucumber/messages' +import React from 'react' +import { FC } from 'react' + +import styles from './SourceReference.module.scss' + +interface Props { + sourceReference: MessagesSourceReference +} + +export const SourceReference: FC = ({ sourceReference }) => { + if (sourceReference.uri) { + let stringified = sourceReference.uri + if (sourceReference.location) { + stringified += `:${sourceReference.location.line}` + } + return {stringified} + } + return null +} diff --git a/src/components/results/TestRunHookOutcome.tsx b/src/components/results/TestRunHookOutcome.tsx index 1c665e62..86e52532 100644 --- a/src/components/results/TestRunHookOutcome.tsx +++ b/src/components/results/TestRunHookOutcome.tsx @@ -1,11 +1,11 @@ -import { Hook, HookType, TestRunHookFinished } from '@cucumber/messages' +import { Hook, HookType, TestRunHookFinished, TestStepResultStatus } from '@cucumber/messages' import React, { FC } from 'react' import { StatusIcon } from '../gherkin/index.js' +import { FailedResult } from './FailedResult.js' import styles from './TestRunHookOutcome.module.scss' import { TestStepAttachments } from './TestStepAttachments.js' import { TestStepDuration } from './TestStepDuration.js' -import { TestStepResultDetails } from './TestStepResultDetails.js' interface Props { hook: Hook @@ -27,7 +27,9 @@ export const TestRunHookOutcome: FC = ({ hook, testRunHookFinished }) =>
- + {testRunHookFinished.result.status === TestStepResultStatus.FAILED && ( + + )}
diff --git a/src/components/results/TestStepOutcome.spec.tsx b/src/components/results/TestStepOutcome.spec.tsx index 4dcdc0c5..f1e8d1c1 100644 --- a/src/components/results/TestStepOutcome.spec.tsx +++ b/src/components/results/TestStepOutcome.spec.tsx @@ -4,13 +4,73 @@ import { render } from '@testing-library/react' import { expect } from 'chai' import React from 'react' +import ambiguousSample from '../../../acceptance/ambiguous/ambiguous.js' import minimalSample from '../../../acceptance/minimal/minimal.js' +import undefinedSample from '../../../acceptance/undefined/undefined.js' import { EnvelopesProvider } from '../app/index.js' import { TestStepOutcome } from './TestStepOutcome.js' describe('TestStepOutcome', () => { + it('should show ambiguous step definitions with source references for an ambiguous result', () => { + const cucumberQuery = new CucumberQuery() + ambiguousSample.forEach((envelope) => cucumberQuery.update(envelope)) + + const [testCaseStarted] = cucumberQuery.findAllTestCaseStarted() + const [[testStepFinished, testStep]] = + cucumberQuery.findTestStepFinishedAndTestStepBy(testCaseStarted) + + const { getByText } = render( + + + + ) + + expect(getByText('Multiple matching step definitions found:')).to.be.visible + expect(getByText('^a (.*?) with (.*?)$')).to.be.visible + expect(getByText('samples/ambiguous/ambiguous.ts:3')).to.be.visible + expect(getByText('^a step with (.*?)$')).to.be.visible + expect(getByText('samples/ambiguous/ambiguous.ts:7')).to.be.visible + }) + + it('should show snippets for an undefined result when available', () => { + const cucumberQuery = new CucumberQuery() + undefinedSample.forEach((envelope) => cucumberQuery.update(envelope)) + + const [testCaseStarted] = cucumberQuery.findAllTestCaseStarted() + const [[testStepFinished, testStep]] = + cucumberQuery.findTestStepFinishedAndTestStepBy(testCaseStarted) + + const { container, getByText } = render( + + + + ) + + expect(getByText('No step definition found. Implement with the snippet(s) below:')).to.be + .visible + expect(container).to.include.text('Given("a step that is yet to be defined", ()') + }) + + it('should show a brief note for an undefined result when no snippets available', () => { + const cucumberQuery = new CucumberQuery() + // omit suggestion messages so there are no snippets + const envelopes = undefinedSample.filter((envelope) => !envelope.suggestion) + envelopes.forEach((envelope) => cucumberQuery.update(envelope)) + + const [testCaseStarted] = cucumberQuery.findAllTestCaseStarted() + const [[testStepFinished, testStep]] = + cucumberQuery.findTestStepFinishedAndTestStepBy(testCaseStarted) + + const { getByText } = render( + + + + ) + + expect(getByText('No step definition found.')).to.be.visible + }) + it('should still work when we cant resolve the original step', () => { - // omit children from gherkinDocument.feature so that Step is unresolved const envelopes = minimalSample.map((envelope) => { if (envelope.gherkinDocument) { return { diff --git a/src/components/results/TestStepOutcome.tsx b/src/components/results/TestStepOutcome.tsx index 9290cfeb..dc46b32c 100644 --- a/src/components/results/TestStepOutcome.tsx +++ b/src/components/results/TestStepOutcome.tsx @@ -1,14 +1,16 @@ -import { PickleStep, TestStep, TestStepFinished } from '@cucumber/messages' +import { PickleStep, TestStep, TestStepFinished, TestStepResultStatus } from '@cucumber/messages' import React, { FC } from 'react' import { useQueries } from '../../hooks/index.js' import { composeHookStepTitle } from '../gherkin/composeHookStepTitle.js' import { composePickleStepTitle } from '../gherkin/composePickleStepTitle.js' import { DataTable, DocString, Keyword, Parameter, StatusIcon } from '../gherkin/index.js' +import { AmbiguousResult } from './AmbiguousResult.js' +import { FailedResult } from './FailedResult.js' import { TestStepAttachments } from './TestStepAttachments.js' import { TestStepDuration } from './TestStepDuration.js' import styles from './TestStepOutcome.module.scss' -import { TestStepResultDetails } from './TestStepResultDetails.js' +import { UndefinedResult } from './UndefinedResult.js' interface Props { testStep: TestStep @@ -32,7 +34,15 @@ export const TestStepOutcome: FC = ({ testStep, testStepFinished }) => {
{testStep.pickleStepId && } - + {testStepFinished.testStepResult.status === TestStepResultStatus.AMBIGUOUS && ( + + )} + {testStepFinished.testStepResult.status === TestStepResultStatus.FAILED && ( + + )} + {testStepFinished.testStepResult.status === TestStepResultStatus.UNDEFINED && ( + + )}
diff --git a/src/components/results/TestStepResultDetails.spec.tsx b/src/components/results/TestStepResultDetails.spec.tsx deleted file mode 100644 index 60beadf3..00000000 --- a/src/components/results/TestStepResultDetails.spec.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { TestStepResultStatus } from '@cucumber/messages' -import { render } from '@testing-library/react' -import { expect } from 'chai' -import React from 'react' - -import { TestStepResultDetails } from './TestStepResultDetails.js' - -describe('TestStepResultDetails', () => { - it('should render nothing if no message or exception', () => { - const { container } = render( - - ) - - expect(container).to.be.empty - }) - - it('should render the message for a legacy message', () => { - const { container } = render( - - ) - - expect(container).to.include.text('Oh no a bad thing happened') - }) - - it('should render the message for a typed exception', () => { - const { container } = render( - - ) - - 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( - - ) - - expect(container).to.include.text('at /some/file.js:1:2') - }) -}) diff --git a/src/components/results/UndefinedResult.module.scss b/src/components/results/UndefinedResult.module.scss new file mode 100644 index 00000000..e7b13df3 --- /dev/null +++ b/src/components/results/UndefinedResult.module.scss @@ -0,0 +1,19 @@ +@use '../../styles/theming'; + +.note { + padding: 0; + margin: 0; +} + +.snippets { + position: relative; + white-space: pre-wrap; + width: fit-content; + font-size: 0.875em; + padding: 0.666em 0.75em; + border-radius: 0.25em; + margin: 0; + overflow-x: auto; + background-color: theming.$codeBackgroundColor; + color: theming.$codeTextColor; +} diff --git a/src/components/results/UndefinedResult.tsx b/src/components/results/UndefinedResult.tsx new file mode 100644 index 00000000..f15515b7 --- /dev/null +++ b/src/components/results/UndefinedResult.tsx @@ -0,0 +1,39 @@ +import { TestStep } from '@cucumber/messages' +import React, { FC } from 'react' + +import { ensure } from '../../hooks/helpers.js' +import { useQueries } from '../../hooks/index.js' +import styles from './UndefinedResult.module.scss' + +interface Props { + testStep: TestStep +} + +export const UndefinedResult: FC = ({ testStep }) => { + const { cucumberQuery } = useQueries() + const pickleStep = ensure( + cucumberQuery.findPickleStepBy(testStep), + 'Expected TestStep with UNDEFINED status to have a PickleStep' + ) + const snippets = cucumberQuery + .findSuggestionsBy(pickleStep) + .flatMap((suggestion) => suggestion.snippets) + if (snippets.length === 0) { + return ( +

+ No step definition found. +

+ ) + } + const concatenatedCode = snippets.map((snippet) => snippet.code).join('\n\n') + return ( + <> +

+ No step definition found. Implement with the snippet(s) below: +

+
+        {concatenatedCode}
+      
+ + ) +} diff --git a/src/components/results/index.ts b/src/components/results/index.ts index 04c79995..3708bec0 100644 --- a/src/components/results/index.ts +++ b/src/components/results/index.ts @@ -1,2 +1,2 @@ +export * from './FailedResult.js' export * from './TestCaseOutcome.js' -export * from './TestStepResultDetails.js'