Skip to content

Commit 3e49aea

Browse files
committed
Add cross-runtime test suite for node, browser, deno, bun, and workers
- 66 new vitest tests covering sub-loggers, masking, errors, exotic types, hidden mode, concurrency, and simulated worker/edge/deno/bun environments - 10 new playwright browser tests for json, masking, sub-loggers, transports - deno test adapter with 12 tests importing from dist/esm - add test:bun and test:deno npm scripts - add bun and deno CI jobs
1 parent 24f755a commit 3e49aea

12 files changed

+1242
-1
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,33 @@ jobs:
3535
run: npm run test:browser
3636
- name: Report coverage
3737
uses: codecov/codecov-action@v3
38+
39+
test-bun:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- uses: actions/checkout@v3
43+
- uses: actions/setup-node@v3
44+
with:
45+
node-version: 20.x
46+
- uses: oven-sh/setup-bun@v2
47+
with:
48+
bun-version: latest
49+
- run: npm ci
50+
- name: Run tests with Bun
51+
run: npm run test:bun
52+
53+
test-deno:
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v3
57+
- uses: actions/setup-node@v3
58+
with:
59+
node-version: 20.x
60+
- uses: denoland/setup-deno@v2
61+
with:
62+
deno-version: v2.x
63+
- run: npm ci
64+
- name: Build for Deno
65+
run: npm run build
66+
- name: Run Deno tests
67+
run: deno test --allow-read --allow-env tests/deno_runner.ts

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"",
4444
"test": "vitest run",
4545
"test:browser": "npx playwright test",
46+
"test:bun": "bunx vitest run",
47+
"test:deno": "npm run build && deno test --allow-read --allow-env tests/deno_runner.ts",
4648
"test:all": "vitest run && npx playwright test",
4749
"test-puppeteer-serve": "npm run build-browser && node tests/support/browser/server/index.cjs -p 4444",
4850
"coverage": "vitest run --coverage",

