Skip to content
Draft
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
27 changes: 27 additions & 0 deletions src/components/results/AmbiguousResult.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
30 changes: 30 additions & 0 deletions src/components/results/AmbiguousResult.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ testStep }) => {
const { cucumberQuery } = useQueries()
const stepDefinitions = cucumberQuery.findStepDefinitionsBy(testStep)
return (
<>
<p className={styles.note}>
<em>Multiple matching step definitions found:</em>
</p>
<ul className={styles.definitions}>
{stepDefinitions.map((stepDefinition) => (
<li key={stepDefinition.id} className={styles.definition}>
<code className={styles.pattern}>{stepDefinition.pattern.source}</code>
<SourceReference sourceReference={stepDefinition.sourceReference} />
</li>
))}
</ul>
</>
)
}
74 changes: 74 additions & 0 deletions src/components/results/FailedResult.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(
<FailedResult
result={{
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(
<FailedResult
result={{
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(
<FailedResult
result={{
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 prefer a full stack trace where present', () => {
const { container } = render(
<FailedResult
result={{
duration: { seconds: 1, nanos: 0 },
status: TestStepResultStatus.FAILED,
message: 'Dont use the legacy field',
exception: {
type: 'Whoopsie',
message: 'This bit is superfluous',
stackTrace: 'Whoopsie: Bad things happened\n at /some/file.js:1:2',
},
}}
/>
)

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')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ 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 = {
result: TestStepResult
}

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

export const Legacy = Template.bind({})
Expand Down Expand Up @@ -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.<anonymous> (/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.<anonymous> (/Users/somebody/Projects/my-project/support/steps/steps.ts:20:5)",
},
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import React, { FC } from 'react'

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

export const TestStepResultDetails: FC<TestStepResult> = ({ message, exception }) => {
interface Props {
result: TestStepResult
}

export const FailedResult: FC<Props> = ({ result: { exception, message } }) => {
if (exception?.stackTrace) {
return <ErrorMessage>{exception.stackTrace}</ErrorMessage>
}
if (exception) {
return (
<ErrorMessage>
<strong>{exception.type}</strong> {exception.message}
{exception.stackTrace && <div>{exception.stackTrace}</div>}
</ErrorMessage>
)
}
Expand Down
10 changes: 10 additions & 0 deletions src/components/results/SourceReference.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions src/components/results/SourceReference.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ sourceReference }) => {
if (sourceReference.uri) {
let stringified = sourceReference.uri
if (sourceReference.location) {
stringified += `:${sourceReference.location.line}`
}
return <code className={styles.sourceReference}>{stringified}</code>
}
return null
}
8 changes: 5 additions & 3 deletions src/components/results/TestRunHookOutcome.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,7 +27,9 @@ export const TestRunHookOutcome: FC<Props> = ({ hook, testRunHookFinished }) =>
</div>
</div>
<div className={styles.content}>
<TestStepResultDetails {...testRunHookFinished.result} />
{testRunHookFinished.result.status === TestStepResultStatus.FAILED && (
<FailedResult result={testRunHookFinished.result} />
)}
<TestStepAttachments testStepOrHookFinished={testRunHookFinished} />
</div>
</li>
Expand Down
62 changes: 61 additions & 1 deletion src/components/results/TestStepOutcome.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EnvelopesProvider envelopes={ambiguousSample}>
<TestStepOutcome testStep={testStep} testStepFinished={testStepFinished} />
</EnvelopesProvider>
)

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(
<EnvelopesProvider envelopes={undefinedSample}>
<TestStepOutcome testStep={testStep} testStepFinished={testStepFinished} />
</EnvelopesProvider>
)

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(
<EnvelopesProvider envelopes={envelopes}>
<TestStepOutcome testStep={testStep} testStepFinished={testStepFinished} />
</EnvelopesProvider>
)

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 {
Expand Down
16 changes: 13 additions & 3 deletions src/components/results/TestStepOutcome.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -32,7 +34,15 @@ export const TestStepOutcome: FC<Props> = ({ testStep, testStepFinished }) => {
</div>
<div className={styles.content}>
{testStep.pickleStepId && <PickleStepArgument testStep={testStep} />}
<TestStepResultDetails {...testStepFinished.testStepResult} />
{testStepFinished.testStepResult.status === TestStepResultStatus.AMBIGUOUS && (
<AmbiguousResult testStep={testStep} />
)}
{testStepFinished.testStepResult.status === TestStepResultStatus.FAILED && (
<FailedResult result={testStepFinished.testStepResult} />
)}
{testStepFinished.testStepResult.status === TestStepResultStatus.UNDEFINED && (
<UndefinedResult testStep={testStep} />
)}
<TestStepAttachments testStepOrHookFinished={testStepFinished} />
</div>
</li>
Expand Down
Loading