feat: 前端统一 Logger 模块 — 接口、双流 Sink、ESLint 规则、测试
This commit is contained in:
390
tests/web/utils/logger.test.ts
Normal file
390
tests/web/utils/logger.test.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
|
||||
import type { Sink } from "../../../src/web/utils/logger";
|
||||
|
||||
import {
|
||||
AntdMessageSink,
|
||||
ConsoleSink,
|
||||
createConsoleLogger,
|
||||
createDefaultLogger,
|
||||
createMemoryLogger,
|
||||
createNoopLogger,
|
||||
} from "../../../src/web/utils/logger";
|
||||
|
||||
describe("ConsoleSink", () => {
|
||||
test("调试环境输出 debug 级别", () => {
|
||||
const sink = new ConsoleSink(false);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.log;
|
||||
console.log = spy;
|
||||
|
||||
sink.write("debug", "测试消息", undefined, {});
|
||||
console.log = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:DEBUG\] 测试消息/);
|
||||
});
|
||||
|
||||
test("生产环境屏蔽 debug 级别", () => {
|
||||
const sink = new ConsoleSink(true);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.log;
|
||||
console.log = spy;
|
||||
|
||||
sink.write("debug", "测试消息", undefined, {});
|
||||
console.log = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("生产环境屏蔽 info 级别", () => {
|
||||
const sink = new ConsoleSink(true);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.log;
|
||||
console.log = spy;
|
||||
|
||||
sink.write("info", "测试消息", undefined, {});
|
||||
console.log = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("生产环境保留 warn 级别", () => {
|
||||
const sink = new ConsoleSink(true);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.warn;
|
||||
console.warn = spy;
|
||||
|
||||
sink.write("warn", "测试消息", undefined, {});
|
||||
console.warn = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:WARN\] 测试消息/);
|
||||
});
|
||||
|
||||
test("生产环境保留 error 级别", () => {
|
||||
const sink = new ConsoleSink(true);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.error;
|
||||
console.error = spy;
|
||||
|
||||
sink.write("error", "测试消息", undefined, {});
|
||||
console.error = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:ERROR\] 测试消息/);
|
||||
});
|
||||
|
||||
test("绑定信息追加到消息后缀", () => {
|
||||
const sink = new ConsoleSink(false);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.log;
|
||||
console.log = spy;
|
||||
|
||||
sink.write("info", "测试消息", undefined, { id: "123", page: "projects" });
|
||||
console.log = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:INFO\] 测试消息 \[id=123\]\[page=projects\]/);
|
||||
});
|
||||
|
||||
test("data 透传不序列化", () => {
|
||||
const sink = new ConsoleSink(false);
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.error;
|
||||
console.error = spy;
|
||||
const err = new Error("测试错误");
|
||||
|
||||
sink.write("error", "失败", err, {});
|
||||
console.error = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[1]).toBe(err);
|
||||
});
|
||||
|
||||
test("error 级别映射到 console.error", () => {
|
||||
const sink = new ConsoleSink(false);
|
||||
const logSpy = mock((..._args: unknown[]) => {});
|
||||
const errorSpy = mock((..._args: unknown[]) => {});
|
||||
const origLog = console.log;
|
||||
const origError = console.error;
|
||||
console.log = logSpy;
|
||||
console.error = errorSpy;
|
||||
|
||||
sink.write("error", "错误", undefined, {});
|
||||
console.log = origLog;
|
||||
console.error = origError;
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("warn 级别映射到 console.warn", () => {
|
||||
const sink = new ConsoleSink(false);
|
||||
const logSpy = mock((..._args: unknown[]) => {});
|
||||
const warnSpy = mock((..._args: unknown[]) => {});
|
||||
const origLog = console.log;
|
||||
const origWarn = console.warn;
|
||||
console.log = logSpy;
|
||||
console.warn = warnSpy;
|
||||
|
||||
sink.write("warn", "警告", undefined, {});
|
||||
console.log = origLog;
|
||||
console.warn = origWarn;
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AntdMessageSink", () => {
|
||||
test("warn 级别调用 message.warning", () => {
|
||||
const warningSpy = mock(() => {});
|
||||
const messageApi = {
|
||||
error: mock(() => {}),
|
||||
info: mock(() => {}),
|
||||
loading: mock(() => {}),
|
||||
success: mock(() => {}),
|
||||
warning: warningSpy,
|
||||
};
|
||||
const sink = new AntdMessageSink(messageApi as never);
|
||||
|
||||
sink.write("warn", "操作警告", undefined, {});
|
||||
|
||||
expect(warningSpy).toHaveBeenCalledWith("操作警告");
|
||||
});
|
||||
|
||||
test("error 级别调用 message.error", () => {
|
||||
const errorSpy = mock(() => {});
|
||||
const messageApi = {
|
||||
error: errorSpy,
|
||||
info: mock(() => {}),
|
||||
loading: mock(() => {}),
|
||||
success: mock(() => {}),
|
||||
warning: mock(() => {}),
|
||||
};
|
||||
const sink = new AntdMessageSink(messageApi as never);
|
||||
|
||||
sink.write("error", "操作失败", undefined, {});
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith("操作失败");
|
||||
});
|
||||
|
||||
test("debug 级别不触发 notification", () => {
|
||||
const messageApi = {
|
||||
error: mock(() => {}),
|
||||
info: mock(() => {}),
|
||||
loading: mock(() => {}),
|
||||
success: mock(() => {}),
|
||||
warning: mock(() => {}),
|
||||
};
|
||||
const sink = new AntdMessageSink(messageApi as never);
|
||||
|
||||
sink.write("debug", "调试消息", undefined, {});
|
||||
|
||||
expect(messageApi.info).toHaveBeenCalledTimes(0);
|
||||
expect(messageApi.warning).toHaveBeenCalledTimes(0);
|
||||
expect(messageApi.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("info 级别不触发 notification", () => {
|
||||
const messageApi = {
|
||||
error: mock(() => {}),
|
||||
info: mock(() => {}),
|
||||
loading: mock(() => {}),
|
||||
success: mock(() => {}),
|
||||
warning: mock(() => {}),
|
||||
};
|
||||
const sink = new AntdMessageSink(messageApi as never);
|
||||
|
||||
sink.write("info", "信息消息", undefined, {});
|
||||
|
||||
expect(messageApi.info).toHaveBeenCalledTimes(0);
|
||||
expect(messageApi.warning).toHaveBeenCalledTimes(0);
|
||||
expect(messageApi.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NoopLogger", () => {
|
||||
test("所有方法静默不抛异常", () => {
|
||||
const logger = createNoopLogger();
|
||||
|
||||
logger.debug("调试");
|
||||
logger.info("信息");
|
||||
logger.warn("警告");
|
||||
logger.error("错误");
|
||||
|
||||
expect(logger.child({ page: "test" })).toBe(logger);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryLogger", () => {
|
||||
test("记录所有级别的日志", () => {
|
||||
const logger = createMemoryLogger();
|
||||
|
||||
logger.debug("调试");
|
||||
logger.info("信息");
|
||||
logger.warn("警告");
|
||||
logger.error("错误");
|
||||
|
||||
expect(logger.entries).toEqual([
|
||||
{ data: undefined, level: "debug", message: "调试" },
|
||||
{ data: undefined, level: "info", message: "信息" },
|
||||
{ data: undefined, level: "warn", message: "警告" },
|
||||
{ data: undefined, level: "error", message: "错误" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("记录附带 data 的日志", () => {
|
||||
const logger = createMemoryLogger();
|
||||
const err = new Error("测试");
|
||||
|
||||
logger.error("失败", err);
|
||||
|
||||
expect(logger.entries[0]).toEqual({ data: err, level: "error", message: "失败" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultLogger isProduction", () => {
|
||||
function createSpySink(): { entries: Array<{ data: unknown; level: string; message: string }>; sink: Sink } {
|
||||
const entries: Array<{ data: unknown; level: string; message: string }> = [];
|
||||
return {
|
||||
entries,
|
||||
sink: {
|
||||
write(level, message, data) {
|
||||
entries.push({ data, level, message });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("isProduction=true 时 debug/info 不记录", () => {
|
||||
const spy = createSpySink();
|
||||
const logger = createDefaultLogger([spy.sink], true);
|
||||
|
||||
logger.debug("调试");
|
||||
logger.info("信息");
|
||||
|
||||
expect(spy.entries).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("isProduction=true 时 warn/error 正常记录", () => {
|
||||
const spy = createSpySink();
|
||||
const logger = createDefaultLogger([spy.sink], true);
|
||||
|
||||
logger.warn("警告");
|
||||
logger.error("错误");
|
||||
|
||||
expect(spy.entries).toHaveLength(2);
|
||||
expect(spy.entries[0]!.level).toBe("warn");
|
||||
expect(spy.entries[1]!.level).toBe("error");
|
||||
});
|
||||
|
||||
test("isProduction=false 时 debug/info 正常记录", () => {
|
||||
const spy = createSpySink();
|
||||
const logger = createDefaultLogger([spy.sink], false);
|
||||
|
||||
logger.debug("调试");
|
||||
logger.info("信息");
|
||||
|
||||
expect(spy.entries).toHaveLength(2);
|
||||
expect(spy.entries[0]!.level).toBe("debug");
|
||||
expect(spy.entries[1]!.level).toBe("info");
|
||||
});
|
||||
});
|
||||
|
||||
describe("child() 作用域", () => {
|
||||
test("child 绑定信息输出到日志前缀", () => {
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.warn;
|
||||
console.warn = spy;
|
||||
const sink = new ConsoleSink(false);
|
||||
const logger = createDefaultLogger([sink], false).child({ page: "projects" });
|
||||
logger.warn("测试");
|
||||
|
||||
console.warn = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:WARN\] 测试 \[page=projects\]/);
|
||||
});
|
||||
|
||||
test("嵌套 child 追加绑定", () => {
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.warn;
|
||||
console.warn = spy;
|
||||
const sink = new ConsoleSink(false);
|
||||
const logger = createDefaultLogger([sink], false).child({ page: "projects" }).child({ action: "delete" });
|
||||
logger.warn("测试");
|
||||
|
||||
console.warn = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:WARN\] 测试 \[page=projects\]\[action=delete\]/);
|
||||
});
|
||||
|
||||
test("嵌套 child 同 key 覆盖", () => {
|
||||
const spy = mock((..._args: unknown[]) => {});
|
||||
const orig = console.warn;
|
||||
console.warn = spy;
|
||||
const sink = new ConsoleSink(false);
|
||||
const logger = createDefaultLogger([sink], false).child({ page: "projects" }).child({ page: "models" });
|
||||
logger.warn("测试");
|
||||
|
||||
console.warn = orig;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const call = spy.mock.calls[0]!;
|
||||
expect(call[0]).toMatch(/\[Alfred:WARN\] 测试 \[page=models\]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setLevel 运行时调整", () => {
|
||||
test("setLevel 可提高最小输出级别", () => {
|
||||
const spy = (() => {
|
||||
const entries: Array<{ level: string; message: string }> = [];
|
||||
return {
|
||||
entries,
|
||||
sink: {
|
||||
write(level: string, message: string) {
|
||||
entries.push({ level, message });
|
||||
},
|
||||
},
|
||||
};
|
||||
})();
|
||||
const logger = createDefaultLogger([spy.sink], false);
|
||||
|
||||
logger.debug("调试");
|
||||
expect(spy.entries).toHaveLength(1);
|
||||
|
||||
logger.setLevel("error");
|
||||
logger.debug("调试2");
|
||||
logger.warn("警告");
|
||||
logger.error("错误");
|
||||
|
||||
expect(spy.entries).toHaveLength(2);
|
||||
expect(spy.entries[1]!.level).toBe("error");
|
||||
expect(spy.entries[1]!.message).toBe("错误");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createConsoleLogger", () => {
|
||||
test("返回非 null Logger 实例", () => {
|
||||
const logger = createConsoleLogger();
|
||||
expect(logger).not.toBeNull();
|
||||
expect(typeof logger.debug).toBe("function");
|
||||
expect(typeof logger.info).toBe("function");
|
||||
expect(typeof logger.warn).toBe("function");
|
||||
expect(typeof logger.error).toBe("function");
|
||||
expect(typeof logger.child).toBe("function");
|
||||
expect(typeof logger.setLevel).toBe("function");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user