|
@@ -0,0 +1,186 @@
|
|
|
|
|
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
|
+
|
|
|
|
|
+beforeEach(() => {
|
|
|
|
|
+ localStorage.clear()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+afterEach(() => {
|
|
|
|
|
+ vi.restoreAllMocks()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+async function freshLogger() {
|
|
|
|
|
+ vi.resetModules()
|
|
|
|
|
+ return import("@/lib/logger")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+describe("createLogger", () => {
|
|
|
|
|
+ it("logs debug when global filter is wildcard", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "*")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("test message")
|
|
|
|
|
+ expect(spy).toHaveBeenCalledWith("[MyFeature]", "test message")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("logs info when global filter is wildcard", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "*")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("info message")
|
|
|
|
|
+ expect(spy).toHaveBeenCalledWith("[MyFeature]", "info message")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("does not log debug when no global filter matches", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "OtherNamespace")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("should not appear")
|
|
|
|
|
+ expect(spy).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("logs when namespace matches specific global filter", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "Block,MyFeature,Parse")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("visible")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("does not log for non-matching namespace in comma-separated filter", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "Block,Parse")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("Other")
|
|
|
|
|
+ const spy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("invisible")
|
|
|
|
|
+ expect(spy).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe("component filter", () => {
|
|
|
|
|
+ it("component filter wildcard logs regardless of global", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "Unrelated")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature", "*")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("visible via component filter")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("component filter with matching namespace logs", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "Unrelated")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature", "MyFeature,Other")
|
|
|
|
|
+ const spy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("visible")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("component filter with non-matching namespace suppresses logs", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "*")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature", "Other")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("suppressed by component filter")
|
|
|
|
|
+ expect(spy).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe("level filtering", () => {
|
|
|
|
|
+ it("does not log debug when minLevel is info", async () => {
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("should not appear")
|
|
|
|
|
+ expect(spy).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("logs warn when namespace matches global filter", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "MyFeature")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
|
|
|
+ log.warn("warning is visible")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("logs error when namespace matches global filter", async () => {
|
|
|
|
|
+ localStorage.setItem("editor:debug", "MyFeature")
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("MyFeature")
|
|
|
|
|
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
|
|
|
+ log.error("error is always visible")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe("configureDebug", () => {
|
|
|
|
|
+ it("updates namespace at runtime", async () => {
|
|
|
|
|
+ const { configureDebug, createLogger } = await freshLogger()
|
|
|
|
|
+ configureDebug({ namespaces: "RuntimeNS", minLevel: "debug" })
|
|
|
|
|
+
|
|
|
|
|
+ const matchingLog = createLogger("RuntimeNS")
|
|
|
|
|
+ const spyMatch = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ matchingLog.debug("matches runtime config")
|
|
|
|
|
+ expect(spyMatch).toHaveBeenCalled()
|
|
|
|
|
+ spyMatch.mockRestore()
|
|
|
|
|
+
|
|
|
|
|
+ const nonMatchingLog = createLogger("Other")
|
|
|
|
|
+ const spyNoMatch = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ nonMatchingLog.debug("does not match")
|
|
|
|
|
+ expect(spyNoMatch).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("updates minLevel at runtime to allow debug", async () => {
|
|
|
|
|
+ const { configureDebug, createLogger } = await freshLogger()
|
|
|
|
|
+ configureDebug({ namespaces: "*", minLevel: "debug" })
|
|
|
|
|
+
|
|
|
|
|
+ const log = createLogger("Any")
|
|
|
|
|
+ const spy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("now visible")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("updates minLevel at runtime to suppress info", async () => {
|
|
|
|
|
+ const { configureDebug, createLogger } = await freshLogger()
|
|
|
|
|
+ configureDebug({ namespaces: "*", minLevel: "warn" })
|
|
|
|
|
+
|
|
|
|
|
+ const log = createLogger("Any")
|
|
|
|
|
+ const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("suppressed by minLevel")
|
|
|
|
|
+ expect(infoSpy).not.toHaveBeenCalled()
|
|
|
|
|
+
|
|
|
|
|
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
|
|
|
+ log.warn("warn is above minLevel")
|
|
|
|
|
+ expect(warnSpy).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe("default config", () => {
|
|
|
|
|
+ it("uses default config when localStorage is empty", async () => {
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("Any")
|
|
|
|
|
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
|
|
|
+ log.warn("warn visible by default")
|
|
|
|
|
+ expect(warnSpy).toHaveBeenCalled()
|
|
|
|
|
+
|
|
|
|
|
+ const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {})
|
|
|
|
|
+ log.debug("debug not visible by default")
|
|
|
|
|
+ expect(debugSpy).not.toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("handles localStorage unavailable gracefully", async () => {
|
|
|
|
|
+ const getItem = vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
|
|
|
|
+ throw new Error("storage unavailable")
|
|
|
|
|
+ })
|
|
|
|
|
+ const { createLogger } = await freshLogger()
|
|
|
|
|
+ const log = createLogger("Any")
|
|
|
|
|
+ const spy = vi.spyOn(console, "info").mockImplementation(() => {})
|
|
|
|
|
+ log.info("still works")
|
|
|
|
|
+ expect(spy).toHaveBeenCalled()
|
|
|
|
|
+ getItem.mockRestore()
|
|
|
|
|
+ })
|
|
|
|
|
+})
|