Skip to content

Commit 9227c55

Browse files
authored
feat: Surface latest upload error for latest commit in a branch for tests tab (#3722)
1 parent dfe9ccb commit 9227c55

File tree

11 files changed

+418
-4
lines changed

11 files changed

+418
-4
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2+
import { render, screen, waitFor } from '@testing-library/react'
3+
import { graphql, HttpResponse } from 'msw'
4+
import { setupServer } from 'msw/node'
5+
import { Suspense } from 'react'
6+
import { MemoryRouter, Route } from 'react-router-dom'
7+
8+
import { ErrorCodeEnum } from 'shared/utils/commit'
9+
10+
import { FailedTestsErrorBanner } from '../FailedTestsErrorBanner'
11+
12+
const server = setupServer()
13+
14+
const queryClient = new QueryClient({
15+
defaultOptions: { queries: { retry: false } },
16+
})
17+
18+
beforeAll(() => {
19+
server.listen()
20+
})
21+
22+
afterEach(() => {
23+
vi.clearAllMocks()
24+
queryClient.clear()
25+
server.resetHandlers()
26+
})
27+
28+
afterAll(() => {
29+
server.close()
30+
})
31+
32+
const mockRepoOverview = {
33+
owner: {
34+
isCurrentUserActivated: true,
35+
repository: {
36+
__typename: 'Repository',
37+
private: false,
38+
defaultBranch: 'main',
39+
oldestCommitAt: '2022-10-10T11:59:59',
40+
coverageEnabled: true,
41+
bundleAnalysisEnabled: true,
42+
testAnalyticsEnabled: false,
43+
languages: ['javascript'],
44+
},
45+
},
46+
}
47+
48+
const mockTestResultsTestSuites = ({
49+
errorCode,
50+
errorMessage,
51+
}: {
52+
errorCode: string
53+
errorMessage: string
54+
}) => ({
55+
owner: {
56+
repository: {
57+
__typename: 'Repository',
58+
testAnalytics: {
59+
testSuites: ['java', 'script'],
60+
},
61+
branch: {
62+
head: {
63+
latestUploadError: { errorCode, errorMessage },
64+
},
65+
},
66+
},
67+
},
68+
})
69+
70+
const wrapper =
71+
(
72+
initialEntries = ['/repo/codecov/gazebo/branch/test']
73+
): React.FC<React.PropsWithChildren> =>
74+
({ children }) => (
75+
<QueryClientProvider client={queryClient}>
76+
<MemoryRouter initialEntries={initialEntries}>
77+
<Route path="/repo/:owner/:repo/branch/:branch">
78+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
79+
</Route>
80+
</MemoryRouter>
81+
</QueryClientProvider>
82+
)
83+
84+
describe('FailedTestsErrorBanner', () => {
85+
function setup({
86+
errorCode,
87+
errorMessage = 'File not found',
88+
}: {
89+
errorCode: string
90+
errorMessage?: string
91+
}) {
92+
server.use(
93+
graphql.query('GetRepoOverview', () => {
94+
return HttpResponse.json({
95+
data: mockRepoOverview,
96+
})
97+
}),
98+
graphql.query('GetTestResultsTestSuites', () => {
99+
return HttpResponse.json({
100+
data: mockTestResultsTestSuites({ errorCode, errorMessage }),
101+
})
102+
})
103+
)
104+
}
105+
106+
it('renders nothing when unexpected error is provided', async () => {
107+
setup({ errorCode: ErrorCodeEnum.unknownProcessing })
108+
const { container } = render(<FailedTestsErrorBanner />, {
109+
wrapper: wrapper(),
110+
})
111+
112+
await waitFor(() => queryClient.isFetching)
113+
await waitFor(() => !queryClient.isFetching)
114+
115+
expect(container).toBeEmptyDOMElement()
116+
})
117+
118+
it('renders file not found in storage error', async () => {
119+
setup({ errorCode: ErrorCodeEnum.fileNotFoundInStorage })
120+
render(<FailedTestsErrorBanner />, { wrapper: wrapper() })
121+
const banner = await screen.findByRole('heading', {
122+
name: 'JUnit XML file not found',
123+
})
124+
expect(banner).toBeInTheDocument()
125+
})
126+
127+
it('renders processing timeout error', async () => {
128+
setup({ errorCode: ErrorCodeEnum.processingTimeout })
129+
render(<FailedTestsErrorBanner />, { wrapper: wrapper() })
130+
const banner = await screen.findByRole('heading', {
131+
name: 'Upload timeout',
132+
})
133+
expect(banner).toBeInTheDocument()
134+
})
135+
136+
it('renders unsupported file format error', async () => {
137+
setup({ errorCode: ErrorCodeEnum.unsupportedFileFormat })
138+
render(<FailedTestsErrorBanner />, { wrapper: wrapper() })
139+
const banner = await screen.findByRole('heading', {
140+
name: 'Unsupported file format',
141+
})
142+
const content = await screen.findByText(
143+
/Please review the parser error message:/
144+
)
145+
const troubleshootingLink = await screen.findByRole('link', {
146+
name: 'troubleshooting guide',
147+
})
148+
149+
expect(banner).toBeInTheDocument()
150+
expect(content).toBeInTheDocument()
151+
expect(troubleshootingLink).toBeInTheDocument()
152+
expect(troubleshootingLink).toHaveAttribute(
153+
'href',
154+
'https://docs.codecov.com/docs/test-analytics#troubleshooting'
155+
)
156+
})
157+
158+
describe('when error message is not provided for unsupported file format', () => {
159+
it('hides the review parser error message', async () => {
160+
setup({
161+
errorCode: ErrorCodeEnum.unsupportedFileFormat,
162+
errorMessage: '',
163+
})
164+
render(<FailedTestsErrorBanner />, { wrapper: wrapper() })
165+
166+
await waitFor(() => queryClient.isFetching)
167+
await waitFor(() => !queryClient.isFetching)
168+
169+
const banner = screen.queryByText(
170+
'Please review the parser error message:'
171+
)
172+
expect(banner).not.toBeInTheDocument()
173+
})
174+
})
175+
176+
describe('when no branch is provided', () => {
177+
it('renders nothing', async () => {
178+
setup({ errorCode: ErrorCodeEnum.fileNotFoundInStorage })
179+
180+
await waitFor(() => queryClient.isFetching)
181+
await waitFor(() => !queryClient.isFetching)
182+
183+
const { container } = render(<FailedTestsErrorBanner />, {
184+
wrapper: wrapper(['/repo/owner/repo/']),
185+
})
186+
expect(container).toBeEmptyDOMElement()
187+
})
188+
})
189+
190+
describe('when branch is the default branch', () => {
191+
it('renders nothing', async () => {
192+
setup({ errorCode: ErrorCodeEnum.fileNotFoundInStorage })
193+
194+
await waitFor(() => queryClient.isFetching)
195+
await waitFor(() => !queryClient.isFetching)
196+
197+
const { container } = render(<FailedTestsErrorBanner />, {
198+
wrapper: wrapper(['/repo/owner/repo/main']),
199+
})
200+
expect(container).toBeEmptyDOMElement()
201+
})
202+
})
203+
})
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useParams } from 'react-router-dom'
2+
3+
import { useRepoOverview } from 'services/repo'
4+
import { ErrorCodeEnum } from 'shared/utils/commit'
5+
import A from 'ui/A'
6+
import Banner from 'ui/Banner'
7+
import BannerContent from 'ui/Banner/BannerContent'
8+
import BannerHeading from 'ui/Banner/BannerHeading'
9+
import Icon from 'ui/Icon'
10+
11+
import { useTestResultsTestSuites } from '../hooks/useTestResultsTestSuites/useTestResultsTestSuites'
12+
13+
const CodeSnippet: React.FC<React.PropsWithChildren> = ({ children }) => (
14+
<code className="rounded-md border border-ds-gray-secondary bg-ds-gray-primary p-1 text-xs text-ds-primary-red">
15+
{children}
16+
</code>
17+
)
18+
19+
const FileNotFoundBanner = () => (
20+
<Banner variant="warning">
21+
<BannerHeading>
22+
<div className="flex items-center gap-2">
23+
<Icon name="exclamation" className="text-orange-500" />
24+
<h3 className="font-semibold">JUnit XML file not found</h3>
25+
</div>
26+
</BannerHeading>
27+
<BannerContent>
28+
<p>
29+
No result to display due to Test Analytics couldn&apos;t locate a JUnit
30+
XML file. Please rename the file to include{' '}
31+
<CodeSnippet>junit</CodeSnippet>, ensure CLI file search is enabled, or
32+
use the <CodeSnippet>file</CodeSnippet> or{' '}
33+
<CodeSnippet>search_dir</CodeSnippet> arguments to specify the file(s)
34+
for upload.
35+
</p>
36+
</BannerContent>
37+
</Banner>
38+
)
39+
40+
const ProcessingTimeoutBanner = () => (
41+
<Banner variant="warning">
42+
<BannerHeading>
43+
<div className="flex items-center gap-2">
44+
<Icon name="exclamation" className="text-orange-500" />
45+
<h3 className="font-semibold">Upload timeout</h3>
46+
</div>
47+
</BannerHeading>
48+
<BannerContent>
49+
Your upload failed due to timeout. Please try it again.
50+
</BannerContent>
51+
</Banner>
52+
)
53+
54+
const UnsupportedFormatBanner = ({
55+
errorMessage,
56+
}: {
57+
errorMessage: string
58+
}) => (
59+
<Banner variant="warning">
60+
<BannerHeading>
61+
<div className="flex items-center gap-2">
62+
<Icon name="exclamation" className="text-orange-500" />
63+
<h3 className="font-semibold">Unsupported file format</h3>
64+
</div>
65+
</BannerHeading>
66+
<BannerContent>
67+
Upload processing failed due to unusable file format.
68+
{errorMessage ? (
69+
<>
70+
{' '}
71+
Please review the parser error message:{' '}
72+
<CodeSnippet>{errorMessage}</CodeSnippet> <br />{' '}
73+
</>
74+
) : (
75+
' '
76+
)}
77+
For more help, visit our{' '}
78+
<A
79+
to={{
80+
pageName: 'testAnalyticsTroubleshooting',
81+
}}
82+
hook="trouble shooting guide"
83+
isExternal={true}
84+
>
85+
troubleshooting guide
86+
</A>
87+
.
88+
</BannerContent>
89+
</Banner>
90+
)
91+
92+
interface URLParams {
93+
provider: string
94+
owner: string
95+
repo: string
96+
branch?: string
97+
}
98+
99+
function FailedTestsErrorBanner() {
100+
const { provider, owner, repo, branch } = useParams<URLParams>()
101+
const { data: overview } = useRepoOverview({
102+
provider,
103+
owner,
104+
repo,
105+
})
106+
107+
const { data } = useTestResultsTestSuites({ branch })
108+
const latestUploadError = data?.latestUploadError ?? {
109+
errorCode: ErrorCodeEnum.fileNotFoundInStorage,
110+
errorMessage: 'File not found',
111+
}
112+
113+
if (!latestUploadError || branch === overview?.defaultBranch) {
114+
return null
115+
}
116+
117+
const errorCode = latestUploadError.errorCode
118+
119+
if (errorCode === ErrorCodeEnum.fileNotFoundInStorage) {
120+
return <FileNotFoundBanner />
121+
}
122+
123+
if (errorCode === ErrorCodeEnum.processingTimeout) {
124+
return <ProcessingTimeoutBanner />
125+
}
126+
127+
if (errorCode === ErrorCodeEnum.unsupportedFileFormat) {
128+
return (
129+
<UnsupportedFormatBanner errorMessage={latestUploadError.errorMessage} />
130+
)
131+
}
132+
133+
return null
134+
}
135+
136+
export default FailedTestsErrorBanner
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as FailedTestsErrorBanner } from './FailedTestsErrorBanner'

