fix(06-05): make AuthSplash dead-end state reachable + persist redirect guard
- CalendarShell now captures maybeRedirectToLogin() return value in meQuery.isError effect
- When the one-shot guard is exhausted (returns false), arm loginRedirectExhausted state
- Render AuthSplash state=dead-end (tap-to-retry) when guard is exhausted, not indefinite redirecting spinner
- Reset loginRedirectExhausted on successful auth (meQuery.isSuccess) for session recovery
- Add sessionStorage.clear() to beforeEach so CalendarShell tests are isolated
- RED test committed in prior commit (36ef7a0)
This commit is contained in:
@@ -140,6 +140,8 @@ function renderWithClient(ui: React.ReactElement) {
|
|||||||
describe('CalendarShell — CAL-03 render smoke', () => {
|
describe('CalendarShell — CAL-03 render smoke', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
// Reset the one-shot redirect guard between tests
|
||||||
|
sessionStorage.clear()
|
||||||
|
|
||||||
// Default mock responses
|
// Default mock responses
|
||||||
;(fetchMe as Mock).mockResolvedValue({
|
;(fetchMe as Mock).mockResolvedValue({
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
// Ref to ensure the session-expiry redirect timer fires only once per expiry
|
// Ref to ensure the session-expiry redirect timer fires only once per expiry
|
||||||
const sessionExpiredRedirectFired = useRef(false)
|
const sessionExpiredRedirectFired = useRef(false)
|
||||||
|
// When the one-shot redirect guard is already exhausted (flag set from a prior
|
||||||
|
// navigation), maybeRedirectToLogin() returns false — arm the dead-end state so
|
||||||
|
// AuthSplash shows the tap-to-retry recovery instead of spinning forever (D-11).
|
||||||
|
const [loginRedirectExhausted, setLoginRedirectExhausted] = useState(false)
|
||||||
|
|
||||||
// Fetch current user to build per-member color config
|
// Fetch current user to build per-member color config
|
||||||
const meQuery = useQuery({
|
const meQuery = useQuery({
|
||||||
@@ -195,19 +199,25 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
|||||||
// If this is the first failure, maybeRedirectToLogin() sets a sessionStorage
|
// If this is the first failure, maybeRedirectToLogin() sets a sessionStorage
|
||||||
// flag and navigates the browser to /api/login (top-level nav, no CORS block).
|
// flag and navigates the browser to /api/login (top-level nav, no CORS block).
|
||||||
// The page will unmount as the browser navigates. If the flag is already set
|
// The page will unmount as the browser navigates. If the flag is already set
|
||||||
// (already bounced through login once and still failing), returns false and the
|
// (already bounced through login once and still failing), returns false — arm
|
||||||
// existing "Sign-in required" branch below renders.
|
// loginRedirectExhausted so the dead-end "Tap to try again" branch renders
|
||||||
|
// instead of spinning indefinitely (D-11 dead-end recovery).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meQuery.isError) {
|
if (meQuery.isError) {
|
||||||
maybeRedirectToLogin()
|
const willRedirect = maybeRedirectToLogin()
|
||||||
|
if (!willRedirect) {
|
||||||
|
setLoginRedirectExhausted(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [meQuery.isError])
|
}, [meQuery.isError])
|
||||||
|
|
||||||
// Clear the one-shot flag on a successful /api/me load so a later session
|
// Clear the one-shot flag on a successful /api/me load so a later session
|
||||||
// expiry can trigger another redirect instead of showing "Sign-in required".
|
// expiry can trigger another redirect instead of showing "Sign-in required".
|
||||||
|
// Also reset the dead-end state in case the component is reused after auth recovery.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meQuery.isSuccess) {
|
if (meQuery.isSuccess) {
|
||||||
clearLoginRedirect()
|
clearLoginRedirect()
|
||||||
|
setLoginRedirectExhausted(false)
|
||||||
}
|
}
|
||||||
}, [meQuery.isSuccess])
|
}, [meQuery.isSuccess])
|
||||||
|
|
||||||
@@ -257,7 +267,13 @@ export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (meQuery.isError) {
|
if (meQuery.isError) {
|
||||||
// useEffect at line 196 calls maybeRedirectToLogin() — splash shows while redirect fires
|
// loginRedirectExhausted: the one-shot guard was already set (prior navigation),
|
||||||
|
// so maybeRedirectToLogin() returned false — show dead-end tap-to-retry (D-11).
|
||||||
|
// Otherwise the useEffect at line 196 already fired maybeRedirectToLogin() and
|
||||||
|
// the browser is navigating — show the redirecting splash while it does.
|
||||||
|
if (loginRedirectExhausted) {
|
||||||
|
return <AuthSplash state="dead-end" />
|
||||||
|
}
|
||||||
return <AuthSplash state="redirecting" />
|
return <AuthSplash state="redirecting" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user