tests/25_deep_subloggers.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Logger } from "../src/index.js";
2+
3+
describe("Deep sub-loggers", () => {
4+
test("5-level chain preserves all parentNames", () => {
5+
const l1 = new Logger({ type: "hidden", name: "root" });
6+
const l2 = l1.getSubLogger({ name: "child1" });
7+
const l3 = l2.getSubLogger({ name: "child2" });
8+
const l4 = l3.getSubLogger({ name: "child3" });
9+
const l5 = l4.getSubLogger({ name: "child4" });
10+
11+
const logObj = l5.info("deep log");
12+
const meta = logObj?._meta;
13+
14+
expect(meta?.name).toBe("child4");
15+
expect(meta?.parentNames).toEqual(["root", "child1", "child2", "child3"]);
16+
});
17+
18+
test("deep chain accumulates prefixes from all ancestors", () => {
19+
const l1 = new Logger({ type: "hidden", prefix: ["[P1]"] });
20+
const l2 = l1.getSubLogger({ prefix: ["[P2]"] });
21+
const l3 = l2.getSubLogger({ prefix: ["[P3]"] });
22+
const l4 = l3.getSubLogger({ prefix: ["[P4]"] });
23+
24+
const logObj = l4.info("msg");
25+
26+
expect(logObj?.["0"]).toBe("[P1]");
27+
expect(logObj?.["1"]).toBe("[P2]");
28+
expect(logObj?.["2"]).toBe("[P3]");
29+
expect(logObj?.["3"]).toBe("[P4]");
30+
expect(logObj?.["4"]).toBe("msg");
31+
});
32+
33+
test("settings override at arbitrary depth", () => {
34+
const root = new Logger({ type: "hidden", minLevel: 0 });
35+
const child = root.getSubLogger({ minLevel: 4 });
36+
const grandchild = child.getSubLogger({});
37+
38+
expect(root.info("ok")).toBeDefined();
39+
expect(child.info("skipped")).toBeUndefined();
40+
expect(child.warn("ok")).toBeDefined();
41+
expect(grandchild.info("also skipped")).toBeUndefined();
42+
expect(grandchild.warn("ok")).toBeDefined();
43+
});
44+
45+
test("transport on parent is inherited by child sub-logger", () => {
46+
const captured: unknown[] = [];
47+
const root = new Logger({ type: "hidden" });
48+
root.attachTransport((logObj) => captured.push(logObj));
49+
50+
const child = root.getSubLogger({ name: "child" });
51+
root.info("root msg");
52+
child.info("child msg");
53+
54+
// child inherits parent's attachedTransports via getSubLogger settings spread
55+
expect(captured.length).toBe(2);
56+
expect((captured[0] as Record<string, unknown>)["0"]).toBe("root msg");
57+
expect((captured[1] as Record<string, unknown>)["0"]).toBe("child msg");
58+
});
59+
60+
test("sub-logger with independent logObj at each level", () => {
61+
const rootObj = { level: "root", shared: true };
62+
const root = new Logger({ type: "hidden" }, rootObj);
63+
64+
const childObj = { level: "child", shared: false };
65+
const child = root.getSubLogger({}, childObj);
66+
67+
const rootLog = root.info("r");
68+
expect(rootLog?.level).toBe("root");
69+
expect(rootLog?.shared).toBe(true);
70+
71+
const childLog = child.info("c");
72+
expect(childLog?.level).toBe("child");
73+
expect(childLog?.shared).toBe(false);
74+
});
75+
76+
test("sub-logger inherits type from parent", () => {
77+
const root = new Logger({ type: "hidden" });
78+
const child = root.getSubLogger({});
79+
80+
expect(child.settings.type).toBe("hidden");
81+
});
82+
83+
test("sub-logger with attachedTransports in settings", () => {
84+
const captured: unknown[] = [];
85+
const root = new Logger({ type: "hidden" });
86+
const child = root.getSubLogger({
87+
attachedTransports: [(logObj) => captured.push(logObj)],
88+
});
89+
90+
child.info("from child");
91+
92+
expect(captured.length).toBe(1);
93+
expect((captured[0] as Record<string, unknown>)["0"]).toBe("from child");
94+
});
95+
});

