Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(client): fully cover client module #2295

Merged
merged 14 commits into from
Aug 26, 2021
82 changes: 70 additions & 12 deletions src/client/__tests__/client-provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { SessionProvider, useSession } from "../react"

const origDocumentVisibility = document.visibilityState

beforeAll(() => {
server.listen()
})

afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
changeTabVisibility(origDocumentVisibility)
})

afterAll(() => {
Expand All @@ -33,11 +36,11 @@ test("it won't allow to fetch the session in isolation without a session context
})

test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
const sessionRouteSpy = jest.fn()

server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
sessionRouteSpy()
res(ctx.status(200), ctx.json(mockSession))
})
)
Expand All @@ -48,7 +51,7 @@ test("fetches the session once and re-uses it for different consumers", async ()
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")

await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
expect(sessionRouteSpy).toHaveBeenCalledTimes(1)

const session1 = screen.getByTestId("session-consumer-1").textContent
const session2 = screen.getByTestId("session-consumer-2").textContent
Expand All @@ -58,29 +61,71 @@ test("fetches the session once and re-uses it for different consumers", async ()
})

test("when there's an existing session, it won't initialize as loading", async () => {
const sessionRouteCall = jest.fn()
const sessionRouteSpy = jest.fn()

server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
sessionRouteSpy()
res(ctx.status(200), ctx.json(mockSession))
})
)

/**
* TODO: How can we force a clean session state between tests? At the moment
* the library uses this internal constant: __NEXTAUTH to track calls
* and we can't clean it between tests.
*/
render(<ProviderFlow session={mockSession} />)

expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent(
"loading"
)
expect(sessionRouteSpy).not.toHaveBeenCalled()
})

test("will refetch the session when the browser tab becomes active again", async () => {
const sessionRouteSpy = jest.fn()

expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
"loading"
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteSpy()
res(ctx.status(200), ctx.json(mockSession))
})
)

expect(sessionRouteCall).not.toHaveBeenCalled()
render(<ProviderFlow session={mockSession} />)

await waitFor(() => {
expect(sessionRouteSpy).not.toHaveBeenCalled()
})

// Hide the current tab
changeTabVisibility("hidden")

// Given the current tab got hidden, it should not attempt to re-fetch the session
await waitFor(() => {
expect(sessionRouteSpy).not.toHaveBeenCalled()
})

// Make the tab again visible
changeTabVisibility("visible")

// Given the user made the tab visible again, now attempts to sync and re-fetch the session
await waitFor(() => {
expect(sessionRouteSpy).toHaveBeenCalledTimes(1)
})
})

function ProviderFlow({ options = {} }) {
test.todo(
"will refetch the session if told to do so programmatically from another window"
)

test.todo(
"allows to customize how often the session will be re-fetched through polling"
)

test.todo("allows to customize the URL for session fetching")

test.todo("allows to customize session stale time")

function ProviderFlow(props) {
return (
<SessionProvider {...options}>
<SessionConsumer />
Expand All @@ -98,3 +143,16 @@ function SessionConsumer({ testId = 1 }) {
</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"))
}
12 changes: 6 additions & 6 deletions src/client/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,14 @@ export function SessionProvider(props) {
setSession(__NEXTAUTH._session)
return
}

if (
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it
(staleTime === 0 && !event) ||
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a "stroage" event
// tab or window that will come through as a "storage" event
// event anyway)
(staleTime > 0 && __NEXTAUTH._session === null) ||
// Bail out early if the client session is not stale yet
Expand Down Expand Up @@ -270,12 +269,13 @@ export function SessionProvider(props) {
}, [])

React.useEffect(() => {
// Set up visibility change
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
// Listen for when the page is visible, if the user switches tabs
// and makes our tab visible again, re-fetch the session.
const visibilityHandler = () => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
if (document.visibilityState === "visible")
__NEXTAUTH._getSession({ event: "visibilitychange" })
}

document.addEventListener("visibilitychange", visibilityHandler, false)
return () =>
document.removeEventListener("visibilitychange", visibilityHandler, false)
Expand Down