Explicitly test for console warnings & errors (#6502)

# Description of Changes
Disallow warnings and errors from being thrown in the browser console
during tests unless explicitly expected in the test. Also adds a
Playwright test to prod around some main UI areas and checks that no
warnings/errors have been thrown.
This commit is contained in:
James Brunton
2026-06-09 08:34:02 +00:00
committed by GitHub
parent 002de06411
commit 0e3cbb3cf2
28 changed files with 544 additions and 213 deletions
@@ -6,6 +6,7 @@ import {
POST_LOGIN_REDIRECT_STORAGE_KEY,
springAuth,
} from "@app/auth/springAuthClient";
import { expectConsole } from "@app/tests/failOnConsole";
// Mock springAuth; keep the real redirect-path helpers.
vi.mock("@app/auth/springAuthClient", async () => {
@@ -90,6 +91,7 @@ describe("AuthCallback", () => {
});
it("should redirect to login when no access token in hash", async () => {
expectConsole.error(/\[AuthCallback\] No access_token in URL fragment/);
// No hash or empty hash
window.location.hash = "";
@@ -109,6 +111,7 @@ describe("AuthCallback", () => {
});
it("should redirect to login when token validation fails", async () => {
expectConsole.error(/\[AuthCallback\] Failed to validate token/);
const invalidToken = "invalid-oauth-token";
window.location.hash = `#access_token=${invalidToken}`;
@@ -137,6 +140,7 @@ describe("AuthCallback", () => {
});
it("should handle errors gracefully", async () => {
expectConsole.error(/\[AuthCallback\] Authentication failed/);
const mockToken = "error-token";
window.location.hash = `#access_token=${mockToken}`;
@@ -18,42 +18,16 @@ export default function AuthCallback() {
const navigate = useNavigate();
const processingRef = useRef(false);
// Log component lifecycle
useEffect(() => {
const mountId = Math.random().toString(36).substring(7);
console.log(`[AuthCallback:${mountId}] 🔵 Component mounted`);
return () => {
console.log(`[AuthCallback:${mountId}] 🔴 Component unmounting`);
};
}, []);
const startedAt = performance.now();
const elapsed = () => `${(performance.now() - startedAt).toFixed(0)}ms`;
useEffect(() => {
const handleCallback = async () => {
const startTime = performance.now();
const executionId = Math.random().toString(36).substring(7);
console.log(
`[AuthCallback:${executionId}] ════════════════════════════════════`,
);
console.log(
`[AuthCallback:${executionId}] Starting authentication callback`,
);
console.log(`[AuthCallback:${executionId}] URL: ${window.location.href}`);
console.log(
`[AuthCallback:${executionId}] Hash: ${window.location.hash}`,
);
console.log(
`[AuthCallback:${executionId}] Document readyState: ${document.readyState}`,
);
if (
typeof window !== "undefined" &&
window.sessionStorage.getItem("stirling_sso_auto_login_logged_out") ===
"1"
) {
console.warn(
`[AuthCallback:${executionId}] ⚠️ Logout block active, skipping token processing`,
);
navigate("/login", {
replace: true,
state: { error: "You have been signed out. Please sign in again." },
@@ -62,30 +36,16 @@ export default function AuthCallback() {
}
// Prevent double execution (React 18 Strict Mode + navigate dependency)
if (processingRef.current) {
console.warn(
`[AuthCallback:${executionId}] ⚠️ Already processing, skipping duplicate execution`,
);
console.warn(
`[AuthCallback:${executionId}] This is expected in React Strict Mode (development)`,
);
return;
}
if (processingRef.current) return;
processingRef.current = true;
try {
console.log(
`[AuthCallback:${executionId}] Step 1: Extracting token from URL fragment`,
);
// Extract JWT from URL fragment (#access_token=...)
const hash = window.location.hash.substring(1); // Remove '#'
const params = new URLSearchParams(hash);
const token = params.get("access_token");
const hash = window.location.hash.substring(1);
const token = new URLSearchParams(hash).get("access_token");
if (!token) {
console.error(
`[AuthCallback:${executionId}] ❌ No access_token in URL fragment`,
`[AuthCallback] No access_token in URL fragment (${elapsed()})`,
);
navigate("/login", {
replace: true,
@@ -94,39 +54,13 @@ export default function AuthCallback() {
return;
}
console.log(
`[AuthCallback:${executionId}] ✓ Token extracted (length: ${token.length})`,
);
console.log(
`[AuthCallback:${executionId}] Step 2: Storing JWT in localStorage`,
);
// Store JWT in localStorage
localStorage.setItem("stirling_jwt", token);
console.log(
`[AuthCallback:${executionId}] ✓ JWT stored in localStorage`,
);
console.log(
`[AuthCallback:${executionId}] Step 3: Dispatching 'jwt-available' event`,
);
// Dispatch custom event for other components to react to JWT availability
window.dispatchEvent(new CustomEvent("jwt-available"));
console.log(`[AuthCallback:${executionId}] ✓ Event dispatched`);
console.log(
`[AuthCallback:${executionId}] Elapsed after jwt-available: ${(performance.now() - startTime).toFixed(2)}ms`,
);
console.log(
`[AuthCallback:${executionId}] Step 4: Validating token with backend`,
);
// Validate the token and load user info
// This calls /api/v1/auth/me with the JWT to get user details
const { data, error } = await springAuth.getSession();
if (error || !data.session) {
console.error(
`[AuthCallback:${executionId}] ❌ Failed to validate token:`,
`[AuthCallback] Failed to validate token (${elapsed()}):`,
error,
);
localStorage.removeItem("stirling_jwt");
@@ -137,68 +71,21 @@ export default function AuthCallback() {
return;
}
console.log(
`[AuthCallback:${executionId}] ✓ Token validated, user: ${data.session.user.username}`,
);
console.log(
`[AuthCallback:${executionId}] Step 5: Running platform-specific callback handlers`,
);
await handleAuthCallbackSuccess(token);
console.log(
`[AuthCallback:${executionId}] ✓ Callback handlers complete`,
);
console.log(
`[AuthCallback:${executionId}] Step 6: Waiting for context stabilization`,
);
// Wait for all context providers to process jwt-available event
// This prevents infinite render loop when coming from cross-domain SAML redirect
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(
`[AuthCallback:${executionId}] Elapsed after stabilization wait: ${(performance.now() - startTime).toFixed(2)}ms`,
);
const target = consumePostLoginRedirectPath() ?? "/";
console.log(
`[AuthCallback:${executionId}] Step 7: Navigating to ${target}`,
console.info(
`[AuthCallback] Authenticated ${data.session.user.username} in ${elapsed()}, navigating to ${target}`,
);
navigate(target, { replace: true });
const duration = performance.now() - startTime;
console.log(
`[AuthCallback:${executionId}] ✓ Authentication complete (${duration.toFixed(2)}ms)`,
);
console.log(
`[AuthCallback:${executionId}] ════════════════════════════════════`,
);
} catch (error) {
const duration = performance.now() - startTime;
console.error(
`[AuthCallback:${executionId}] ════════════════════════════════════`,
);
console.error(
`[AuthCallback:${executionId}] ❌ FATAL ERROR during authentication`,
);
console.error(`[AuthCallback:${executionId}] Error:`, error);
console.error(
`[AuthCallback:${executionId}] Error name:`,
(error as Error)?.name,
);
console.error(
`[AuthCallback:${executionId}] Error message:`,
(error as Error)?.message,
);
console.error(
`[AuthCallback:${executionId}] Error stack:`,
(error as Error)?.stack,
);
console.error(
`[AuthCallback:${executionId}] Duration before failure: ${duration.toFixed(2)}ms`,
);
console.error(
`[AuthCallback:${executionId}] ════════════════════════════════════`,
`[AuthCallback] Authentication failed (${elapsed()}):`,
error,
);
navigate("/login", {
replace: true,
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter, MemoryRouter } from "react-router-dom";
import { MantineProvider } from "@mantine/core";
@@ -176,7 +176,7 @@ describe("Login", () => {
});
});
it("should show loading state while auth is loading", () => {
it("should show loading state while auth is loading", async () => {
vi.mocked(useAuth).mockReturnValue({
session: null,
user: null,
@@ -187,13 +187,15 @@ describe("Login", () => {
refreshSession: vi.fn(),
});
render(
<TestWrapper>
<BrowserRouter>
<Login />
</BrowserRouter>
</TestWrapper>,
);
await act(async () => {
render(
<TestWrapper>
<BrowserRouter>
<Login />
</BrowserRouter>
</TestWrapper>,
);
});
// Component shouldn't redirect or show form while loading
expect(mockNavigate).not.toHaveBeenCalled();
@@ -510,26 +512,30 @@ describe("Login", () => {
expect(springAuth.signInWithPassword).not.toHaveBeenCalled();
});
it("should display session expired message from URL param", () => {
render(
<TestWrapper>
<MemoryRouter initialEntries={["/login?expired=true"]}>
<Login />
</MemoryRouter>
</TestWrapper>,
);
it("should display session expired message from URL param", async () => {
await act(async () => {
render(
<TestWrapper>
<MemoryRouter initialEntries={["/login?expired=true"]}>
<Login />
</MemoryRouter>
</TestWrapper>,
);
});
expect(screen.getByText(/session.*expired/i)).toBeInTheDocument();
});
it("should display account created success message", () => {
render(
<TestWrapper>
<MemoryRouter initialEntries={["/login?messageType=accountCreated"]}>
<Login />
</MemoryRouter>
</TestWrapper>,
);
it("should display account created success message", async () => {
await act(async () => {
render(
<TestWrapper>
<MemoryRouter initialEntries={["/login?messageType=accountCreated"]}>
<Login />
</MemoryRouter>
</TestWrapper>,
);
});
expect(screen.getByText(/account created/i)).toBeInTheDocument();
});
@@ -287,8 +287,6 @@ export default function Login() {
setPostLoginRedirectPath(returnPath);
}
console.log(`[Login] Signing in with provider: ${provider}`);
// Redirect to Spring OAuth2 endpoint using the actual provider ID from backend
// The backend returns the correct registration ID (e.g., 'authentik', 'oidc', 'keycloak')
const { error } = await springAuth.signInWithOAuth({
@@ -520,8 +518,6 @@ export default function Login() {
setError(null);
clearLogoutBlock();
console.log("[Login] Signing in with email:", email);
const { user, session, error } = await springAuth.signInWithPassword({
email: email.trim(),
password: password,
@@ -529,13 +525,11 @@ export default function Login() {
});
if (error) {
console.error("[Login] Email sign in error:", error);
setError(error.message);
if (error.mfaRequired || error.code === "invalid_mfa_code") {
setRequiresMfa(true);
}
} else if (user && session) {
console.log("[Login] Email sign in successful");
clearLogoutBlock();
setRequiresMfa(false);
setMfaCode("");