tests/26_advanced_masking.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Logger } from "../src/index.js";
2+
3+
describe("Advanced masking", () => {
4+
test("masks keys in deeply nested structure (5+ levels)", () => {
5+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["secret"] });
6+
const input = {
7+
a: {
8+
b: {
9+
c: {
10+
d: {
11+
e: {
12+
secret: "top-secret-value",
13+
visible: "ok",
14+
},
15+
},
16+
},
17+
},
18+
},
19+
};
20+
21+
const logObj = logger.info(input);
22+
const nested = (logObj as Record<string, unknown>)?.a as Record<string, unknown>;
23+
const deep = (nested?.b as Record<string, unknown>)?.c as Record<string, unknown>;
24+
const deeper = (deep?.d as Record<string, unknown>)?.e as Record<string, unknown>;
25+
26+
expect(deeper?.secret).toBe("[***]");
27+
expect(deeper?.visible).toBe("ok");
28+
});
29+
30+
test("masks keys in circular structures without throwing", () => {
31+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["password"] });
32+
const obj: Record<string, unknown> = { password: "secret123", name: "test" };
33+
obj.self = obj;
34+
35+
expect(() => {
36+
const logObj = logger.info(obj);
37+
expect(logObj?.password).toBe("[***]");
38+
expect(logObj?.name).toBe("test");
39+
}).not.toThrow();
40+
});
41+
42+
test("masking preserves Date instances", () => {
43+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["token"] });
44+
const now = new Date();
45+
const input = { token: "abc", created: now };
46+
47+
const logObj = logger.info(input);
48+
expect(logObj?.token).toBe("[***]");
49+
expect(logObj?.created).toBeInstanceOf(Date);
50+
expect((logObj?.created as Date).getTime()).toBe(now.getTime());
51+
});
52+
53+
test("masking preserves Map and Set instances", () => {
54+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["apiKey"] });
55+
const map = new Map([["a", 1]]);
56+
const set = new Set([1, 2, 3]);
57+
const input = { apiKey: "key123", data: { map, set } };
58+
59+
const logObj = logger.info(input);
60+
expect(logObj?.apiKey).toBe("[***]");
61+
const data = logObj?.data as Record<string, unknown>;
62+
expect(data?.map).toBeInstanceOf(Map);
63+
expect(data?.set).toBeInstanceOf(Set);
64+
});
65+
66+
test("masks keys within objects passed as multiple args", () => {
67+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["password"] });
68+
const a = { user: "alice", password: "pass1" };
69+
const b = { user: "bob", password: "pass2" };
70+
71+
const logObj = logger.info(a, b);
72+
expect((logObj?.["0"] as Record<string, unknown>)?.password).toBe("[***]");
73+
expect((logObj?.["1"] as Record<string, unknown>)?.password).toBe("[***]");
74+
expect((logObj?.["0"] as Record<string, unknown>)?.user).toBe("alice");
75+
});
76+
77+
test("key masking and regex masking work simultaneously", () => {
78+
const logger = new Logger({
79+
type: "hidden",
80+
maskValuesOfKeys: ["password"],
81+
maskValuesRegEx: [/\d{3}-\d{2}-\d{4}/],
82+
});
83+
84+
const input = {
85+
password: "secret",
86+
message: "SSN is 123-45-6789",
87+
};
88+
89+
const logObj = logger.info(input);
90+
expect(logObj?.password).toBe("[***]");
91+
expect(logObj?.message).toBe("SSN is [***]");
92+
});
93+
94+
test("case-insensitive masking matches regardless of key casing", () => {
95+
const logger = new Logger({
96+
type: "hidden",
97+
maskValuesOfKeys: ["password"],
98+
maskValuesOfKeysCaseInsensitive: true,
99+
});
100+
101+
const input = { Password: "a", PASSWORD: "b", pAsSwOrD: "c", other: "visible" };
102+
const logObj = logger.info(input);
103+
104+
expect(logObj?.Password).toBe("[***]");
105+
expect(logObj?.PASSWORD).toBe("[***]");
106+
expect(logObj?.pAsSwOrD).toBe("[***]");
107+
expect(logObj?.other).toBe("visible");
108+
});
109+
110+
test("original input is not mutated after logging", () => {
111+
const logger = new Logger({ type: "hidden", maskValuesOfKeys: ["password"] });
112+
const input = { password: "original", nested: { password: "also-original" } };
113+
const inputSnapshot = JSON.parse(JSON.stringify(input));
114+
115+
logger.info(input);
116+
117+
expect(input.password).toBe(inputSnapshot.password);
118+
expect(input.nested.password).toBe(inputSnapshot.nested.password);
119+
});
120+
121+
test("custom maskPlaceholder is used", () => {
122+
const logger = new Logger({
123+
type: "hidden",
124+
maskValuesOfKeys: ["secret"],
125+
maskPlaceholder: "<REDACTED>",
126+
});
127+
128+
const logObj = logger.info({ secret: "value" });
129+
expect(logObj?.secret).toBe("<REDACTED>");
130+
});
131+
});