src/pages/RepoPage/FailedTestsTab/FailedTestsPage/FailedTestsPage.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ vi.mock('./MetricsSection/MetricsSection', () => ({
1616
vi.mock('./FailedTestsTable/FailedTestsTable', () => ({
1717
default: () => 'Failed Tests Table',
1818
}))
19+
vi.mock('./FailedTestsErrorBanner/FailedTestsErrorBanner', () => ({
20+
default: () => 'Failed Tests Error Banner',
21+
}))
1922

2023
const server = setupServer()
2124
const queryClient = new QueryClient({
@@ -76,9 +79,11 @@ describe('FailedTestsPage', () => {
7679
const selectorSection = screen.getByText(/Selector Section/)
7780
const metricSection = screen.getByText(/Metrics Section/)
7881
const table = screen.getByText(/Failed Tests Table/)
82+
const errorBanner = screen.getByText(/Failed Tests Error Banner/)
7983

8084
expect(selectorSection).toBeInTheDocument()
8185
expect(metricSection).toBeInTheDocument()
8286
expect(table).toBeInTheDocument()
87+
expect(errorBanner).toBeInTheDocument()
8388
})
8489
})

src/pages/RepoPage/FailedTestsTab/FailedTestsPage/FailedTestsPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { FailedTestsErrorBanner } from './FailedTestsErrorBanner'
12
import FailedTestsTable from './FailedTestsTable'
23
import { MetricsSection } from './MetricsSection'
34
import { SelectorSection } from './SelectorSection'
45

56
function FailedTestsPage() {
67
return (
78
<div className="flex flex-1 flex-col gap-2">
8-
<SelectorSection />
9+
<div className="flex gap-2">
10+
<SelectorSection />
11+
<FailedTestsErrorBanner />
12+
</div>
913
<MetricsSection />
1014
<FailedTestsTable />
1115
</div>

0 commit comments

Comments
 (0)