|
1 | 1 | import React from "react"; |
2 | | -import { describe, expect, it } from "vite-plus/test"; |
| 2 | +import { describe, expect, it, vi } from "vite-plus/test"; |
| 3 | +import { devOnCaughtError } from "../packages/vinext/src/server/app-browser-error.js"; |
3 | 4 | import { |
4 | 5 | APP_INTERCEPTION_CONTEXT_KEY, |
5 | 6 | APP_ROOT_LAYOUT_KEY, |
@@ -444,6 +445,72 @@ describe("app browser entry previousNextUrl helpers", () => { |
444 | 445 | }); |
445 | 446 | }); |
446 | 447 |
|
| 448 | +describe("devOnCaughtError (hydrateRoot dev handler)", () => { |
| 449 | + it("logs caught errors to console.error", () => { |
| 450 | + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); |
| 451 | + try { |
| 452 | + const err = new Error("Maximum update depth exceeded"); |
| 453 | + devOnCaughtError(err, { componentStack: "\n at List\n at Apps" }); |
| 454 | + expect(consoleSpy).toHaveBeenCalled(); |
| 455 | + const loggedErrors = consoleSpy.mock.calls.map((args) => args[0]); |
| 456 | + expect(loggedErrors).toContain(err); |
| 457 | + } finally { |
| 458 | + consoleSpy.mockRestore(); |
| 459 | + } |
| 460 | + }); |
| 461 | + |
| 462 | + it("includes the React component stack in the log when provided", () => { |
| 463 | + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); |
| 464 | + try { |
| 465 | + devOnCaughtError(new Error("boom"), { |
| 466 | + componentStack: "\n at List (apps/list.tsx:202)", |
| 467 | + }); |
| 468 | + expect(consoleSpy).toHaveBeenCalledTimes(2); |
| 469 | + expect(String(consoleSpy.mock.calls[1][0])).toContain("apps/list.tsx:202"); |
| 470 | + } finally { |
| 471 | + consoleSpy.mockRestore(); |
| 472 | + } |
| 473 | + }); |
| 474 | + |
| 475 | + it("does not re-dispatch a window 'error' event (would trigger Vite overlay)", () => { |
| 476 | + // This test runs in a Node environment where `window` is undefined, so the |
| 477 | + // listener registration is skipped and windowErrorCount stays 0 trivially. |
| 478 | + // The test still documents the contract: devOnCaughtError must not dispatch |
| 479 | + // window error events (which would re-trigger the Vite overlay). If a DOM |
| 480 | + // environment is ever added to this project, this will become a live check. |
| 481 | + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); |
| 482 | + let windowErrorCount = 0; |
| 483 | + const onError = (): void => { |
| 484 | + windowErrorCount += 1; |
| 485 | + }; |
| 486 | + if (typeof window !== "undefined") { |
| 487 | + window.addEventListener("error", onError); |
| 488 | + } |
| 489 | + try { |
| 490 | + devOnCaughtError(new Error("caught by user error.tsx"), {}); |
| 491 | + expect(windowErrorCount).toBe(0); |
| 492 | + } finally { |
| 493 | + if (typeof window !== "undefined") { |
| 494 | + window.removeEventListener("error", onError); |
| 495 | + } |
| 496 | + consoleSpy.mockRestore(); |
| 497 | + } |
| 498 | + }); |
| 499 | + |
| 500 | + it("is not a no-op (regression guard against `() => {}`)", () => { |
| 501 | + // Explicit regression guard: the original implementation was `() => {}`, |
| 502 | + // which silently swallowed all caught errors. This test ensures the handler |
| 503 | + // always calls console.error at least once. |
| 504 | + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); |
| 505 | + try { |
| 506 | + devOnCaughtError(new Error("regression"), {}); |
| 507 | + expect(consoleSpy.mock.calls.length).toBeGreaterThan(0); |
| 508 | + } finally { |
| 509 | + consoleSpy.mockRestore(); |
| 510 | + } |
| 511 | + }); |
| 512 | +}); |
| 513 | + |
447 | 514 | describe("mounted slot helpers", () => { |
448 | 515 | it("collects only mounted slot ids", () => { |
449 | 516 | const elements: AppElements = createResolvedElements("route:/dashboard", "/", null, { |
|
0 commit comments