tests/27_advanced_errors.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Logger } from "../src/index.js";
2+
3+
class HttpError extends Error {
4+
status: number;
5+
constructor(message: string, status: number) {
6+
super(message);
7+
this.name = "HttpError";
8+
this.status = status;
9+
}
10+
}
11+
12+
describe("Advanced error handling", () => {
13+
test("custom error subclass with extra properties", () => {
14+
const logger = new Logger({ type: "hidden" });
15+
const err = new HttpError("Not Found", 404);
16+
const logObj = logger.info(err);
17+
18+
expect(logObj?.nativeError).toBeInstanceOf(HttpError);
19+
expect(logObj?.name).toBe("HttpError");
20+
expect(logObj?.message).toBe("Not Found");
21+
expect(logObj?.stack).toBeInstanceOf(Array);
22+
});
23+
24+
test("error cause chain serializes nested causes", () => {
25+
const logger = new Logger({ type: "hidden" });
26+
const root = new Error("root cause");
27+
const middle = new Error("middle", { cause: root });
28+
const outer = new Error("outer", { cause: middle });
29+
30+
const logObj = logger.info(outer);
31+
32+
expect(logObj?.name).toBe("Error");
33+
expect(logObj?.message).toBe("outer");
34+
expect(logObj?.cause).toBeDefined();
35+
expect(logObj?.cause?.message).toBe("middle");
36+
expect(logObj?.cause?.cause?.message).toBe("root cause");
37+
});
38+
39+
test("deep cause chain is capped at max depth", () => {
40+
const logger = new Logger({ type: "hidden" });
41+
42+
// Build 8-level chain, expect capped at 5
43+
let err: Error = new Error("level-0");
44+
for (let i = 1; i <= 7; i++) {
45+
err = new Error(`level-${i}`, { cause: err });
46+
}
47+
48+
const logObj = logger.info(err);
49+
50+
let current = logObj as Record<string, unknown> | undefined;
51+
let depth = 0;
52+
while (current?.cause != null) {
53+
depth++;
54+
current = current.cause as Record<string, unknown>;
55+
}
56+
57+
expect(depth).toBeLessThanOrEqual(5);
58+
});
59+
60+
test("non-Error cause value (string) is wrapped", () => {
61+
const logger = new Logger({ type: "hidden" });
62+
const err = new Error("main") as Error & { cause: string };
63+
err.cause = "string cause";
64+
65+
const logObj = logger.info(err);
66+
67+
expect(logObj?.cause).toBeDefined();
68+
expect(logObj?.cause?.nativeError).toBeInstanceOf(Error);
69+
});
70+
71+
test("non-Error cause value (object) is wrapped", () => {
72+
const logger = new Logger({ type: "hidden" });
73+
const err = new Error("main") as Error & { cause: unknown };
74+
err.cause = { code: 404, msg: "not found" };
75+
76+
const logObj = logger.info(err);
77+
78+
expect(logObj?.cause).toBeDefined();
79+
expect(logObj?.cause?.nativeError).toBeInstanceOf(Error);
80+
});
81+
82+
test("AggregateError is logged without throwing", () => {
83+
const logger = new Logger({ type: "hidden" });
84+
const agg = new AggregateError([new Error("e1"), new Error("e2")], "multiple errors");
85+
86+
expect(() => {
87+
const logObj = logger.info(agg);
88+
expect(logObj).toBeDefined();
89+
expect(logObj?.name).toBe("AggregateError");
90+
expect(logObj?.message).toBe("multiple errors");
91+
}).not.toThrow();
92+
});
93+
94+
test("error with empty stack does not throw", () => {
95+
const logger = new Logger({ type: "hidden" });
96+
const err = new Error("no stack");
97+
err.stack = undefined;
98+
99+
expect(() => {
100+
const logObj = logger.info(err);
101+
expect(logObj).toBeDefined();
102+
expect(logObj?.stack).toBeInstanceOf(Array);
103+
}).not.toThrow();
104+
});
105+
106+
test("error with non-standard stack format degrades gracefully", () => {
107+
const logger = new Logger({ type: "hidden" });
108+
const err = new Error("weird");
109+
err.stack = "CUSTOM_FORMAT: weird\n-- custom frame info --";
110+
111+
expect(() => {
112+
const logObj = logger.info(err);
113+
expect(logObj).toBeDefined();
114+
}).not.toThrow();
115+
});
116+
117+
test("TypeError and RangeError preserve correct name", () => {
118+
const logger = new Logger({ type: "hidden" });
119+
120+
const typeErr = logger.info(new TypeError("bad type"));
121+
expect(typeErr?.name).toBe("TypeError");
122+
123+
const rangeErr = logger.info(new RangeError("out of range"));
124+
expect(rangeErr?.name).toBe("RangeError");
125+
});
126+
});

0 commit comments

Comments
 (0)