Skip to content

Commit

Permalink
test(client): fully cover client module (#2295)
Browse files Browse the repository at this point in the history
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
ubbe-xyz authored Aug 26, 2021
1 parent eb8ba69 commit d76f15b
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 54 deletions.
104 changes: 104 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@babel/preset-react": "^7.14.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^7.0.1",
"@testing-library/user-event": "^13.1.9",
"@types/nodemailer": "^6.4.2",
"@types/oauth": "^0.9.1",
Expand Down
182 changes: 135 additions & 47 deletions src/client/__tests__/client-provider.test.js
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"))
}
Loading

0 comments on commit d76f15b

Please sign in to comment.