@logtape/testing 2.2.0-dev.720 → 2.2.0-dev.724

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,14 +54,14 @@ try {
54
54
  });
55
55
 
56
56
  getLogger(["my-lib"]).info("User {userId} logged in.", {
57
- userId: "u-123",
57
+ userId: 123,
58
58
  });
59
59
 
60
60
  recorder.assertLogged({
61
61
  category: ["my-lib"],
62
62
  level: "info",
63
- message: "User u-123 logged in.",
64
- properties: { userId: "u-123" },
63
+ message: "User 123 logged in.",
64
+ properties: { userId: 123 },
65
65
  });
66
66
  } finally {
67
67
  await reset();
@@ -70,6 +70,8 @@ try {
70
70
 
71
71
  The recorder also provides `records`, `clear()`, `take()`, `find()`,
72
72
  `filter()`, and `assertNotLogged()` for tests that need lower-level access.
73
+ Rendered message matching uses the same value rendering as LogTape's default
74
+ text formatter.
73
75
 
74
76
 
75
77
  Docs
@@ -0,0 +1,30 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+
25
+ Object.defineProperty(exports, '__toESM', {
26
+ enumerable: true,
27
+ get: function () {
28
+ return __toESM;
29
+ }
30
+ });
package/dist/mod.cjs CHANGED
@@ -1,5 +1,12 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
1
3
 
2
4
  //#region src/mod.ts
5
+ const messageFormatter = (0, __logtape_logtape.getTextFormatter)({
6
+ format: ({ message }) => message,
7
+ lineEnding: "lf",
8
+ timestamp: "none"
9
+ });
3
10
  /**
4
11
  * Creates a LogTape test recorder.
5
12
  *
@@ -19,14 +26,14 @@
19
26
  * });
20
27
  *
21
28
  * getLogger(["my-lib"]).info("User {userId} logged in.", {
22
- * userId: "u-123",
29
+ * userId: 123,
23
30
  * });
24
31
  *
25
32
  * recorder.assertLogged({
26
33
  * category: ["my-lib"],
27
34
  * level: "info",
28
- * message: "User u-123 logged in.",
29
- * properties: { userId: "u-123" },
35
+ * message: "User 123 logged in.",
36
+ * properties: { userId: 123 },
30
37
  * });
31
38
  * } finally {
32
39
  * await reset();
@@ -85,7 +92,7 @@ function matchesLogRecord(record, match) {
85
92
  if (match.category != null && !matchesCategory(record.category, match.category)) return false;
86
93
  if (match.categoryPrefix != null && !matchesCategoryPrefix(record.category, match.categoryPrefix)) return false;
87
94
  if (match.level != null && record.level !== match.level) return false;
88
- if (match.message != null && !matchesMessage(renderMessage(record.message), record, match.message)) return false;
95
+ if (match.message != null && !matchesMessage(renderMessage(record), record, match.message)) return false;
89
96
  if (match.rawMessage != null && !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)) return false;
90
97
  if (match.properties != null && !matchesProperties(record.properties, record, match.properties)) return false;
91
98
  if (match.predicate != null && !match.predicate(record)) return false;
@@ -130,28 +137,8 @@ function testRegExp(pattern, text) {
130
137
  function renderRawMessage(rawMessage) {
131
138
  return typeof rawMessage === "string" ? rawMessage : [...rawMessage].join("");
132
139
  }
133
- function renderMessage(message) {
134
- let rendered = "";
135
- for (const part of message) rendered += renderMessagePart(part);
136
- return rendered;
137
- }
138
- function renderMessagePart(part) {
139
- if (typeof part === "string") return part;
140
- if (typeof part === "bigint") return `${part}n`;
141
- if (part instanceof Error) return `${part.name}: ${part.message}`;
142
- if (part instanceof RegExp) return String(part);
143
- if (part instanceof Date) try {
144
- return part.toISOString();
145
- } catch {
146
- return String(part);
147
- }
148
- if (part == null) return String(part);
149
- if (typeof part === "object") try {
150
- return JSON.stringify(part) ?? String(part);
151
- } catch {
152
- return String(part);
153
- }
154
- return String(part);
140
+ function renderMessage(record) {
141
+ return messageFormatter(record).slice(0, -1);
155
142
  }
156
143
  function formatMatcher(match) {
157
144
  const lines = [];
@@ -189,7 +176,7 @@ function formatRecords(records) {
189
176
  }
190
177
  function formatRecord(record) {
191
178
  const category = formatCategory(record.category);
192
- return ` [${record.level}] ${category}: ${renderMessage(record.message)}${formatProperties(record.properties)}`;
179
+ return ` [${record.level}] ${category}: ${renderMessage(record)}${formatProperties(record.properties)}`;
193
180
  }
194
181
  function formatCategory(category) {
195
182
  return category.length < 1 ? "<root>" : category.join(".");
@@ -212,9 +199,16 @@ function formatValue(value) {
212
199
  if (value instanceof RegExp) return String(value);
213
200
  if (value instanceof Error) return `${value.name}: ${value.message}`;
214
201
  try {
215
- return JSON.stringify(value) ?? String(value);
202
+ return JSON.stringify(value) ?? safeString(value);
216
203
  } catch {
204
+ return safeString(value);
205
+ }
206
+ }
207
+ function safeString(value) {
208
+ try {
217
209
  return String(value);
210
+ } catch {
211
+ return Object.prototype.toString.call(value);
218
212
  }
219
213
  }
220
214
 
package/dist/mod.d.cts CHANGED
@@ -40,7 +40,8 @@ interface LogRecordMatch {
40
40
  readonly level?: LogLevel;
41
41
  /**
42
42
  * Rendered message matcher. String and regular expression matchers are
43
- * applied to the rendered message. A predicate receives the full record.
43
+ * applied to the rendered message, using the same value rendering as
44
+ * LogTape's default text formatter. A predicate receives the full record.
44
45
  */
45
46
  readonly message?: string | RegExp | ((record: LogRecord) => boolean);
46
47
  /**
@@ -127,14 +128,14 @@ interface LogRecorder {
127
128
  * });
128
129
  *
129
130
  * getLogger(["my-lib"]).info("User {userId} logged in.", {
130
- * userId: "u-123",
131
+ * userId: 123,
131
132
  * });
132
133
  *
133
134
  * recorder.assertLogged({
134
135
  * category: ["my-lib"],
135
136
  * level: "info",
136
- * message: "User u-123 logged in.",
137
- * properties: { userId: "u-123" },
137
+ * message: "User 123 logged in.",
138
+ * properties: { userId: 123 },
138
139
  * });
139
140
  * } finally {
140
141
  * await reset();
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.cts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;;;;;AAEmB;AAanB;AAA+B,KAfnB,eAAA,GAemB,CAAA,UAAA,EAdjB,QAciB,CAdR,MAcQ,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA,EAbrB,SAaqB,EAAA,GAAA,OAAA;;;;;;;;;;AAuCU;AAQxB,UA/CA,cAAA,CA+CW;EAAA;;;;;EA2BA,SAAG,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA,GApEoB,MAoEpB;EAAS;;;;EAwBD,SAAA,cAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA;EAuCvB;;;mBAxHG;;;;;8BAMW,mBAAmB;;;;;iCAMhB;;;;wBAKT,SAAS,2BAA2B;;;;gCAK5B;;;;;;;UAQf,WAAA;;;;iBAIA;;;;6BAKY;;;;;;;;mBAUV;;;;;;;cAQL,iBAAiB;;;;;;;gBAQf,0BAA0B;;;;;;;sBAQpB;;;;;;;yBAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCT,iBAAA,CAAA,GAAqB"}
1
+ {"version":3,"file":"mod.d.cts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;AAsBA;;;;;AAEmB;AAanB;AAA+B,KAfnB,eAAA,GAemB,CAAA,UAAA,EAdjB,QAciB,CAdR,MAcQ,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA,EAbrB,SAaqB,EAAA,GAAA,OAAA;;;;;;;;;;AAwCU;AAQxB,UAhDA,cAAA,CAgDW;EAAA;;;;;EA2BA,SAAG,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA,GArEoB,MAqEpB;EAAS;;;;EAwBD,SAAA,cAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA;EAuCvB;;;mBAzHG;;;;;;8BAOW,mBAAmB;;;;;iCAMhB;;;;wBAKT,SAAS,2BAA2B;;;;gCAK5B;;;;;;;UAQf,WAAA;;;;iBAIA;;;;6BAKY;;;;;;;;mBAUV;;;;;;;cAQL,iBAAiB;;;;;;;gBAQf,0BAA0B;;;;;;;sBAQpB;;;;;;;yBAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCT,iBAAA,CAAA,GAAqB"}
package/dist/mod.d.ts CHANGED
@@ -40,7 +40,8 @@ interface LogRecordMatch {
40
40
  readonly level?: LogLevel;
41
41
  /**
42
42
  * Rendered message matcher. String and regular expression matchers are
43
- * applied to the rendered message. A predicate receives the full record.
43
+ * applied to the rendered message, using the same value rendering as
44
+ * LogTape's default text formatter. A predicate receives the full record.
44
45
  */
45
46
  readonly message?: string | RegExp | ((record: LogRecord) => boolean);
46
47
  /**
@@ -127,14 +128,14 @@ interface LogRecorder {
127
128
  * });
128
129
  *
129
130
  * getLogger(["my-lib"]).info("User {userId} logged in.", {
130
- * userId: "u-123",
131
+ * userId: 123,
131
132
  * });
132
133
  *
133
134
  * recorder.assertLogged({
134
135
  * category: ["my-lib"],
135
136
  * level: "info",
136
- * message: "User u-123 logged in.",
137
- * properties: { userId: "u-123" },
137
+ * message: "User 123 logged in.",
138
+ * properties: { userId: 123 },
138
139
  * });
139
140
  * } finally {
140
141
  * await reset();
package/dist/mod.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;;;;;AAEmB;AAanB;AAA+B,KAfnB,eAAA,GAemB,CAAA,UAAA,EAdjB,QAciB,CAdR,MAcQ,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA,EAbrB,SAaqB,EAAA,GAAA,OAAA;;;;;;;;;;AAuCU;AAQxB,UA/CA,cAAA,CA+CW;EAAA;;;;;EA2BA,SAAG,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA,GApEoB,MAoEpB;EAAS;;;;EAwBD,SAAA,cAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA;EAuCvB;;;mBAxHG;;;;;8BAMW,mBAAmB;;;;;iCAMhB;;;;wBAKT,SAAS,2BAA2B;;;;gCAK5B;;;;;;;UAQf,WAAA;;;;iBAIA;;;;6BAKY;;;;;;;;mBAUV;;;;;;;cAQL,iBAAiB;;;;;;;gBAQf,0BAA0B;;;;;;;sBAQpB;;;;;;;yBAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCT,iBAAA,CAAA,GAAqB"}
1
+ {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;AAsBA;;;;;AAEmB;AAanB;AAA+B,KAfnB,eAAA,GAemB,CAAA,UAAA,EAdjB,QAciB,CAdR,MAcQ,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA,EAbrB,SAaqB,EAAA,GAAA,OAAA;;;;;;;;;;AAwCU;AAQxB,UAhDA,cAAA,CAgDW;EAAA;;;;;EA2BA,SAAG,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA,GArEoB,MAqEpB;EAAS;;;;EAwBD,SAAA,cAAA,CAAA,EAAA,MAAA,GAAA,SAAA,MAAA,EAAA;EAuCvB;;;mBAzHG;;;;;;8BAOW,mBAAmB;;;;;iCAMhB;;;;wBAKT,SAAS,2BAA2B;;;;gCAK5B;;;;;;;UAQf,WAAA;;;;iBAIA;;;;6BAKY;;;;;;;;mBAUV;;;;;;;cAQL,iBAAiB;;;;;;;gBAQf,0BAA0B;;;;;;;sBAQpB;;;;;;;yBAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCT,iBAAA,CAAA,GAAqB"}
package/dist/mod.js CHANGED
@@ -1,4 +1,11 @@
1
+ import { getTextFormatter } from "@logtape/logtape";
2
+
1
3
  //#region src/mod.ts
4
+ const messageFormatter = getTextFormatter({
5
+ format: ({ message }) => message,
6
+ lineEnding: "lf",
7
+ timestamp: "none"
8
+ });
2
9
  /**
3
10
  * Creates a LogTape test recorder.
4
11
  *
@@ -18,14 +25,14 @@
18
25
  * });
19
26
  *
20
27
  * getLogger(["my-lib"]).info("User {userId} logged in.", {
21
- * userId: "u-123",
28
+ * userId: 123,
22
29
  * });
23
30
  *
24
31
  * recorder.assertLogged({
25
32
  * category: ["my-lib"],
26
33
  * level: "info",
27
- * message: "User u-123 logged in.",
28
- * properties: { userId: "u-123" },
34
+ * message: "User 123 logged in.",
35
+ * properties: { userId: 123 },
29
36
  * });
30
37
  * } finally {
31
38
  * await reset();
@@ -84,7 +91,7 @@ function matchesLogRecord(record, match) {
84
91
  if (match.category != null && !matchesCategory(record.category, match.category)) return false;
85
92
  if (match.categoryPrefix != null && !matchesCategoryPrefix(record.category, match.categoryPrefix)) return false;
86
93
  if (match.level != null && record.level !== match.level) return false;
87
- if (match.message != null && !matchesMessage(renderMessage(record.message), record, match.message)) return false;
94
+ if (match.message != null && !matchesMessage(renderMessage(record), record, match.message)) return false;
88
95
  if (match.rawMessage != null && !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)) return false;
89
96
  if (match.properties != null && !matchesProperties(record.properties, record, match.properties)) return false;
90
97
  if (match.predicate != null && !match.predicate(record)) return false;
@@ -129,28 +136,8 @@ function testRegExp(pattern, text) {
129
136
  function renderRawMessage(rawMessage) {
130
137
  return typeof rawMessage === "string" ? rawMessage : [...rawMessage].join("");
131
138
  }
132
- function renderMessage(message) {
133
- let rendered = "";
134
- for (const part of message) rendered += renderMessagePart(part);
135
- return rendered;
136
- }
137
- function renderMessagePart(part) {
138
- if (typeof part === "string") return part;
139
- if (typeof part === "bigint") return `${part}n`;
140
- if (part instanceof Error) return `${part.name}: ${part.message}`;
141
- if (part instanceof RegExp) return String(part);
142
- if (part instanceof Date) try {
143
- return part.toISOString();
144
- } catch {
145
- return String(part);
146
- }
147
- if (part == null) return String(part);
148
- if (typeof part === "object") try {
149
- return JSON.stringify(part) ?? String(part);
150
- } catch {
151
- return String(part);
152
- }
153
- return String(part);
139
+ function renderMessage(record) {
140
+ return messageFormatter(record).slice(0, -1);
154
141
  }
155
142
  function formatMatcher(match) {
156
143
  const lines = [];
@@ -188,7 +175,7 @@ function formatRecords(records) {
188
175
  }
189
176
  function formatRecord(record) {
190
177
  const category = formatCategory(record.category);
191
- return ` [${record.level}] ${category}: ${renderMessage(record.message)}${formatProperties(record.properties)}`;
178
+ return ` [${record.level}] ${category}: ${renderMessage(record)}${formatProperties(record.properties)}`;
192
179
  }
193
180
  function formatCategory(category) {
194
181
  return category.length < 1 ? "<root>" : category.join(".");
@@ -211,9 +198,16 @@ function formatValue(value) {
211
198
  if (value instanceof RegExp) return String(value);
212
199
  if (value instanceof Error) return `${value.name}: ${value.message}`;
213
200
  try {
214
- return JSON.stringify(value) ?? String(value);
201
+ return JSON.stringify(value) ?? safeString(value);
215
202
  } catch {
203
+ return safeString(value);
204
+ }
205
+ }
206
+ function safeString(value) {
207
+ try {
216
208
  return String(value);
209
+ } catch {
210
+ return Object.prototype.toString.call(value);
217
211
  }
218
212
  }
219
213
 
package/dist/mod.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.js","names":["records: LogRecord[]","sink: Sink","record: LogRecord","match: LogRecordMatch","category: readonly string[]","expected: string | readonly string[] | RegExp","prefix: string | readonly string[]","category: string | readonly string[]","renderedMessage: string","matcher: string | RegExp | ((record: LogRecord) => boolean)","text: string","matcher: string | RegExp","properties: Readonly<Record<string, unknown>> | null | undefined","matcher: Readonly<Record<string, unknown>> | PropertyMatcher","pattern: RegExp","rawMessage: string | TemplateStringsArray","message: readonly unknown[]","part: unknown","lines: string[]","category: string | readonly string[] | RegExp","records: readonly LogRecord[]","count: number","noun: string","value: unknown"],"sources":["../src/mod.ts"],"sourcesContent":["import type { LogLevel, LogRecord, Sink } from \"@logtape/logtape\";\n\n/**\n * A predicate that matches log record properties.\n *\n * The first argument is the resolved properties object. The second argument\n * is the full log record for cases where the predicate needs category, level,\n * or message context.\n *\n * @since 2.2.0\n */\nexport type PropertyMatcher = (\n properties: Readonly<Record<string, unknown>>,\n record: LogRecord,\n) => boolean;\n\n/**\n * A matcher for records collected by a {@link LogRecorder}.\n *\n * Object property matching is shallow: every own string key in the matcher\n * must exist on the record properties and have the same value according to\n * `Object.is()`. Use a {@link PropertyMatcher} when a test needs absence\n * checks or deeper matching.\n *\n * @since 2.2.0\n */\nexport interface LogRecordMatch {\n /**\n * Exact category matcher. A string is matched against the dot-joined\n * category, while an array is matched segment by segment. A regular\n * expression is tested against the dot-joined category.\n */\n readonly category?: string | readonly string[] | RegExp;\n\n /**\n * Category prefix matcher. A string is split on dots, while an array is\n * matched segment by segment.\n */\n readonly categoryPrefix?: string | readonly string[];\n\n /**\n * Exact severity level matcher.\n */\n readonly level?: LogLevel;\n\n /**\n * Rendered message matcher. String and regular expression matchers are\n * applied to the rendered message. A predicate receives the full record.\n */\n readonly message?: string | RegExp | ((record: LogRecord) => boolean);\n\n /**\n * Raw message matcher. A string record is matched directly. A tagged\n * template record is matched against the concatenated template strings.\n */\n readonly rawMessage?: string | RegExp;\n\n /**\n * Shallow property matcher or predicate.\n */\n readonly properties?: Readonly<Record<string, unknown>> | PropertyMatcher;\n\n /**\n * Full-record predicate for custom checks.\n */\n readonly predicate?: (record: LogRecord) => boolean;\n}\n\n/**\n * A test recorder for LogTape records.\n *\n * @since 2.2.0\n */\nexport interface LogRecorder {\n /**\n * A sink that appends each received record to {@link LogRecorder.records}.\n */\n readonly sink: Sink;\n\n /**\n * Records collected so far, in sink call order.\n */\n readonly records: readonly LogRecord[];\n\n /**\n * Removes all collected records.\n */\n clear(): void;\n\n /**\n * Returns collected records and clears the recorder.\n */\n take(): readonly LogRecord[];\n\n /**\n * Finds the first collected record matching the given matcher.\n *\n * @param match The matcher to apply.\n * @returns The first matching record, or `undefined`.\n */\n find(match: LogRecordMatch): LogRecord | undefined;\n\n /**\n * Finds all collected records matching the given matcher.\n *\n * @param match The matcher to apply.\n * @returns All matching records in collection order.\n */\n filter(match: LogRecordMatch): readonly LogRecord[];\n\n /**\n * Asserts that at least one collected record matches the given matcher.\n *\n * @param match The matcher to apply.\n * @throws {Error} If no matching record exists.\n */\n assertLogged(match: LogRecordMatch): void;\n\n /**\n * Asserts that no collected record matches the given matcher.\n *\n * @param match The matcher to apply.\n * @throws {Error} If a matching record exists.\n */\n assertNotLogged(match: LogRecordMatch): void;\n}\n\n/**\n * Creates a LogTape test recorder.\n *\n * @example\n * ```ts\n * import { configure, getLogger, reset } from \"@logtape/logtape\";\n * import { createLogRecorder } from \"@logtape/testing\";\n *\n * const recorder = createLogRecorder();\n *\n * try {\n * await configure({\n * sinks: { recorder: recorder.sink },\n * loggers: [\n * { category: [\"my-lib\"], lowestLevel: \"debug\", sinks: [\"recorder\"] },\n * ],\n * });\n *\n * getLogger([\"my-lib\"]).info(\"User {userId} logged in.\", {\n * userId: \"u-123\",\n * });\n *\n * recorder.assertLogged({\n * category: [\"my-lib\"],\n * level: \"info\",\n * message: \"User u-123 logged in.\",\n * properties: { userId: \"u-123\" },\n * });\n * } finally {\n * await reset();\n * }\n * ```\n *\n * @returns A recorder with a sink and assertion helpers.\n * @since 2.2.0\n */\nexport function createLogRecorder(): LogRecorder {\n const records: LogRecord[] = [];\n const sink: Sink = (record: LogRecord): void => {\n records.push(record);\n };\n\n return {\n sink,\n get records(): readonly LogRecord[] {\n return records;\n },\n clear(): void {\n records.length = 0;\n },\n take(): readonly LogRecord[] {\n return records.splice(0);\n },\n find(match: LogRecordMatch): LogRecord | undefined {\n return records.find((record) => matchesLogRecord(record, match));\n },\n filter(match: LogRecordMatch): readonly LogRecord[] {\n return records.filter((record) => matchesLogRecord(record, match));\n },\n assertLogged(match: LogRecordMatch): void {\n if (records.some((record) => matchesLogRecord(record, match))) return;\n\n throw new Error(\n [\n \"Expected a LogTape record matching:\",\n formatMatcher(match),\n \"\",\n `Recorded ${formatCount(records.length, \"record\")}:`,\n formatRecords(records),\n ].join(\"\\n\"),\n );\n },\n assertNotLogged(match: LogRecordMatch): void {\n const matching = records.filter((record) =>\n matchesLogRecord(record, match)\n );\n if (matching.length < 1) return;\n\n throw new Error(\n [\n \"Expected no LogTape record matching:\",\n formatMatcher(match),\n \"\",\n `Found ${formatCount(matching.length, \"matching record\")}:`,\n formatRecords(matching),\n ].join(\"\\n\"),\n );\n },\n };\n}\n\nfunction matchesLogRecord(record: LogRecord, match: LogRecordMatch): boolean {\n if (\n match.category != null &&\n !matchesCategory(record.category, match.category)\n ) {\n return false;\n }\n if (\n match.categoryPrefix != null &&\n !matchesCategoryPrefix(record.category, match.categoryPrefix)\n ) {\n return false;\n }\n if (match.level != null && record.level !== match.level) return false;\n if (\n match.message != null &&\n !matchesMessage(renderMessage(record.message), record, match.message)\n ) {\n return false;\n }\n if (\n match.rawMessage != null &&\n !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)\n ) {\n return false;\n }\n if (\n match.properties != null &&\n !matchesProperties(record.properties, record, match.properties)\n ) {\n return false;\n }\n if (match.predicate != null && !match.predicate(record)) return false;\n return true;\n}\n\nfunction matchesCategory(\n category: readonly string[],\n expected: string | readonly string[] | RegExp,\n): boolean {\n const joinedCategory = category.join(\".\");\n if (expected instanceof RegExp) {\n return testRegExp(expected, joinedCategory);\n }\n if (typeof expected === \"string\") {\n return joinedCategory === expected;\n }\n const expectedCategory = parseCategory(expected);\n return category.length === expectedCategory.length &&\n category.every((part, index) => part === expectedCategory[index]);\n}\n\nfunction matchesCategoryPrefix(\n category: readonly string[],\n prefix: string | readonly string[],\n): boolean {\n const expectedPrefix = parseCategory(prefix);\n return expectedPrefix.length <= category.length &&\n expectedPrefix.every((part, index) => part === category[index]);\n}\n\nfunction parseCategory(\n category: string | readonly string[],\n): readonly string[] {\n if (typeof category !== \"string\") return category;\n return category === \"\" ? [] : category.split(\".\");\n}\n\nfunction matchesMessage(\n renderedMessage: string,\n record: LogRecord,\n matcher: string | RegExp | ((record: LogRecord) => boolean),\n): boolean {\n if (typeof matcher === \"function\") return matcher(record);\n return matchesText(renderedMessage, matcher);\n}\n\nfunction matchesText(text: string, matcher: string | RegExp): boolean {\n return typeof matcher === \"string\"\n ? text === matcher\n : testRegExp(matcher, text);\n}\n\nfunction matchesProperties(\n properties: Readonly<Record<string, unknown>> | null | undefined,\n record: LogRecord,\n matcher: Readonly<Record<string, unknown>> | PropertyMatcher,\n): boolean {\n const props = properties ?? {};\n if (typeof matcher === \"function\") return matcher(props, record);\n for (const key of Object.keys(matcher)) {\n if (!Object.hasOwn(props, key)) return false;\n if (!Object.is(props[key], matcher[key])) return false;\n }\n return true;\n}\n\nfunction testRegExp(pattern: RegExp, text: string): boolean {\n if (!pattern.global && !pattern.sticky) {\n return pattern.test(text);\n }\n const clone = new RegExp(pattern.source, pattern.flags);\n return clone.test(text);\n}\n\nfunction renderRawMessage(rawMessage: string | TemplateStringsArray): string {\n return typeof rawMessage === \"string\" ? rawMessage : [...rawMessage].join(\"\");\n}\n\nfunction renderMessage(message: readonly unknown[]): string {\n let rendered = \"\";\n for (const part of message) {\n rendered += renderMessagePart(part);\n }\n return rendered;\n}\n\nfunction renderMessagePart(part: unknown): string {\n if (typeof part === \"string\") return part;\n if (typeof part === \"bigint\") return `${part}n`;\n if (part instanceof Error) return `${part.name}: ${part.message}`;\n if (part instanceof RegExp) return String(part);\n if (part instanceof Date) {\n try {\n return part.toISOString();\n } catch {\n return String(part);\n }\n }\n if (part == null) return String(part);\n if (typeof part === \"object\") {\n try {\n return JSON.stringify(part) ?? String(part);\n } catch {\n return String(part);\n }\n }\n return String(part);\n}\n\nfunction formatMatcher(match: LogRecordMatch): string {\n const lines: string[] = [];\n if (match.category != null) {\n lines.push(` category: ${formatCategoryMatcher(match.category)}`);\n }\n if (match.categoryPrefix != null) {\n lines.push(\n ` categoryPrefix: ${\n formatCategoryValue(parseCategory(match.categoryPrefix))\n }`,\n );\n }\n if (match.level != null) lines.push(` level: ${formatValue(match.level)}`);\n if (match.message != null) {\n lines.push(` message: ${formatMessageMatcher(match.message)}`);\n }\n if (match.rawMessage != null) {\n lines.push(` rawMessage: ${formatTextMatcher(match.rawMessage)}`);\n }\n if (match.properties != null) {\n lines.push(...formatPropertiesMatcher(match.properties));\n }\n if (match.predicate != null) lines.push(\" predicate: <predicate>\");\n return lines.length < 1 ? \" <any record>\" : lines.join(\"\\n\");\n}\n\nfunction formatCategoryMatcher(\n category: string | readonly string[] | RegExp,\n): string {\n return category instanceof RegExp\n ? String(category)\n : typeof category === \"string\"\n ? formatValue(category)\n : formatCategoryValue(category);\n}\n\nfunction formatCategoryValue(category: readonly string[]): string {\n return `[${category.map((part) => formatValue(part)).join(\", \")}]`;\n}\n\nfunction formatMessageMatcher(\n matcher: string | RegExp | ((record: LogRecord) => boolean),\n): string {\n return typeof matcher === \"function\" ? \"<predicate>\" : formatTextMatcher(\n matcher,\n );\n}\n\nfunction formatTextMatcher(matcher: string | RegExp): string {\n return typeof matcher === \"string\" ? formatValue(matcher) : String(matcher);\n}\n\nfunction formatPropertiesMatcher(\n matcher: Readonly<Record<string, unknown>> | PropertyMatcher,\n): string[] {\n if (typeof matcher === \"function\") return [\" properties: <predicate>\"];\n const lines = Object.keys(matcher).map((key) =>\n ` properties.${key}: ${formatValue(matcher[key])}`\n );\n return lines.length < 1 ? [\" properties: {}\"] : lines;\n}\n\nfunction formatRecords(records: readonly LogRecord[]): string {\n if (records.length < 1) return \" <none>\";\n const lines = records.slice(0, 3).map(formatRecord);\n if (records.length > 3) {\n lines.push(` ... ${records.length - 3} more`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction formatRecord(record: LogRecord): string {\n const category = formatCategory(record.category);\n return ` [${record.level}] ${category}: ${renderMessage(record.message)}${\n formatProperties(record.properties)\n }`;\n}\n\nfunction formatCategory(category: readonly string[]): string {\n return category.length < 1 ? \"<root>\" : category.join(\".\");\n}\n\nfunction formatProperties(\n properties: Readonly<Record<string, unknown>> | null | undefined,\n): string {\n const props = properties ?? {};\n const entries = Object.keys(props);\n if (entries.length < 1) return \"\";\n const summary = entries.slice(0, 3).map((key) =>\n `${key}: ${formatValue(props[key])}`\n );\n if (entries.length > 3) summary.push(`... ${entries.length - 3} more`);\n return ` {${summary.join(\", \")}}`;\n}\n\nfunction formatCount(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nfunction formatValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"bigint\") return `${value}n`;\n if (typeof value === \"symbol\") return String(value);\n if (value instanceof RegExp) return String(value);\n if (value instanceof Error) {\n return `${value.name}: ${value.message}`;\n }\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mod.js","names":["records: LogRecord[]","sink: Sink","record: LogRecord","match: LogRecordMatch","category: readonly string[]","expected: string | readonly string[] | RegExp","prefix: string | readonly string[]","category: string | readonly string[]","renderedMessage: string","matcher: string | RegExp | ((record: LogRecord) => boolean)","text: string","matcher: string | RegExp","properties: Readonly<Record<string, unknown>> | null | undefined","matcher: Readonly<Record<string, unknown>> | PropertyMatcher","pattern: RegExp","rawMessage: string | TemplateStringsArray","lines: string[]","category: string | readonly string[] | RegExp","records: readonly LogRecord[]","count: number","noun: string","value: unknown"],"sources":["../src/mod.ts"],"sourcesContent":["import {\n getTextFormatter,\n type LogLevel,\n type LogRecord,\n type Sink,\n} from \"@logtape/logtape\";\n\nconst messageFormatter = getTextFormatter({\n format: ({ message }) => message,\n lineEnding: \"lf\",\n timestamp: \"none\",\n});\n\n/**\n * A predicate that matches log record properties.\n *\n * The first argument is the resolved properties object. The second argument\n * is the full log record for cases where the predicate needs category, level,\n * or message context.\n *\n * @since 2.2.0\n */\nexport type PropertyMatcher = (\n properties: Readonly<Record<string, unknown>>,\n record: LogRecord,\n) => boolean;\n\n/**\n * A matcher for records collected by a {@link LogRecorder}.\n *\n * Object property matching is shallow: every own string key in the matcher\n * must exist on the record properties and have the same value according to\n * `Object.is()`. Use a {@link PropertyMatcher} when a test needs absence\n * checks or deeper matching.\n *\n * @since 2.2.0\n */\nexport interface LogRecordMatch {\n /**\n * Exact category matcher. A string is matched against the dot-joined\n * category, while an array is matched segment by segment. A regular\n * expression is tested against the dot-joined category.\n */\n readonly category?: string | readonly string[] | RegExp;\n\n /**\n * Category prefix matcher. A string is split on dots, while an array is\n * matched segment by segment.\n */\n readonly categoryPrefix?: string | readonly string[];\n\n /**\n * Exact severity level matcher.\n */\n readonly level?: LogLevel;\n\n /**\n * Rendered message matcher. String and regular expression matchers are\n * applied to the rendered message, using the same value rendering as\n * LogTape's default text formatter. A predicate receives the full record.\n */\n readonly message?: string | RegExp | ((record: LogRecord) => boolean);\n\n /**\n * Raw message matcher. A string record is matched directly. A tagged\n * template record is matched against the concatenated template strings.\n */\n readonly rawMessage?: string | RegExp;\n\n /**\n * Shallow property matcher or predicate.\n */\n readonly properties?: Readonly<Record<string, unknown>> | PropertyMatcher;\n\n /**\n * Full-record predicate for custom checks.\n */\n readonly predicate?: (record: LogRecord) => boolean;\n}\n\n/**\n * A test recorder for LogTape records.\n *\n * @since 2.2.0\n */\nexport interface LogRecorder {\n /**\n * A sink that appends each received record to {@link LogRecorder.records}.\n */\n readonly sink: Sink;\n\n /**\n * Records collected so far, in sink call order.\n */\n readonly records: readonly LogRecord[];\n\n /**\n * Removes all collected records.\n */\n clear(): void;\n\n /**\n * Returns collected records and clears the recorder.\n */\n take(): readonly LogRecord[];\n\n /**\n * Finds the first collected record matching the given matcher.\n *\n * @param match The matcher to apply.\n * @returns The first matching record, or `undefined`.\n */\n find(match: LogRecordMatch): LogRecord | undefined;\n\n /**\n * Finds all collected records matching the given matcher.\n *\n * @param match The matcher to apply.\n * @returns All matching records in collection order.\n */\n filter(match: LogRecordMatch): readonly LogRecord[];\n\n /**\n * Asserts that at least one collected record matches the given matcher.\n *\n * @param match The matcher to apply.\n * @throws {Error} If no matching record exists.\n */\n assertLogged(match: LogRecordMatch): void;\n\n /**\n * Asserts that no collected record matches the given matcher.\n *\n * @param match The matcher to apply.\n * @throws {Error} If a matching record exists.\n */\n assertNotLogged(match: LogRecordMatch): void;\n}\n\n/**\n * Creates a LogTape test recorder.\n *\n * @example\n * ```ts\n * import { configure, getLogger, reset } from \"@logtape/logtape\";\n * import { createLogRecorder } from \"@logtape/testing\";\n *\n * const recorder = createLogRecorder();\n *\n * try {\n * await configure({\n * sinks: { recorder: recorder.sink },\n * loggers: [\n * { category: [\"my-lib\"], lowestLevel: \"debug\", sinks: [\"recorder\"] },\n * ],\n * });\n *\n * getLogger([\"my-lib\"]).info(\"User {userId} logged in.\", {\n * userId: 123,\n * });\n *\n * recorder.assertLogged({\n * category: [\"my-lib\"],\n * level: \"info\",\n * message: \"User 123 logged in.\",\n * properties: { userId: 123 },\n * });\n * } finally {\n * await reset();\n * }\n * ```\n *\n * @returns A recorder with a sink and assertion helpers.\n * @since 2.2.0\n */\nexport function createLogRecorder(): LogRecorder {\n const records: LogRecord[] = [];\n const sink: Sink = (record: LogRecord): void => {\n records.push(record);\n };\n\n return {\n sink,\n get records(): readonly LogRecord[] {\n return records;\n },\n clear(): void {\n records.length = 0;\n },\n take(): readonly LogRecord[] {\n return records.splice(0);\n },\n find(match: LogRecordMatch): LogRecord | undefined {\n return records.find((record) => matchesLogRecord(record, match));\n },\n filter(match: LogRecordMatch): readonly LogRecord[] {\n return records.filter((record) => matchesLogRecord(record, match));\n },\n assertLogged(match: LogRecordMatch): void {\n if (records.some((record) => matchesLogRecord(record, match))) return;\n\n throw new Error(\n [\n \"Expected a LogTape record matching:\",\n formatMatcher(match),\n \"\",\n `Recorded ${formatCount(records.length, \"record\")}:`,\n formatRecords(records),\n ].join(\"\\n\"),\n );\n },\n assertNotLogged(match: LogRecordMatch): void {\n const matching = records.filter((record) =>\n matchesLogRecord(record, match)\n );\n if (matching.length < 1) return;\n\n throw new Error(\n [\n \"Expected no LogTape record matching:\",\n formatMatcher(match),\n \"\",\n `Found ${formatCount(matching.length, \"matching record\")}:`,\n formatRecords(matching),\n ].join(\"\\n\"),\n );\n },\n };\n}\n\nfunction matchesLogRecord(record: LogRecord, match: LogRecordMatch): boolean {\n if (\n match.category != null &&\n !matchesCategory(record.category, match.category)\n ) {\n return false;\n }\n if (\n match.categoryPrefix != null &&\n !matchesCategoryPrefix(record.category, match.categoryPrefix)\n ) {\n return false;\n }\n if (match.level != null && record.level !== match.level) return false;\n if (\n match.message != null &&\n !matchesMessage(renderMessage(record), record, match.message)\n ) {\n return false;\n }\n if (\n match.rawMessage != null &&\n !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)\n ) {\n return false;\n }\n if (\n match.properties != null &&\n !matchesProperties(record.properties, record, match.properties)\n ) {\n return false;\n }\n if (match.predicate != null && !match.predicate(record)) return false;\n return true;\n}\n\nfunction matchesCategory(\n category: readonly string[],\n expected: string | readonly string[] | RegExp,\n): boolean {\n const joinedCategory = category.join(\".\");\n if (expected instanceof RegExp) {\n return testRegExp(expected, joinedCategory);\n }\n if (typeof expected === \"string\") {\n return joinedCategory === expected;\n }\n const expectedCategory = parseCategory(expected);\n return category.length === expectedCategory.length &&\n category.every((part, index) => part === expectedCategory[index]);\n}\n\nfunction matchesCategoryPrefix(\n category: readonly string[],\n prefix: string | readonly string[],\n): boolean {\n const expectedPrefix = parseCategory(prefix);\n return expectedPrefix.length <= category.length &&\n expectedPrefix.every((part, index) => part === category[index]);\n}\n\nfunction parseCategory(\n category: string | readonly string[],\n): readonly string[] {\n if (typeof category !== \"string\") return category;\n return category === \"\" ? [] : category.split(\".\");\n}\n\nfunction matchesMessage(\n renderedMessage: string,\n record: LogRecord,\n matcher: string | RegExp | ((record: LogRecord) => boolean),\n): boolean {\n if (typeof matcher === \"function\") return matcher(record);\n return matchesText(renderedMessage, matcher);\n}\n\nfunction matchesText(text: string, matcher: string | RegExp): boolean {\n return typeof matcher === \"string\"\n ? text === matcher\n : testRegExp(matcher, text);\n}\n\nfunction matchesProperties(\n properties: Readonly<Record<string, unknown>> | null | undefined,\n record: LogRecord,\n matcher: Readonly<Record<string, unknown>> | PropertyMatcher,\n): boolean {\n const props = properties ?? {};\n if (typeof matcher === \"function\") return matcher(props, record);\n for (const key of Object.keys(matcher)) {\n if (!Object.hasOwn(props, key)) return false;\n if (!Object.is(props[key], matcher[key])) return false;\n }\n return true;\n}\n\nfunction testRegExp(pattern: RegExp, text: string): boolean {\n if (!pattern.global && !pattern.sticky) {\n return pattern.test(text);\n }\n const clone = new RegExp(pattern.source, pattern.flags);\n return clone.test(text);\n}\n\nfunction renderRawMessage(rawMessage: string | TemplateStringsArray): string {\n return typeof rawMessage === \"string\" ? rawMessage : [...rawMessage].join(\"\");\n}\n\nfunction renderMessage(record: LogRecord): string {\n return messageFormatter(record).slice(0, -1);\n}\n\nfunction formatMatcher(match: LogRecordMatch): string {\n const lines: string[] = [];\n if (match.category != null) {\n lines.push(` category: ${formatCategoryMatcher(match.category)}`);\n }\n if (match.categoryPrefix != null) {\n lines.push(\n ` categoryPrefix: ${\n formatCategoryValue(parseCategory(match.categoryPrefix))\n }`,\n );\n }\n if (match.level != null) lines.push(` level: ${formatValue(match.level)}`);\n if (match.message != null) {\n lines.push(` message: ${formatMessageMatcher(match.message)}`);\n }\n if (match.rawMessage != null) {\n lines.push(` rawMessage: ${formatTextMatcher(match.rawMessage)}`);\n }\n if (match.properties != null) {\n lines.push(...formatPropertiesMatcher(match.properties));\n }\n if (match.predicate != null) lines.push(\" predicate: <predicate>\");\n return lines.length < 1 ? \" <any record>\" : lines.join(\"\\n\");\n}\n\nfunction formatCategoryMatcher(\n category: string | readonly string[] | RegExp,\n): string {\n return category instanceof RegExp\n ? String(category)\n : typeof category === \"string\"\n ? formatValue(category)\n : formatCategoryValue(category);\n}\n\nfunction formatCategoryValue(category: readonly string[]): string {\n return `[${category.map((part) => formatValue(part)).join(\", \")}]`;\n}\n\nfunction formatMessageMatcher(\n matcher: string | RegExp | ((record: LogRecord) => boolean),\n): string {\n return typeof matcher === \"function\" ? \"<predicate>\" : formatTextMatcher(\n matcher,\n );\n}\n\nfunction formatTextMatcher(matcher: string | RegExp): string {\n return typeof matcher === \"string\" ? formatValue(matcher) : String(matcher);\n}\n\nfunction formatPropertiesMatcher(\n matcher: Readonly<Record<string, unknown>> | PropertyMatcher,\n): string[] {\n if (typeof matcher === \"function\") return [\" properties: <predicate>\"];\n const lines = Object.keys(matcher).map((key) =>\n ` properties.${key}: ${formatValue(matcher[key])}`\n );\n return lines.length < 1 ? [\" properties: {}\"] : lines;\n}\n\nfunction formatRecords(records: readonly LogRecord[]): string {\n if (records.length < 1) return \" <none>\";\n const lines = records.slice(0, 3).map(formatRecord);\n if (records.length > 3) {\n lines.push(` ... ${records.length - 3} more`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction formatRecord(record: LogRecord): string {\n const category = formatCategory(record.category);\n return ` [${record.level}] ${category}: ${renderMessage(record)}${\n formatProperties(record.properties)\n }`;\n}\n\nfunction formatCategory(category: readonly string[]): string {\n return category.length < 1 ? \"<root>\" : category.join(\".\");\n}\n\nfunction formatProperties(\n properties: Readonly<Record<string, unknown>> | null | undefined,\n): string {\n const props = properties ?? {};\n const entries = Object.keys(props);\n if (entries.length < 1) return \"\";\n const summary = entries.slice(0, 3).map((key) =>\n `${key}: ${formatValue(props[key])}`\n );\n if (entries.length > 3) summary.push(`... ${entries.length - 3} more`);\n return ` {${summary.join(\", \")}}`;\n}\n\nfunction formatCount(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nfunction formatValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"bigint\") return `${value}n`;\n if (typeof value === \"symbol\") return String(value);\n if (value instanceof RegExp) return String(value);\n if (value instanceof Error) {\n return `${value.name}: ${value.message}`;\n }\n try {\n return JSON.stringify(value) ?? safeString(value);\n } catch {\n return safeString(value);\n }\n}\n\nfunction safeString(value: unknown): string {\n try {\n return String(value);\n } catch {\n return Object.prototype.toString.call(value);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/testing",
3
- "version": "2.2.0-dev.720+7e379ca4",
3
+ "version": "2.2.0-dev.724+c4c75217",
4
4
  "description": "Testing utilities for collecting and asserting LogTape records",
5
5
  "keywords": [
6
6
  "logging",
@@ -48,12 +48,12 @@
48
48
  "dist/"
49
49
  ],
50
50
  "peerDependencies": {
51
- "@logtape/logtape": "^2.2.0-dev.720+7e379ca4"
51
+ "@logtape/logtape": "^2.2.0-dev.724+c4c75217"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsdown": "^0.12.7",
55
55
  "typescript": "^5.8.3",
56
- "@logtape/redaction": "^2.2.0-dev.720+7e379ca4"
56
+ "@logtape/redaction": "^2.2.0-dev.724+c4c75217"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsdown",