-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(client): fully cover client module (#2295)
Contains: * test(client-provider): fix flaky test * wip * test(client-provider): verify more use-cases * test(client): programmatic session refetch * test(client): further coverage * test(client): `stateTime` + `refetchInterval` * refactor(client): test insights * refactor: unused variable * chore: revert `package-lock.json` to v2 * refactor: pair-review suggestions
- Loading branch information
Showing
6 changed files
with
400 additions
and
54 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,188 @@ | ||
import { rest } from "msw" | ||
import { render, screen, waitFor } from "@testing-library/react" | ||
import { server, mockSession } from "./helpers/mocks" | ||
import { SessionProvider, useSession } from "../react" | ||
import { printFetchCalls } from "./helpers/utils" | ||
import { SessionProvider, useSession, signOut, getSession } from "../react" | ||
|
||
const origDocumentVisibility = document.visibilityState | ||
const fetchSpy = jest.spyOn(global, "fetch") | ||
|
||
beforeAll(() => { | ||
server.listen() | ||
}) | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
server.resetHandlers() | ||
changeTabVisibility(origDocumentVisibility) | ||
fetchSpy.mockClear() | ||
}) | ||
|
||
afterAll(() => { | ||
server.close() | ||
}) | ||
|
||
test("it won't allow to fetch the session in isolation without a session context", () => { | ||
function App() { | ||
useSession() | ||
return null | ||
} | ||
test("fetches the session once and re-uses it for different consumers", async () => { | ||
render(<ProviderFlow />) | ||
|
||
expect(screen.getByTestId("session-1")).toHaveTextContent("loading") | ||
expect(screen.getByTestId("session-2")).toHaveTextContent("loading") | ||
|
||
jest.spyOn(console, "error") | ||
console.error.mockImplementation(() => {}) | ||
return waitFor(() => { | ||
expect(fetchSpy).toHaveBeenCalledTimes(1) | ||
|
||
expect(() => render(<App />)).toThrow( | ||
"useSession must be wrapped in a SessionProvider" | ||
) | ||
expect(fetchSpy).toHaveBeenCalledWith( | ||
"/api/auth/session", | ||
expect.anything() | ||
) | ||
|
||
const session1 = screen.getByTestId("session-1").textContent | ||
const session2 = screen.getByTestId("session-2").textContent | ||
|
||
console.error.mockRestore() | ||
expect(session1).toEqual(session2) | ||
}) | ||
}) | ||
|
||
test("fetches the session once and re-uses it for different consumers", async () => { | ||
const sessionRouteCall = jest.fn() | ||
test("when there's an existing session, it won't try to fetch a new one straightaway", async () => { | ||
render(<ProviderFlow session={mockSession} />) | ||
|
||
server.use( | ||
rest.get("/api/auth/session", (req, res, ctx) => { | ||
sessionRouteCall() | ||
res(ctx.status(200), ctx.json(mockSession)) | ||
}) | ||
) | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
}) | ||
|
||
render(<ProviderFlow />) | ||
test("will refetch the session when the browser tab becomes active again", async () => { | ||
render(<ProviderFlow session={mockSession} />) | ||
|
||
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading") | ||
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading") | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
await waitFor(() => { | ||
expect(sessionRouteCall).toHaveBeenCalledTimes(1) | ||
// Hide the current tab | ||
changeTabVisibility("hidden") | ||
|
||
const session1 = screen.getByTestId("session-consumer-1").textContent | ||
const session2 = screen.getByTestId("session-consumer-2").textContent | ||
// Given the current tab got hidden, it should not attempt to re-fetch the session | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
expect(session1).toEqual(session2) | ||
// Make the tab again visible | ||
changeTabVisibility("visible") | ||
|
||
// Given the user made the tab visible again, now attempts to sync and re-fetch the session | ||
return waitFor(() => { | ||
expect(fetchSpy).toHaveBeenCalledTimes(1) | ||
expect(fetchSpy).toHaveBeenCalledWith( | ||
"/api/auth/session", | ||
expect.anything() | ||
) | ||
}) | ||
}) | ||
|
||
test("will refetch the session if told to do so programmatically from another window", async () => { | ||
render(<ProviderFlow session={mockSession} />) | ||
|
||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
// Hide the current tab | ||
changeTabVisibility("hidden") | ||
|
||
// Given the current tab got hidden, it should not attempt to re-fetch the session | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
// simulate sign-out triggered by another tab | ||
signOut({ redirect: false }) | ||
|
||
// Given signed out in another tab, it attempts to sync and re-fetch the session | ||
return waitFor(() => { | ||
expect(fetchSpy).toHaveBeenCalledWith( | ||
"/api/auth/session", | ||
expect.anything() | ||
) | ||
|
||
// We should have a call to sign-out and a call to refetch the session accordingly | ||
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(` | ||
Array [ | ||
"GET /api/auth/csrf", | ||
"POST /api/auth/signout", | ||
"GET /api/auth/session", | ||
] | ||
`) | ||
}) | ||
}) | ||
|
||
test("when there's an existing session, it won't initialize as loading", async () => { | ||
const sessionRouteCall = jest.fn() | ||
test("allows to customize how often the session will be re-fetched through polling", () => { | ||
jest.useFakeTimers() | ||
|
||
render(<ProviderFlow session={mockSession} refetchInterval={1} />) | ||
|
||
// we provided a mock session so it shouldn't try to fetch a new one | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(1000) | ||
|
||
expect(fetchSpy).toHaveBeenCalledTimes(1) | ||
expect(fetchSpy).toHaveBeenCalledWith("/api/auth/session", expect.anything()) | ||
|
||
jest.advanceTimersByTime(1000) | ||
|
||
// it should have tried to refetch the session, hence counting 2 calls to the session endpoint | ||
expect(fetchSpy).toHaveBeenCalledTimes(2) | ||
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(` | ||
Array [ | ||
"GET /api/auth/session", | ||
"GET /api/auth/session", | ||
] | ||
`) | ||
}) | ||
|
||
test("allows to customize the URL for session fetching", async () => { | ||
const myPath = "/api/v1/auth" | ||
|
||
server.use( | ||
rest.get("/api/auth/session", (req, res, ctx) => { | ||
sessionRouteCall() | ||
rest.get(`${myPath}/session`, (req, res, ctx) => | ||
res(ctx.status(200), ctx.json(mockSession)) | ||
}) | ||
) | ||
) | ||
|
||
render(<ProviderFlow session={mockSession} />) | ||
render(<ProviderFlow session={mockSession} basePath={myPath} />) | ||
|
||
expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent( | ||
"loading" | ||
) | ||
// there's an existing session so it should not try to fetch a new one | ||
expect(fetchSpy).not.toHaveBeenCalled() | ||
|
||
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent( | ||
"loading" | ||
) | ||
// force a session refetch across all clients... | ||
getSession() | ||
|
||
expect(sessionRouteCall).not.toHaveBeenCalled() | ||
return waitFor(() => { | ||
expect(fetchSpy).toHaveBeenCalledTimes(1) | ||
expect(fetchSpy).toHaveBeenCalledWith( | ||
`${myPath}/session`, | ||
expect.anything() | ||
) | ||
}) | ||
}) | ||
|
||
function ProviderFlow({ options = {} }) { | ||
function ProviderFlow(props) { | ||
return ( | ||
<SessionProvider {...options}> | ||
<SessionProvider {...props}> | ||
<SessionConsumer /> | ||
<SessionConsumer testId="2" /> | ||
</SessionProvider> | ||
) | ||
} | ||
|
||
function SessionConsumer({ testId = 1 }) { | ||
const { data: session, status } = useSession() | ||
function SessionConsumer({ testId = 1, ...rest }) { | ||
const { data: session, status } = useSession(rest) | ||
|
||
return ( | ||
<div data-testid={`session-consumer-${testId}`}> | ||
<div data-testid={`session-${testId}`}> | ||
{status === "loading" ? "loading" : JSON.stringify(session)} | ||
</div> | ||
) | ||
} | ||
|
||
function changeTabVisibility(status) { | ||
const visibleStates = ["visible", "hidden"] | ||
|
||
if (!visibleStates.includes(status)) return | ||
|
||
Object.defineProperty(document, "visibilityState", { | ||
configurable: true, | ||
value: status, | ||
}) | ||
|
||
document.dispatchEvent(new Event("visibilitychange")) | ||
} |
Oops, something went wrong.