Skip to content

Commit b914c70

Browse files
feat: Update NotFoundError to have login button (#3804)
1 parent 9699c04 commit b914c70

File tree

2 files changed

+214
-28
lines changed

2 files changed

+214
-28
lines changed

src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.jsx

+54-12
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { useQueryClient } from '@tanstack/react-query'
22
import cs from 'classnames'
33
import PropTypes from 'prop-types'
44
import { Component, useEffect } from 'react'
5-
import { useHistory } from 'react-router-dom'
5+
import { useHistory, useLocation } from 'react-router-dom'
66

77
import config from 'config'
88

9+
import { useUser } from 'services/user'
910
import A from 'ui/A'
1011
import Button from 'ui/Button'
1112

@@ -67,11 +68,11 @@ const graphQLErrorToUI = {
6768
},
6869
}
6970

70-
export const NetworkErrorMessage = () => {
71+
export const DocErrorMessage = ({ capitalize = true }) => {
7172
if (config.IS_SELF_HOSTED) {
7273
return (
73-
<p className="my-4 px-3 sm:px-0">
74-
Please see{' '}
74+
<>
75+
{capitalize ? 'Please' : 'please'} see{' '}
7576
<A
7677
rel="noreferrer"
7778
className="text-ds-blue-default"
@@ -82,13 +83,13 @@ export const NetworkErrorMessage = () => {
8283
our docs
8384
</A>{' '}
8485
for common support.
85-
</p>
86+
</>
8687
)
8788
}
8889

8990
return (
90-
<p className="my-4 px-3 sm:px-0">
91-
Check on{' '}
91+
<>
92+
{capitalize ? 'Check' : 'check'} on{' '}
9293
<A
9394
rel="noreferrer"
9495
className="text-ds-blue-default"
@@ -109,13 +110,40 @@ export const NetworkErrorMessage = () => {
109110
our docs
110111
</A>{' '}
111112
for common support.
113+
</>
114+
)
115+
}
116+
117+
DocErrorMessage.propTypes = {
118+
capitalize: PropTypes.bool,
119+
}
120+
121+
export const NetworkErrorMessage = ({ statusCode }) => {
122+
if (statusCode === 404) {
123+
return (
124+
<p className="my-4 max-w-2xl px-3 text-center sm:px-0">
125+
To view private repositories ensure you are logged in, you can also{' '}
126+
<DocErrorMessage capitalize={false} />
127+
</p>
128+
)
129+
}
130+
131+
return (
132+
<p className="my-4 max-w-2xl px-3 text-center sm:px-0">
133+
<DocErrorMessage capitalize={true} />
112134
</p>
113135
)
114136
}
115137

116-
function ResetHandler({ logoutUser = false, reset }) {
138+
NetworkErrorMessage.propTypes = {
139+
statusCode: PropTypes.number,
140+
}
141+
142+
function ResetHandler({ logoutUser = false, reset, statusCode }) {
117143
const queryClient = useQueryClient()
118144
const history = useHistory()
145+
const location = useLocation()
146+
const { data: user } = useUser()
119147

120148
useEffect(() => {
121149
let unMounted = false
@@ -148,17 +176,27 @@ function ResetHandler({ logoutUser = false, reset }) {
148176
}
149177

150178
return (
151-
<div className="my-4">
179+
<div className="my-4 flex items-center gap-2">
152180
<Button onClick={logoutUser ? handleSignOut : handleReset}>
153181
{logoutUser ? 'Return to login' : 'Return to previous page'}
154182
</Button>
183+
{/* if the user is logged in, we don't want to show the login button */}
184+
{statusCode === 404 && !user ? (
185+
<Button
186+
variant="primary"
187+
to={{ pageName: 'login', options: { to: location.pathname } }}
188+
>
189+
Login
190+
</Button>
191+
) : null}
155192
</div>
156193
)
157194
}
158195

159196
ResetHandler.propTypes = {
160197
reset: PropTypes.func,
161198
logoutUser: PropTypes.bool,
199+
statusCode: PropTypes.number,
162200
}
163201

164202
class NetworkErrorBoundary extends Component {
@@ -211,8 +249,11 @@ class NetworkErrorBoundary extends Component {
211249
src={illustration}
212250
/>
213251
<h1 className="mt-6 text-2xl">{title}</h1>
214-
<NetworkErrorMessage />
215-
<ResetHandler reset={this.resetErrorBoundary} />
252+
<NetworkErrorMessage statusCode={this.state.error.status} />
253+
<ResetHandler
254+
reset={this.resetErrorBoundary}
255+
statusCode={this.state.error.status}
256+
/>
216257
</article>
217258
)
218259
}
@@ -230,13 +271,14 @@ class NetworkErrorBoundary extends Component {
230271
/>
231272
<h1 className="mt-6 text-2xl">{title}</h1>
232273
{description ? <p className="mt-2">{description(data)}</p> : null}
233-
{showDocs ? <NetworkErrorMessage /> : null}
274+
{showDocs ? <NetworkErrorMessage statusCode={status} /> : null}
234275
<p>
235276
<strong>Error {status}</strong>
236277
</p>
237278
<ResetHandler
238279
logoutUser={status === 429}
239280
reset={this.resetErrorBoundary}
281+
statusCode={status}
240282
/>
241283
</article>
242284
)

src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.test.jsx

+160-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
22
import { render, screen, waitFor } from '@testing-library/react'
33
import userEvent from '@testing-library/user-event'
4-
import { Component, useState } from 'react'
5-
import { MemoryRouter, useHistory } from 'react-router-dom'
4+
import { graphql, HttpResponse } from 'msw'
5+
import { setupServer } from 'msw/node'
6+
import qs from 'qs'
7+
import { Component, Suspense, useState } from 'react'
8+
import { MemoryRouter, Route, useHistory } from 'react-router-dom'
69
import { vi } from 'vitest'
710

811
import config from 'config'
@@ -13,15 +16,67 @@ import NetworkErrorBoundary from './NetworkErrorBoundary'
1316
vi.spyOn(console, 'error').mockImplementation(() => undefined)
1417
vi.mock('config')
1518

16-
const queryClient = new QueryClient({
17-
defaultOptions: { queries: { retry: false } },
19+
const mockUser = {
20+
me: {
21+
owner: {
22+
defaultOrgUsername: 'codecov',
23+
},
24+
25+
privateAccess: true,
26+
onboardingCompleted: true,
27+
businessEmail: '[email protected]',
28+
termsAgreement: true,
29+
user: {
30+
name: 'Jane Doe',
31+
username: 'janedoe',
32+
avatarUrl: 'http://127.0.0.1/avatar-url',
33+
avatar: 'http://127.0.0.1/avatar-url',
34+
student: false,
35+
studentCreatedAt: null,
36+
studentUpdatedAt: null,
37+
},
38+
trackingMetadata: {
39+
service: 'github',
40+
ownerid: 123,
41+
serviceId: '123',
42+
plan: 'users-basic',
43+
staff: false,
44+
hasYaml: false,
45+
bot: null,
46+
delinquent: null,
47+
didTrial: null,
48+
planProvider: null,
49+
planUserCount: 1,
50+
createdAt: 'timestamp',
51+
updatedAt: 'timestamp',
52+
profile: {
53+
createdAt: 'timestamp',
54+
otherGoal: null,
55+
typeProjects: [],
56+
goals: [],
57+
},
58+
},
59+
},
60+
}
61+
62+
const nullUser = {
63+
me: null,
64+
}
65+
66+
const server = setupServer()
67+
beforeAll(() => {
68+
server.listen()
1869
})
1970

2071
afterEach(() => {
21-
queryClient.clear()
72+
server.resetHandlers()
2273
vi.clearAllMocks()
2374
})
2475

76+
afterAll(() => {
77+
server.close()
78+
})
79+
2580
class TestErrorBoundary extends Component {
2681
constructor(props) {
2782
super(props)
@@ -48,9 +103,7 @@ function ErrorComponent({ status, detail, typename, dev, error }) {
48103
// eslint-disable-next-line no-throw-literal
49104
throw {
50105
status,
51-
data: {
52-
detail,
53-
},
106+
data: { detail },
54107
dev,
55108
error,
56109
__typename: typename,
@@ -105,16 +158,37 @@ function App({ status, detail, typename, dev, error }) {
105158

106159
const wrapper =
107160
(initialEntries = ['/gh/codecov', '/gh']) =>
108-
({ children }) => (
109-
<QueryClientProvider client={queryClient}>
110-
<MemoryRouter initialEntries={initialEntries}>{children}</MemoryRouter>
111-
</QueryClientProvider>
112-
)
161+
({ children }) => {
162+
const queryClient = new QueryClient({
163+
defaultOptions: { queries: { retry: false, suspense: true } },
164+
})
165+
166+
return (
167+
<QueryClientProvider client={queryClient}>
168+
<MemoryRouter initialEntries={initialEntries}>
169+
<Route path="/:provider">
170+
<Suspense fallback={<div>Loading</div>}>{children}</Suspense>
171+
</Route>
172+
</MemoryRouter>
173+
</QueryClientProvider>
174+
)
175+
}
113176

114177
describe('NetworkErrorBoundary', () => {
115-
function setup({ isSelfHosted = false } = { isSelfHosted: false }) {
178+
function setup(
179+
{ isSelfHosted = false, user = nullUser } = {
180+
isSelfHosted: false,
181+
user: nullUser,
182+
}
183+
) {
116184
config.IS_SELF_HOSTED = isSelfHosted
117185

186+
server.use(
187+
graphql.query('CurrentUser', () => {
188+
return HttpResponse.json({ data: user })
189+
})
190+
)
191+
118192
return { user: userEvent.setup() }
119193
}
120194

@@ -310,6 +384,41 @@ describe('NetworkErrorBoundary', () => {
310384
const button = await screen.findByText('Return to previous page')
311385
expect(button).toBeInTheDocument()
312386
})
387+
388+
describe('user is not logged in', () => {
389+
it('renders a login button', async () => {
390+
const { user } = setup({ user: nullUser })
391+
render(<App status={404} />, {
392+
wrapper: wrapper(),
393+
})
394+
395+
const textBox = await screen.findByRole('textbox')
396+
await user.type(textBox, 'fail')
397+
398+
const queryString = qs.stringify({ to: '/gh/codecov' })
399+
const loginButton = await screen.findByText(/Login/)
400+
expect(loginButton).toBeInTheDocument()
401+
expect(loginButton).toHaveAttribute('href', `/login?${queryString}`)
402+
})
403+
})
404+
405+
describe('user is logged in', () => {
406+
it('does not render a login button', async () => {
407+
const { user } = setup({ user: mockUser })
408+
render(<App status={404} />, {
409+
wrapper: wrapper(),
410+
})
411+
412+
const textBox = await screen.findByRole('textbox')
413+
await user.type(textBox, 'fail')
414+
415+
const notFound = await screen.findByText(/Not found/)
416+
expect(notFound).toBeInTheDocument()
417+
418+
const loginButton = screen.queryByText(/Login/)
419+
expect(loginButton).not.toBeInTheDocument()
420+
})
421+
})
313422
})
314423

315424
describe('when running in self hosted mode', () => {
@@ -322,7 +431,7 @@ describe('NetworkErrorBoundary', () => {
322431
const textBox = await screen.findByRole('textbox')
323432
await user.type(textBox, 'fail')
324433

325-
const pleaseSee = await screen.findByText(/Please see/)
434+
const pleaseSee = await screen.findByText(/please see/)
326435
expect(pleaseSee).toBeInTheDocument()
327436
})
328437

@@ -338,6 +447,41 @@ describe('NetworkErrorBoundary', () => {
338447
const button = await screen.findByText('Return to previous page')
339448
expect(button).toBeInTheDocument()
340449
})
450+
451+
describe('user is not logged in', () => {
452+
it('renders a login button', async () => {
453+
const { user } = setup({ user: nullUser, isSelfHosted: true })
454+
render(<App status={404} />, {
455+
wrapper: wrapper(),
456+
})
457+
458+
const textBox = await screen.findByRole('textbox')
459+
await user.type(textBox, 'fail')
460+
461+
const queryString = qs.stringify({ to: '/gh/codecov' })
462+
const loginButton = await screen.findByText(/Login/)
463+
expect(loginButton).toBeInTheDocument()
464+
expect(loginButton).toHaveAttribute('href', `/?${queryString}`)
465+
})
466+
})
467+
468+
describe('user is logged in', () => {
469+
it('does not render a login button', async () => {
470+
const { user } = setup({ user: mockUser, isSelfHosted: true })
471+
render(<App status={404} />, {
472+
wrapper: wrapper(),
473+
})
474+
475+
const textBox = await screen.findByRole('textbox')
476+
await user.type(textBox, 'fail')
477+
478+
const notFound = await screen.findByText(/Not found/)
479+
expect(notFound).toBeInTheDocument()
480+
481+
const loginButton = screen.queryByText(/Login/)
482+
expect(loginButton).not.toBeInTheDocument()
483+
})
484+
})
341485
})
342486
})
343487

@@ -366,7 +510,7 @@ describe('NetworkErrorBoundary', () => {
366510
global.fetch = vi.fn(() =>
367511
Promise.resolve({
368512
ok: true,
369-
json: () => Promise.resolve({}),
513+
json: () => Promise.resolve({ data: nullUser }),
370514
})
371515
)
372516

0 commit comments

Comments
 (0)