@logtape/testing 2.2.0-dev.0
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/LICENSE +20 -0
- package/README.md +80 -0
- package/dist/mod.cjs +214 -0
- package/dist/mod.d.cts +151 -0
- package/dist/mod.d.cts.map +1 -0
- package/dist/mod.d.ts +151 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +214 -0
- package/dist/mod.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2024–2026 Hong Minhee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!-- deno-fmt-ignore-file -->
|
|
2
|
+
|
|
3
|
+
Testing utilities for LogTape
|
|
4
|
+
=============================
|
|
5
|
+
|
|
6
|
+
[![JSR][JSR badge]][JSR]
|
|
7
|
+
[![npm][npm badge]][npm]
|
|
8
|
+
|
|
9
|
+
This package provides testing utilities for [LogTape]. It includes a log
|
|
10
|
+
recorder that collects `LogRecord` values in memory and provides matcher-based
|
|
11
|
+
assertions for category, level, rendered message, raw message, and structured
|
|
12
|
+
properties.
|
|
13
|
+
|
|
14
|
+
[JSR badge]: https://jsr.io/badges/@logtape/testing
|
|
15
|
+
[JSR]: https://jsr.io/@logtape/testing
|
|
16
|
+
[npm badge]: https://img.shields.io/npm/v/@logtape/testing?logo=npm
|
|
17
|
+
[npm]: https://www.npmjs.com/package/@logtape/testing
|
|
18
|
+
[LogTape]: https://logtape.org/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Installation
|
|
22
|
+
------------
|
|
23
|
+
|
|
24
|
+
This package is available on [JSR] and [npm]. You can install it for various
|
|
25
|
+
JavaScript runtimes and package managers:
|
|
26
|
+
|
|
27
|
+
~~~~ sh
|
|
28
|
+
deno add jsr:@logtape/testing # for Deno
|
|
29
|
+
npm add @logtape/testing # for npm
|
|
30
|
+
pnpm add @logtape/testing # for pnpm
|
|
31
|
+
yarn add @logtape/testing # for Yarn
|
|
32
|
+
bun add @logtape/testing # for Bun
|
|
33
|
+
~~~~
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Usage
|
|
37
|
+
-----
|
|
38
|
+
|
|
39
|
+
Use `createLogRecorder()` as a sink in tests:
|
|
40
|
+
|
|
41
|
+
~~~~ typescript
|
|
42
|
+
import { configure, getLogger, reset } from "@logtape/logtape";
|
|
43
|
+
import { createLogRecorder } from "@logtape/testing";
|
|
44
|
+
|
|
45
|
+
const recorder = createLogRecorder();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await configure({
|
|
49
|
+
sinks: { recorder: recorder.sink },
|
|
50
|
+
loggers: [
|
|
51
|
+
{ category: ["my-lib"], lowestLevel: "debug", sinks: ["recorder"] },
|
|
52
|
+
{ category: ["logtape", "meta"], sinks: [] },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
getLogger(["my-lib"]).info("User {userId} logged in.", {
|
|
57
|
+
userId: "u-123",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
recorder.assertLogged({
|
|
61
|
+
category: ["my-lib"],
|
|
62
|
+
level: "info",
|
|
63
|
+
message: "User u-123 logged in.",
|
|
64
|
+
properties: { userId: "u-123" },
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
await reset();
|
|
68
|
+
}
|
|
69
|
+
~~~~
|
|
70
|
+
|
|
71
|
+
The recorder also provides `records`, `clear()`, `take()`, `find()`,
|
|
72
|
+
`filter()`, and `assertNotLogged()` for tests that need lower-level access.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Docs
|
|
76
|
+
----
|
|
77
|
+
|
|
78
|
+
The docs of this package is available at
|
|
79
|
+
<https://logtape.org/manual/testing>. For the API references, see
|
|
80
|
+
<https://jsr.io/@logtape/testing>.
|
package/dist/mod.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/mod.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a LogTape test recorder.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { configure, getLogger, reset } from "@logtape/logtape";
|
|
9
|
+
* import { createLogRecorder } from "@logtape/testing";
|
|
10
|
+
*
|
|
11
|
+
* const recorder = createLogRecorder();
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* await configure({
|
|
15
|
+
* sinks: { recorder: recorder.sink },
|
|
16
|
+
* loggers: [
|
|
17
|
+
* { category: ["my-lib"], lowestLevel: "debug", sinks: ["recorder"] },
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* getLogger(["my-lib"]).info("User {userId} logged in.", {
|
|
22
|
+
* userId: "u-123",
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* recorder.assertLogged({
|
|
26
|
+
* category: ["my-lib"],
|
|
27
|
+
* level: "info",
|
|
28
|
+
* message: "User u-123 logged in.",
|
|
29
|
+
* properties: { userId: "u-123" },
|
|
30
|
+
* });
|
|
31
|
+
* } finally {
|
|
32
|
+
* await reset();
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @returns A recorder with a sink and assertion helpers.
|
|
37
|
+
* @since 2.2.0
|
|
38
|
+
*/
|
|
39
|
+
function createLogRecorder() {
|
|
40
|
+
const records = [];
|
|
41
|
+
const sink = (record) => {
|
|
42
|
+
records.push(record);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
sink,
|
|
46
|
+
get records() {
|
|
47
|
+
return records;
|
|
48
|
+
},
|
|
49
|
+
clear() {
|
|
50
|
+
records.length = 0;
|
|
51
|
+
},
|
|
52
|
+
take() {
|
|
53
|
+
return records.splice(0);
|
|
54
|
+
},
|
|
55
|
+
find(match) {
|
|
56
|
+
return records.find((record) => matchesLogRecord(record, match));
|
|
57
|
+
},
|
|
58
|
+
filter(match) {
|
|
59
|
+
return records.filter((record) => matchesLogRecord(record, match));
|
|
60
|
+
},
|
|
61
|
+
assertLogged(match) {
|
|
62
|
+
if (records.some((record) => matchesLogRecord(record, match))) return;
|
|
63
|
+
throw new Error([
|
|
64
|
+
"Expected a LogTape record matching:",
|
|
65
|
+
formatMatcher(match),
|
|
66
|
+
"",
|
|
67
|
+
`Recorded ${formatCount(records.length, "record")}:`,
|
|
68
|
+
formatRecords(records)
|
|
69
|
+
].join("\n"));
|
|
70
|
+
},
|
|
71
|
+
assertNotLogged(match) {
|
|
72
|
+
const matching = records.filter((record) => matchesLogRecord(record, match));
|
|
73
|
+
if (matching.length < 1) return;
|
|
74
|
+
throw new Error([
|
|
75
|
+
"Expected no LogTape record matching:",
|
|
76
|
+
formatMatcher(match),
|
|
77
|
+
"",
|
|
78
|
+
`Found ${formatCount(matching.length, "matching record")}:`,
|
|
79
|
+
formatRecords(matching)
|
|
80
|
+
].join("\n"));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function matchesLogRecord(record, match) {
|
|
85
|
+
if (match.category != null && !matchesCategory(record.category, match.category)) return false;
|
|
86
|
+
if (match.categoryPrefix != null && !matchesCategoryPrefix(record.category, match.categoryPrefix)) return false;
|
|
87
|
+
if (match.level != null && record.level !== match.level) return false;
|
|
88
|
+
if (match.message != null && !matchesMessage(renderMessage(record.message), record, match.message)) return false;
|
|
89
|
+
if (match.rawMessage != null && !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)) return false;
|
|
90
|
+
if (match.properties != null && !matchesProperties(record.properties, record, match.properties)) return false;
|
|
91
|
+
if (match.predicate != null && !match.predicate(record)) return false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function matchesCategory(category, expected) {
|
|
95
|
+
const joinedCategory = category.join(".");
|
|
96
|
+
if (expected instanceof RegExp) return testRegExp(expected, joinedCategory);
|
|
97
|
+
if (typeof expected === "string") return joinedCategory === expected;
|
|
98
|
+
const expectedCategory = parseCategory(expected);
|
|
99
|
+
return category.length === expectedCategory.length && category.every((part, index) => part === expectedCategory[index]);
|
|
100
|
+
}
|
|
101
|
+
function matchesCategoryPrefix(category, prefix) {
|
|
102
|
+
const expectedPrefix = parseCategory(prefix);
|
|
103
|
+
return expectedPrefix.length <= category.length && expectedPrefix.every((part, index) => part === category[index]);
|
|
104
|
+
}
|
|
105
|
+
function parseCategory(category) {
|
|
106
|
+
return typeof category === "string" ? category.split(".") : category;
|
|
107
|
+
}
|
|
108
|
+
function matchesMessage(renderedMessage, record, matcher) {
|
|
109
|
+
if (typeof matcher === "function") return matcher(record);
|
|
110
|
+
return matchesText(renderedMessage, matcher);
|
|
111
|
+
}
|
|
112
|
+
function matchesText(text, matcher) {
|
|
113
|
+
return typeof matcher === "string" ? text === matcher : testRegExp(matcher, text);
|
|
114
|
+
}
|
|
115
|
+
function matchesProperties(properties, record, matcher) {
|
|
116
|
+
if (typeof matcher === "function") return matcher(properties, record);
|
|
117
|
+
for (const key of Object.keys(matcher)) {
|
|
118
|
+
if (!Object.hasOwn(properties, key)) return false;
|
|
119
|
+
if (!Object.is(properties[key], matcher[key])) return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
function testRegExp(pattern, text) {
|
|
124
|
+
const flags = pattern.flags;
|
|
125
|
+
const clone = new RegExp(pattern.source, flags);
|
|
126
|
+
clone.lastIndex = pattern.lastIndex;
|
|
127
|
+
return clone.test(text);
|
|
128
|
+
}
|
|
129
|
+
function renderRawMessage(rawMessage) {
|
|
130
|
+
return typeof rawMessage === "string" ? rawMessage : [...rawMessage].join("");
|
|
131
|
+
}
|
|
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.message;
|
|
141
|
+
if (part == null) return String(part);
|
|
142
|
+
if (typeof part === "object") try {
|
|
143
|
+
return JSON.stringify(part) ?? String(part);
|
|
144
|
+
} catch {
|
|
145
|
+
return String(part);
|
|
146
|
+
}
|
|
147
|
+
return String(part);
|
|
148
|
+
}
|
|
149
|
+
function formatMatcher(match) {
|
|
150
|
+
const lines = [];
|
|
151
|
+
if (match.category != null) lines.push(` category: ${formatCategoryMatcher(match.category)}`);
|
|
152
|
+
if (match.categoryPrefix != null) lines.push(` categoryPrefix: ${formatCategoryValue(parseCategory(match.categoryPrefix))}`);
|
|
153
|
+
if (match.level != null) lines.push(` level: ${formatValue(match.level)}`);
|
|
154
|
+
if (match.message != null) lines.push(` message: ${formatMessageMatcher(match.message)}`);
|
|
155
|
+
if (match.rawMessage != null) lines.push(` rawMessage: ${formatTextMatcher(match.rawMessage)}`);
|
|
156
|
+
if (match.properties != null) lines.push(...formatPropertiesMatcher(match.properties));
|
|
157
|
+
if (match.predicate != null) lines.push(" predicate: <predicate>");
|
|
158
|
+
return lines.length < 1 ? " <any record>" : lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
function formatCategoryMatcher(category) {
|
|
161
|
+
return category instanceof RegExp ? String(category) : typeof category === "string" ? formatValue(category) : formatCategoryValue(category);
|
|
162
|
+
}
|
|
163
|
+
function formatCategoryValue(category) {
|
|
164
|
+
return `[${category.map((part) => formatValue(part)).join(", ")}]`;
|
|
165
|
+
}
|
|
166
|
+
function formatMessageMatcher(matcher) {
|
|
167
|
+
return typeof matcher === "function" ? "<predicate>" : formatTextMatcher(matcher);
|
|
168
|
+
}
|
|
169
|
+
function formatTextMatcher(matcher) {
|
|
170
|
+
return typeof matcher === "string" ? formatValue(matcher) : String(matcher);
|
|
171
|
+
}
|
|
172
|
+
function formatPropertiesMatcher(matcher) {
|
|
173
|
+
if (typeof matcher === "function") return [" properties: <predicate>"];
|
|
174
|
+
const lines = Object.keys(matcher).map((key) => ` properties.${key}: ${formatValue(matcher[key])}`);
|
|
175
|
+
return lines.length < 1 ? [" properties: {}"] : lines;
|
|
176
|
+
}
|
|
177
|
+
function formatRecords(records) {
|
|
178
|
+
if (records.length < 1) return " <none>";
|
|
179
|
+
const lines = records.slice(0, 3).map(formatRecord);
|
|
180
|
+
if (records.length > 3) lines.push(` ... ${records.length - 3} more`);
|
|
181
|
+
return lines.join("\n");
|
|
182
|
+
}
|
|
183
|
+
function formatRecord(record) {
|
|
184
|
+
const category = formatCategory(record.category);
|
|
185
|
+
return ` [${record.level}] ${category}: ${renderMessage(record.message)}${formatProperties(record.properties)}`;
|
|
186
|
+
}
|
|
187
|
+
function formatCategory(category) {
|
|
188
|
+
return category.length < 1 ? "<root>" : category.join(".");
|
|
189
|
+
}
|
|
190
|
+
function formatProperties(properties) {
|
|
191
|
+
const entries = Object.keys(properties);
|
|
192
|
+
if (entries.length < 1) return "";
|
|
193
|
+
const summary = entries.slice(0, 3).map((key) => `${key}: ${formatValue(properties[key])}`);
|
|
194
|
+
if (entries.length > 3) summary.push(`... ${entries.length - 3} more`);
|
|
195
|
+
return ` {${summary.join(", ")}}`;
|
|
196
|
+
}
|
|
197
|
+
function formatCount(count, noun) {
|
|
198
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
199
|
+
}
|
|
200
|
+
function formatValue(value) {
|
|
201
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
202
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
203
|
+
if (typeof value === "symbol") return String(value);
|
|
204
|
+
if (value instanceof RegExp) return String(value);
|
|
205
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
206
|
+
try {
|
|
207
|
+
return JSON.stringify(value) ?? String(value);
|
|
208
|
+
} catch {
|
|
209
|
+
return String(value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
exports.createLogRecorder = createLogRecorder;
|
package/dist/mod.d.cts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { LogLevel, LogRecord, Sink } from "@logtape/logtape";
|
|
2
|
+
|
|
3
|
+
//#region src/mod.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A predicate that matches log record properties.
|
|
7
|
+
*
|
|
8
|
+
* The first argument is the resolved properties object. The second argument
|
|
9
|
+
* is the full log record for cases where the predicate needs category, level,
|
|
10
|
+
* or message context.
|
|
11
|
+
*
|
|
12
|
+
* @since 2.2.0
|
|
13
|
+
*/
|
|
14
|
+
type PropertyMatcher = (properties: Readonly<Record<string, unknown>>, record: LogRecord) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* A matcher for records collected by a {@link LogRecorder}.
|
|
17
|
+
*
|
|
18
|
+
* Object property matching is shallow: every own string key in the matcher
|
|
19
|
+
* must exist on the record properties and have the same value according to
|
|
20
|
+
* `Object.is()`. Use a {@link PropertyMatcher} when a test needs absence
|
|
21
|
+
* checks or deeper matching.
|
|
22
|
+
*
|
|
23
|
+
* @since 2.2.0
|
|
24
|
+
*/
|
|
25
|
+
interface LogRecordMatch {
|
|
26
|
+
/**
|
|
27
|
+
* Exact category matcher. A string is matched against the dot-joined
|
|
28
|
+
* category, while an array is matched segment by segment. A regular
|
|
29
|
+
* expression is tested against the dot-joined category.
|
|
30
|
+
*/
|
|
31
|
+
readonly category?: string | readonly string[] | RegExp;
|
|
32
|
+
/**
|
|
33
|
+
* Category prefix matcher. A string is split on dots, while an array is
|
|
34
|
+
* matched segment by segment.
|
|
35
|
+
*/
|
|
36
|
+
readonly categoryPrefix?: string | readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Exact severity level matcher.
|
|
39
|
+
*/
|
|
40
|
+
readonly level?: LogLevel;
|
|
41
|
+
/**
|
|
42
|
+
* Rendered message matcher. String and regular expression matchers are
|
|
43
|
+
* applied to the rendered message. A predicate receives the full record.
|
|
44
|
+
*/
|
|
45
|
+
readonly message?: string | RegExp | ((record: LogRecord) => boolean);
|
|
46
|
+
/**
|
|
47
|
+
* Raw message matcher. A string record is matched directly. A tagged
|
|
48
|
+
* template record is matched against the concatenated template strings.
|
|
49
|
+
*/
|
|
50
|
+
readonly rawMessage?: string | RegExp;
|
|
51
|
+
/**
|
|
52
|
+
* Shallow property matcher or predicate.
|
|
53
|
+
*/
|
|
54
|
+
readonly properties?: Readonly<Record<string, unknown>> | PropertyMatcher;
|
|
55
|
+
/**
|
|
56
|
+
* Full-record predicate for custom checks.
|
|
57
|
+
*/
|
|
58
|
+
readonly predicate?: (record: LogRecord) => boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A test recorder for LogTape records.
|
|
62
|
+
*
|
|
63
|
+
* @since 2.2.0
|
|
64
|
+
*/
|
|
65
|
+
interface LogRecorder {
|
|
66
|
+
/**
|
|
67
|
+
* A sink that appends each received record to {@link LogRecorder.records}.
|
|
68
|
+
*/
|
|
69
|
+
readonly sink: Sink;
|
|
70
|
+
/**
|
|
71
|
+
* Records collected so far, in sink call order.
|
|
72
|
+
*/
|
|
73
|
+
readonly records: readonly LogRecord[];
|
|
74
|
+
/**
|
|
75
|
+
* Removes all collected records.
|
|
76
|
+
*/
|
|
77
|
+
clear(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Returns collected records and clears the recorder.
|
|
80
|
+
*/
|
|
81
|
+
take(): readonly LogRecord[];
|
|
82
|
+
/**
|
|
83
|
+
* Finds the first collected record matching the given matcher.
|
|
84
|
+
*
|
|
85
|
+
* @param match The matcher to apply.
|
|
86
|
+
* @returns The first matching record, or `undefined`.
|
|
87
|
+
*/
|
|
88
|
+
find(match: LogRecordMatch): LogRecord | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Finds all collected records matching the given matcher.
|
|
91
|
+
*
|
|
92
|
+
* @param match The matcher to apply.
|
|
93
|
+
* @returns All matching records in collection order.
|
|
94
|
+
*/
|
|
95
|
+
filter(match: LogRecordMatch): readonly LogRecord[];
|
|
96
|
+
/**
|
|
97
|
+
* Asserts that at least one collected record matches the given matcher.
|
|
98
|
+
*
|
|
99
|
+
* @param match The matcher to apply.
|
|
100
|
+
* @throws {Error} If no matching record exists.
|
|
101
|
+
*/
|
|
102
|
+
assertLogged(match: LogRecordMatch): void;
|
|
103
|
+
/**
|
|
104
|
+
* Asserts that no collected record matches the given matcher.
|
|
105
|
+
*
|
|
106
|
+
* @param match The matcher to apply.
|
|
107
|
+
* @throws {Error} If a matching record exists.
|
|
108
|
+
*/
|
|
109
|
+
assertNotLogged(match: LogRecordMatch): void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates a LogTape test recorder.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { configure, getLogger, reset } from "@logtape/logtape";
|
|
117
|
+
* import { createLogRecorder } from "@logtape/testing";
|
|
118
|
+
*
|
|
119
|
+
* const recorder = createLogRecorder();
|
|
120
|
+
*
|
|
121
|
+
* try {
|
|
122
|
+
* await configure({
|
|
123
|
+
* sinks: { recorder: recorder.sink },
|
|
124
|
+
* loggers: [
|
|
125
|
+
* { category: ["my-lib"], lowestLevel: "debug", sinks: ["recorder"] },
|
|
126
|
+
* ],
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* getLogger(["my-lib"]).info("User {userId} logged in.", {
|
|
130
|
+
* userId: "u-123",
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* recorder.assertLogged({
|
|
134
|
+
* category: ["my-lib"],
|
|
135
|
+
* level: "info",
|
|
136
|
+
* message: "User u-123 logged in.",
|
|
137
|
+
* properties: { userId: "u-123" },
|
|
138
|
+
* });
|
|
139
|
+
* } finally {
|
|
140
|
+
* await reset();
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @returns A recorder with a sink and assertion helpers.
|
|
145
|
+
* @since 2.2.0
|
|
146
|
+
*/
|
|
147
|
+
declare function createLogRecorder(): LogRecorder;
|
|
148
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
149
|
+
//#endregion
|
|
150
|
+
export { LogRecordMatch, LogRecorder, PropertyMatcher, createLogRecorder };
|
|
151
|
+
//# sourceMappingURL=mod.d.cts.map
|
|
@@ -0,0 +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"}
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { LogLevel, LogRecord, Sink } from "@logtape/logtape";
|
|
2
|
+
|
|
3
|
+
//#region src/mod.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A predicate that matches log record properties.
|
|
7
|
+
*
|
|
8
|
+
* The first argument is the resolved properties object. The second argument
|
|
9
|
+
* is the full log record for cases where the predicate needs category, level,
|
|
10
|
+
* or message context.
|
|
11
|
+
*
|
|
12
|
+
* @since 2.2.0
|
|
13
|
+
*/
|
|
14
|
+
type PropertyMatcher = (properties: Readonly<Record<string, unknown>>, record: LogRecord) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* A matcher for records collected by a {@link LogRecorder}.
|
|
17
|
+
*
|
|
18
|
+
* Object property matching is shallow: every own string key in the matcher
|
|
19
|
+
* must exist on the record properties and have the same value according to
|
|
20
|
+
* `Object.is()`. Use a {@link PropertyMatcher} when a test needs absence
|
|
21
|
+
* checks or deeper matching.
|
|
22
|
+
*
|
|
23
|
+
* @since 2.2.0
|
|
24
|
+
*/
|
|
25
|
+
interface LogRecordMatch {
|
|
26
|
+
/**
|
|
27
|
+
* Exact category matcher. A string is matched against the dot-joined
|
|
28
|
+
* category, while an array is matched segment by segment. A regular
|
|
29
|
+
* expression is tested against the dot-joined category.
|
|
30
|
+
*/
|
|
31
|
+
readonly category?: string | readonly string[] | RegExp;
|
|
32
|
+
/**
|
|
33
|
+
* Category prefix matcher. A string is split on dots, while an array is
|
|
34
|
+
* matched segment by segment.
|
|
35
|
+
*/
|
|
36
|
+
readonly categoryPrefix?: string | readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Exact severity level matcher.
|
|
39
|
+
*/
|
|
40
|
+
readonly level?: LogLevel;
|
|
41
|
+
/**
|
|
42
|
+
* Rendered message matcher. String and regular expression matchers are
|
|
43
|
+
* applied to the rendered message. A predicate receives the full record.
|
|
44
|
+
*/
|
|
45
|
+
readonly message?: string | RegExp | ((record: LogRecord) => boolean);
|
|
46
|
+
/**
|
|
47
|
+
* Raw message matcher. A string record is matched directly. A tagged
|
|
48
|
+
* template record is matched against the concatenated template strings.
|
|
49
|
+
*/
|
|
50
|
+
readonly rawMessage?: string | RegExp;
|
|
51
|
+
/**
|
|
52
|
+
* Shallow property matcher or predicate.
|
|
53
|
+
*/
|
|
54
|
+
readonly properties?: Readonly<Record<string, unknown>> | PropertyMatcher;
|
|
55
|
+
/**
|
|
56
|
+
* Full-record predicate for custom checks.
|
|
57
|
+
*/
|
|
58
|
+
readonly predicate?: (record: LogRecord) => boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A test recorder for LogTape records.
|
|
62
|
+
*
|
|
63
|
+
* @since 2.2.0
|
|
64
|
+
*/
|
|
65
|
+
interface LogRecorder {
|
|
66
|
+
/**
|
|
67
|
+
* A sink that appends each received record to {@link LogRecorder.records}.
|
|
68
|
+
*/
|
|
69
|
+
readonly sink: Sink;
|
|
70
|
+
/**
|
|
71
|
+
* Records collected so far, in sink call order.
|
|
72
|
+
*/
|
|
73
|
+
readonly records: readonly LogRecord[];
|
|
74
|
+
/**
|
|
75
|
+
* Removes all collected records.
|
|
76
|
+
*/
|
|
77
|
+
clear(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Returns collected records and clears the recorder.
|
|
80
|
+
*/
|
|
81
|
+
take(): readonly LogRecord[];
|
|
82
|
+
/**
|
|
83
|
+
* Finds the first collected record matching the given matcher.
|
|
84
|
+
*
|
|
85
|
+
* @param match The matcher to apply.
|
|
86
|
+
* @returns The first matching record, or `undefined`.
|
|
87
|
+
*/
|
|
88
|
+
find(match: LogRecordMatch): LogRecord | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Finds all collected records matching the given matcher.
|
|
91
|
+
*
|
|
92
|
+
* @param match The matcher to apply.
|
|
93
|
+
* @returns All matching records in collection order.
|
|
94
|
+
*/
|
|
95
|
+
filter(match: LogRecordMatch): readonly LogRecord[];
|
|
96
|
+
/**
|
|
97
|
+
* Asserts that at least one collected record matches the given matcher.
|
|
98
|
+
*
|
|
99
|
+
* @param match The matcher to apply.
|
|
100
|
+
* @throws {Error} If no matching record exists.
|
|
101
|
+
*/
|
|
102
|
+
assertLogged(match: LogRecordMatch): void;
|
|
103
|
+
/**
|
|
104
|
+
* Asserts that no collected record matches the given matcher.
|
|
105
|
+
*
|
|
106
|
+
* @param match The matcher to apply.
|
|
107
|
+
* @throws {Error} If a matching record exists.
|
|
108
|
+
*/
|
|
109
|
+
assertNotLogged(match: LogRecordMatch): void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates a LogTape test recorder.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { configure, getLogger, reset } from "@logtape/logtape";
|
|
117
|
+
* import { createLogRecorder } from "@logtape/testing";
|
|
118
|
+
*
|
|
119
|
+
* const recorder = createLogRecorder();
|
|
120
|
+
*
|
|
121
|
+
* try {
|
|
122
|
+
* await configure({
|
|
123
|
+
* sinks: { recorder: recorder.sink },
|
|
124
|
+
* loggers: [
|
|
125
|
+
* { category: ["my-lib"], lowestLevel: "debug", sinks: ["recorder"] },
|
|
126
|
+
* ],
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* getLogger(["my-lib"]).info("User {userId} logged in.", {
|
|
130
|
+
* userId: "u-123",
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* recorder.assertLogged({
|
|
134
|
+
* category: ["my-lib"],
|
|
135
|
+
* level: "info",
|
|
136
|
+
* message: "User u-123 logged in.",
|
|
137
|
+
* properties: { userId: "u-123" },
|
|
138
|
+
* });
|
|
139
|
+
* } finally {
|
|
140
|
+
* await reset();
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @returns A recorder with a sink and assertion helpers.
|
|
145
|
+
* @since 2.2.0
|
|
146
|
+
*/
|
|
147
|
+
declare function createLogRecorder(): LogRecorder;
|
|
148
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
149
|
+
//#endregion
|
|
150
|
+
export { LogRecordMatch, LogRecorder, PropertyMatcher, createLogRecorder };
|
|
151
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//#region src/mod.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a LogTape test recorder.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { configure, getLogger, reset } from "@logtape/logtape";
|
|
8
|
+
* import { createLogRecorder } from "@logtape/testing";
|
|
9
|
+
*
|
|
10
|
+
* const recorder = createLogRecorder();
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* await configure({
|
|
14
|
+
* sinks: { recorder: recorder.sink },
|
|
15
|
+
* loggers: [
|
|
16
|
+
* { category: ["my-lib"], lowestLevel: "debug", sinks: ["recorder"] },
|
|
17
|
+
* ],
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* getLogger(["my-lib"]).info("User {userId} logged in.", {
|
|
21
|
+
* userId: "u-123",
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* recorder.assertLogged({
|
|
25
|
+
* category: ["my-lib"],
|
|
26
|
+
* level: "info",
|
|
27
|
+
* message: "User u-123 logged in.",
|
|
28
|
+
* properties: { userId: "u-123" },
|
|
29
|
+
* });
|
|
30
|
+
* } finally {
|
|
31
|
+
* await reset();
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @returns A recorder with a sink and assertion helpers.
|
|
36
|
+
* @since 2.2.0
|
|
37
|
+
*/
|
|
38
|
+
function createLogRecorder() {
|
|
39
|
+
const records = [];
|
|
40
|
+
const sink = (record) => {
|
|
41
|
+
records.push(record);
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
sink,
|
|
45
|
+
get records() {
|
|
46
|
+
return records;
|
|
47
|
+
},
|
|
48
|
+
clear() {
|
|
49
|
+
records.length = 0;
|
|
50
|
+
},
|
|
51
|
+
take() {
|
|
52
|
+
return records.splice(0);
|
|
53
|
+
},
|
|
54
|
+
find(match) {
|
|
55
|
+
return records.find((record) => matchesLogRecord(record, match));
|
|
56
|
+
},
|
|
57
|
+
filter(match) {
|
|
58
|
+
return records.filter((record) => matchesLogRecord(record, match));
|
|
59
|
+
},
|
|
60
|
+
assertLogged(match) {
|
|
61
|
+
if (records.some((record) => matchesLogRecord(record, match))) return;
|
|
62
|
+
throw new Error([
|
|
63
|
+
"Expected a LogTape record matching:",
|
|
64
|
+
formatMatcher(match),
|
|
65
|
+
"",
|
|
66
|
+
`Recorded ${formatCount(records.length, "record")}:`,
|
|
67
|
+
formatRecords(records)
|
|
68
|
+
].join("\n"));
|
|
69
|
+
},
|
|
70
|
+
assertNotLogged(match) {
|
|
71
|
+
const matching = records.filter((record) => matchesLogRecord(record, match));
|
|
72
|
+
if (matching.length < 1) return;
|
|
73
|
+
throw new Error([
|
|
74
|
+
"Expected no LogTape record matching:",
|
|
75
|
+
formatMatcher(match),
|
|
76
|
+
"",
|
|
77
|
+
`Found ${formatCount(matching.length, "matching record")}:`,
|
|
78
|
+
formatRecords(matching)
|
|
79
|
+
].join("\n"));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function matchesLogRecord(record, match) {
|
|
84
|
+
if (match.category != null && !matchesCategory(record.category, match.category)) return false;
|
|
85
|
+
if (match.categoryPrefix != null && !matchesCategoryPrefix(record.category, match.categoryPrefix)) return false;
|
|
86
|
+
if (match.level != null && record.level !== match.level) return false;
|
|
87
|
+
if (match.message != null && !matchesMessage(renderMessage(record.message), record, match.message)) return false;
|
|
88
|
+
if (match.rawMessage != null && !matchesText(renderRawMessage(record.rawMessage), match.rawMessage)) return false;
|
|
89
|
+
if (match.properties != null && !matchesProperties(record.properties, record, match.properties)) return false;
|
|
90
|
+
if (match.predicate != null && !match.predicate(record)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function matchesCategory(category, expected) {
|
|
94
|
+
const joinedCategory = category.join(".");
|
|
95
|
+
if (expected instanceof RegExp) return testRegExp(expected, joinedCategory);
|
|
96
|
+
if (typeof expected === "string") return joinedCategory === expected;
|
|
97
|
+
const expectedCategory = parseCategory(expected);
|
|
98
|
+
return category.length === expectedCategory.length && category.every((part, index) => part === expectedCategory[index]);
|
|
99
|
+
}
|
|
100
|
+
function matchesCategoryPrefix(category, prefix) {
|
|
101
|
+
const expectedPrefix = parseCategory(prefix);
|
|
102
|
+
return expectedPrefix.length <= category.length && expectedPrefix.every((part, index) => part === category[index]);
|
|
103
|
+
}
|
|
104
|
+
function parseCategory(category) {
|
|
105
|
+
return typeof category === "string" ? category.split(".") : category;
|
|
106
|
+
}
|
|
107
|
+
function matchesMessage(renderedMessage, record, matcher) {
|
|
108
|
+
if (typeof matcher === "function") return matcher(record);
|
|
109
|
+
return matchesText(renderedMessage, matcher);
|
|
110
|
+
}
|
|
111
|
+
function matchesText(text, matcher) {
|
|
112
|
+
return typeof matcher === "string" ? text === matcher : testRegExp(matcher, text);
|
|
113
|
+
}
|
|
114
|
+
function matchesProperties(properties, record, matcher) {
|
|
115
|
+
if (typeof matcher === "function") return matcher(properties, record);
|
|
116
|
+
for (const key of Object.keys(matcher)) {
|
|
117
|
+
if (!Object.hasOwn(properties, key)) return false;
|
|
118
|
+
if (!Object.is(properties[key], matcher[key])) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
function testRegExp(pattern, text) {
|
|
123
|
+
const flags = pattern.flags;
|
|
124
|
+
const clone = new RegExp(pattern.source, flags);
|
|
125
|
+
clone.lastIndex = pattern.lastIndex;
|
|
126
|
+
return clone.test(text);
|
|
127
|
+
}
|
|
128
|
+
function renderRawMessage(rawMessage) {
|
|
129
|
+
return typeof rawMessage === "string" ? rawMessage : [...rawMessage].join("");
|
|
130
|
+
}
|
|
131
|
+
function renderMessage(message) {
|
|
132
|
+
let rendered = "";
|
|
133
|
+
for (const part of message) rendered += renderMessagePart(part);
|
|
134
|
+
return rendered;
|
|
135
|
+
}
|
|
136
|
+
function renderMessagePart(part) {
|
|
137
|
+
if (typeof part === "string") return part;
|
|
138
|
+
if (typeof part === "bigint") return `${part}n`;
|
|
139
|
+
if (part instanceof Error) return part.message;
|
|
140
|
+
if (part == null) return String(part);
|
|
141
|
+
if (typeof part === "object") try {
|
|
142
|
+
return JSON.stringify(part) ?? String(part);
|
|
143
|
+
} catch {
|
|
144
|
+
return String(part);
|
|
145
|
+
}
|
|
146
|
+
return String(part);
|
|
147
|
+
}
|
|
148
|
+
function formatMatcher(match) {
|
|
149
|
+
const lines = [];
|
|
150
|
+
if (match.category != null) lines.push(` category: ${formatCategoryMatcher(match.category)}`);
|
|
151
|
+
if (match.categoryPrefix != null) lines.push(` categoryPrefix: ${formatCategoryValue(parseCategory(match.categoryPrefix))}`);
|
|
152
|
+
if (match.level != null) lines.push(` level: ${formatValue(match.level)}`);
|
|
153
|
+
if (match.message != null) lines.push(` message: ${formatMessageMatcher(match.message)}`);
|
|
154
|
+
if (match.rawMessage != null) lines.push(` rawMessage: ${formatTextMatcher(match.rawMessage)}`);
|
|
155
|
+
if (match.properties != null) lines.push(...formatPropertiesMatcher(match.properties));
|
|
156
|
+
if (match.predicate != null) lines.push(" predicate: <predicate>");
|
|
157
|
+
return lines.length < 1 ? " <any record>" : lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
function formatCategoryMatcher(category) {
|
|
160
|
+
return category instanceof RegExp ? String(category) : typeof category === "string" ? formatValue(category) : formatCategoryValue(category);
|
|
161
|
+
}
|
|
162
|
+
function formatCategoryValue(category) {
|
|
163
|
+
return `[${category.map((part) => formatValue(part)).join(", ")}]`;
|
|
164
|
+
}
|
|
165
|
+
function formatMessageMatcher(matcher) {
|
|
166
|
+
return typeof matcher === "function" ? "<predicate>" : formatTextMatcher(matcher);
|
|
167
|
+
}
|
|
168
|
+
function formatTextMatcher(matcher) {
|
|
169
|
+
return typeof matcher === "string" ? formatValue(matcher) : String(matcher);
|
|
170
|
+
}
|
|
171
|
+
function formatPropertiesMatcher(matcher) {
|
|
172
|
+
if (typeof matcher === "function") return [" properties: <predicate>"];
|
|
173
|
+
const lines = Object.keys(matcher).map((key) => ` properties.${key}: ${formatValue(matcher[key])}`);
|
|
174
|
+
return lines.length < 1 ? [" properties: {}"] : lines;
|
|
175
|
+
}
|
|
176
|
+
function formatRecords(records) {
|
|
177
|
+
if (records.length < 1) return " <none>";
|
|
178
|
+
const lines = records.slice(0, 3).map(formatRecord);
|
|
179
|
+
if (records.length > 3) lines.push(` ... ${records.length - 3} more`);
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
function formatRecord(record) {
|
|
183
|
+
const category = formatCategory(record.category);
|
|
184
|
+
return ` [${record.level}] ${category}: ${renderMessage(record.message)}${formatProperties(record.properties)}`;
|
|
185
|
+
}
|
|
186
|
+
function formatCategory(category) {
|
|
187
|
+
return category.length < 1 ? "<root>" : category.join(".");
|
|
188
|
+
}
|
|
189
|
+
function formatProperties(properties) {
|
|
190
|
+
const entries = Object.keys(properties);
|
|
191
|
+
if (entries.length < 1) return "";
|
|
192
|
+
const summary = entries.slice(0, 3).map((key) => `${key}: ${formatValue(properties[key])}`);
|
|
193
|
+
if (entries.length > 3) summary.push(`... ${entries.length - 3} more`);
|
|
194
|
+
return ` {${summary.join(", ")}}`;
|
|
195
|
+
}
|
|
196
|
+
function formatCount(count, noun) {
|
|
197
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
198
|
+
}
|
|
199
|
+
function formatValue(value) {
|
|
200
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
201
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
202
|
+
if (typeof value === "symbol") return String(value);
|
|
203
|
+
if (value instanceof RegExp) return String(value);
|
|
204
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.stringify(value) ?? String(value);
|
|
207
|
+
} catch {
|
|
208
|
+
return String(value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
//#endregion
|
|
213
|
+
export { createLogRecorder };
|
|
214
|
+
//# sourceMappingURL=mod.js.map
|
package/dist/mod.js.map
ADDED
|
@@ -0,0 +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>>","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 return typeof category === \"string\" ? category.split(\".\") : category;\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>>,\n record: LogRecord,\n matcher: Readonly<Record<string, unknown>> | PropertyMatcher,\n): boolean {\n if (typeof matcher === \"function\") return matcher(properties, record);\n for (const key of Object.keys(matcher)) {\n if (!Object.hasOwn(properties, key)) return false;\n if (!Object.is(properties[key], matcher[key])) return false;\n }\n return true;\n}\n\nfunction testRegExp(pattern: RegExp, text: string): boolean {\n const flags = pattern.flags;\n const clone = new RegExp(pattern.source, flags);\n clone.lastIndex = pattern.lastIndex;\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.message;\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>>,\n): string {\n const entries = Object.keys(properties);\n if (entries.length < 1) return \"\";\n const summary = entries.slice(0, 3).map((key) =>\n `${key}: ${formatValue(properties[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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmKA,SAAgB,oBAAiC;CAC/C,MAAMA,UAAuB,CAAE;CAC/B,MAAMC,OAAa,CAACC,WAA4B;AAC9C,UAAQ,KAAK,OAAO;CACrB;AAED,QAAO;EACL;EACA,IAAI,UAAgC;AAClC,UAAO;EACR;EACD,QAAc;AACZ,WAAQ,SAAS;EAClB;EACD,OAA6B;AAC3B,UAAO,QAAQ,OAAO,EAAE;EACzB;EACD,KAAKC,OAA8C;AACjD,UAAO,QAAQ,KAAK,CAAC,WAAW,iBAAiB,QAAQ,MAAM,CAAC;EACjE;EACD,OAAOA,OAA6C;AAClD,UAAO,QAAQ,OAAO,CAAC,WAAW,iBAAiB,QAAQ,MAAM,CAAC;EACnE;EACD,aAAaA,OAA6B;AACxC,OAAI,QAAQ,KAAK,CAAC,WAAW,iBAAiB,QAAQ,MAAM,CAAC,CAAE;AAE/D,SAAM,IAAI,MACR;IACE;IACA,cAAc,MAAM;IACpB;KACC,WAAW,YAAY,QAAQ,QAAQ,SAAS,CAAC;IAClD,cAAc,QAAQ;GACvB,EAAC,KAAK,KAAK;EAEf;EACD,gBAAgBA,OAA6B;GAC3C,MAAM,WAAW,QAAQ,OAAO,CAAC,WAC/B,iBAAiB,QAAQ,MAAM,CAChC;AACD,OAAI,SAAS,SAAS,EAAG;AAEzB,SAAM,IAAI,MACR;IACE;IACA,cAAc,MAAM;IACpB;KACC,QAAQ,YAAY,SAAS,QAAQ,kBAAkB,CAAC;IACzD,cAAc,SAAS;GACxB,EAAC,KAAK,KAAK;EAEf;CACF;AACF;AAED,SAAS,iBAAiBD,QAAmBC,OAAgC;AAC3E,KACE,MAAM,YAAY,SACjB,gBAAgB,OAAO,UAAU,MAAM,SAAS,CAEjD,QAAO;AAET,KACE,MAAM,kBAAkB,SACvB,sBAAsB,OAAO,UAAU,MAAM,eAAe,CAE7D,QAAO;AAET,KAAI,MAAM,SAAS,QAAQ,OAAO,UAAU,MAAM,MAAO,QAAO;AAChE,KACE,MAAM,WAAW,SAChB,eAAe,cAAc,OAAO,QAAQ,EAAE,QAAQ,MAAM,QAAQ,CAErE,QAAO;AAET,KACE,MAAM,cAAc,SACnB,YAAY,iBAAiB,OAAO,WAAW,EAAE,MAAM,WAAW,CAEnE,QAAO;AAET,KACE,MAAM,cAAc,SACnB,kBAAkB,OAAO,YAAY,QAAQ,MAAM,WAAW,CAE/D,QAAO;AAET,KAAI,MAAM,aAAa,SAAS,MAAM,UAAU,OAAO,CAAE,QAAO;AAChE,QAAO;AACR;AAED,SAAS,gBACPC,UACAC,UACS;CACT,MAAM,iBAAiB,SAAS,KAAK,IAAI;AACzC,KAAI,oBAAoB,OACtB,QAAO,WAAW,UAAU,eAAe;AAE7C,YAAW,aAAa,SACtB,QAAO,mBAAmB;CAE5B,MAAM,mBAAmB,cAAc,SAAS;AAChD,QAAO,SAAS,WAAW,iBAAiB,UAC1C,SAAS,MAAM,CAAC,MAAM,UAAU,SAAS,iBAAiB,OAAO;AACpE;AAED,SAAS,sBACPD,UACAE,QACS;CACT,MAAM,iBAAiB,cAAc,OAAO;AAC5C,QAAO,eAAe,UAAU,SAAS,UACvC,eAAe,MAAM,CAAC,MAAM,UAAU,SAAS,SAAS,OAAO;AAClE;AAED,SAAS,cACPC,UACmB;AACnB,eAAc,aAAa,WAAW,SAAS,MAAM,IAAI,GAAG;AAC7D;AAED,SAAS,eACPC,iBACAN,QACAO,SACS;AACT,YAAW,YAAY,WAAY,QAAO,QAAQ,OAAO;AACzD,QAAO,YAAY,iBAAiB,QAAQ;AAC7C;AAED,SAAS,YAAYC,MAAcC,SAAmC;AACpE,eAAc,YAAY,WACtB,SAAS,UACT,WAAW,SAAS,KAAK;AAC9B;AAED,SAAS,kBACPC,YACAV,QACAW,SACS;AACT,YAAW,YAAY,WAAY,QAAO,QAAQ,YAAY,OAAO;AACrE,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;AACtC,OAAK,OAAO,OAAO,YAAY,IAAI,CAAE,QAAO;AAC5C,OAAK,OAAO,GAAG,WAAW,MAAM,QAAQ,KAAK,CAAE,QAAO;CACvD;AACD,QAAO;AACR;AAED,SAAS,WAAWC,SAAiBJ,MAAuB;CAC1D,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,IAAI,OAAO,QAAQ,QAAQ;AACzC,OAAM,YAAY,QAAQ;AAC1B,QAAO,MAAM,KAAK,KAAK;AACxB;AAED,SAAS,iBAAiBK,YAAmD;AAC3E,eAAc,eAAe,WAAW,aAAa,CAAC,GAAG,UAAW,EAAC,KAAK,GAAG;AAC9E;AAED,SAAS,cAAcC,SAAqC;CAC1D,IAAI,WAAW;AACf,MAAK,MAAM,QAAQ,QACjB,aAAY,kBAAkB,KAAK;AAErC,QAAO;AACR;AAED,SAAS,kBAAkBC,MAAuB;AAChD,YAAW,SAAS,SAAU,QAAO;AACrC,YAAW,SAAS,SAAU,SAAQ,EAAE,KAAK;AAC7C,KAAI,gBAAgB,MAAO,QAAO,KAAK;AACvC,KAAI,QAAQ,KAAM,QAAO,OAAO,KAAK;AACrC,YAAW,SAAS,SAClB,KAAI;AACF,SAAO,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;CAC5C,QAAO;AACN,SAAO,OAAO,KAAK;CACpB;AAEH,QAAO,OAAO,KAAK;AACpB;AAED,SAAS,cAAcd,OAA+B;CACpD,MAAMe,QAAkB,CAAE;AAC1B,KAAI,MAAM,YAAY,KACpB,OAAM,MAAM,cAAc,sBAAsB,MAAM,SAAS,CAAC,EAAE;AAEpE,KAAI,MAAM,kBAAkB,KAC1B,OAAM,MACH,oBACC,oBAAoB,cAAc,MAAM,eAAe,CAAC,CACzD,EACF;AAEH,KAAI,MAAM,SAAS,KAAM,OAAM,MAAM,WAAW,YAAY,MAAM,MAAM,CAAC,EAAE;AAC3E,KAAI,MAAM,WAAW,KACnB,OAAM,MAAM,aAAa,qBAAqB,MAAM,QAAQ,CAAC,EAAE;AAEjE,KAAI,MAAM,cAAc,KACtB,OAAM,MAAM,gBAAgB,kBAAkB,MAAM,WAAW,CAAC,EAAE;AAEpE,KAAI,MAAM,cAAc,KACtB,OAAM,KAAK,GAAG,wBAAwB,MAAM,WAAW,CAAC;AAE1D,KAAI,MAAM,aAAa,KAAM,OAAM,KAAK,2BAA2B;AACnE,QAAO,MAAM,SAAS,IAAI,mBAAmB,MAAM,KAAK,KAAK;AAC9D;AAED,SAAS,sBACPC,UACQ;AACR,QAAO,oBAAoB,SACvB,OAAO,SAAS,UACT,aAAa,WACpB,YAAY,SAAS,GACrB,oBAAoB,SAAS;AAClC;AAED,SAAS,oBAAoBf,UAAqC;AAChE,SAAQ,GAAG,SAAS,IAAI,CAAC,SAAS,YAAY,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC;AACjE;AAED,SAAS,qBACPK,SACQ;AACR,eAAc,YAAY,aAAa,gBAAgB,kBACrD,QACD;AACF;AAED,SAAS,kBAAkBE,SAAkC;AAC3D,eAAc,YAAY,WAAW,YAAY,QAAQ,GAAG,OAAO,QAAQ;AAC5E;AAED,SAAS,wBACPE,SACU;AACV,YAAW,YAAY,WAAY,QAAO,CAAC,2BAA4B;CACvE,MAAM,QAAQ,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,SACrC,eAAe,IAAI,IAAI,YAAY,QAAQ,KAAK,CAAC,EACnD;AACD,QAAO,MAAM,SAAS,IAAI,CAAC,kBAAmB,IAAG;AAClD;AAED,SAAS,cAAcO,SAAuC;AAC5D,KAAI,QAAQ,SAAS,EAAG,QAAO;CAC/B,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI,aAAa;AACnD,KAAI,QAAQ,SAAS,EACnB,OAAM,MAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO;AAEhD,QAAO,MAAM,KAAK,KAAK;AACxB;AAED,SAAS,aAAalB,QAA2B;CAC/C,MAAM,WAAW,eAAe,OAAO,SAAS;AAChD,SAAQ,KAAK,OAAO,MAAM,IAAI,SAAS,IAAI,cAAc,OAAO,QAAQ,CAAC,EACvE,iBAAiB,OAAO,WAAW,CACpC;AACF;AAED,SAAS,eAAeE,UAAqC;AAC3D,QAAO,SAAS,SAAS,IAAI,WAAW,SAAS,KAAK,IAAI;AAC3D;AAED,SAAS,iBACPQ,YACQ;CACR,MAAM,UAAU,OAAO,KAAK,WAAW;AACvC,KAAI,QAAQ,SAAS,EAAG,QAAO;CAC/B,MAAM,UAAU,QAAQ,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC,SACtC,EAAE,IAAI,IAAI,YAAY,WAAW,KAAK,CAAC,EACzC;AACD,KAAI,QAAQ,SAAS,EAAG,SAAQ,MAAM,MAAM,QAAQ,SAAS,EAAE,OAAO;AACtE,SAAQ,IAAI,QAAQ,KAAK,KAAK,CAAC;AAChC;AAED,SAAS,YAAYS,OAAeC,MAAsB;AACxD,SAAQ,EAAE,MAAM,GAAG,KAAK,EAAE,UAAU,IAAI,KAAK,IAAI;AAClD;AAED,SAAS,YAAYC,OAAwB;AAC3C,YAAW,UAAU,SAAU,QAAO,KAAK,UAAU,MAAM;AAC3D,YAAW,UAAU,SAAU,SAAQ,EAAE,MAAM;AAC/C,YAAW,UAAU,SAAU,QAAO,OAAO,MAAM;AACnD,KAAI,iBAAiB,OAAQ,QAAO,OAAO,MAAM;AACjD,KAAI,iBAAiB,MACnB,SAAQ,EAAE,MAAM,KAAK,IAAI,MAAM,QAAQ;AAEzC,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,IAAI,OAAO,MAAM;CAC9C,QAAO;AACN,SAAO,OAAO,MAAM;CACrB;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logtape/testing",
|
|
3
|
+
"version": "2.2.0-dev.0",
|
|
4
|
+
"description": "Testing utilities for collecting and asserting LogTape records",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"logging",
|
|
7
|
+
"log",
|
|
8
|
+
"logger",
|
|
9
|
+
"logtape",
|
|
10
|
+
"testing",
|
|
11
|
+
"test"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Hong Minhee",
|
|
16
|
+
"email": "hong@minhee.org",
|
|
17
|
+
"url": "https://hongminhee.org/"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://logtape.org/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/dahlia/logtape.git",
|
|
23
|
+
"directory": "packages/testing/"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/dahlia/logtape/issues"
|
|
27
|
+
},
|
|
28
|
+
"funding": [
|
|
29
|
+
"https://github.com/sponsors/dahlia"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"module": "./dist/mod.js",
|
|
33
|
+
"main": "./dist/mod.cjs",
|
|
34
|
+
"types": "./dist/mod.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": {
|
|
38
|
+
"import": "./dist/mod.d.ts",
|
|
39
|
+
"require": "./dist/mod.d.cts"
|
|
40
|
+
},
|
|
41
|
+
"import": "./dist/mod.js",
|
|
42
|
+
"require": "./dist/mod.cjs"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"files": [
|
|
48
|
+
"dist/"
|
|
49
|
+
],
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@logtape/logtape": "^2.2.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"tsdown": "^0.12.7",
|
|
55
|
+
"typescript": "^5.8.3",
|
|
56
|
+
"@logtape/redaction": "^2.2.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"prepublish": "tsdown",
|
|
61
|
+
"test": "tsdown && node --experimental-transform-types --test",
|
|
62
|
+
"test:bun": "tsdown && bun test",
|
|
63
|
+
"test:deno": "deno test",
|
|
64
|
+
"test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
|
|
65
|
+
}
|
|
66
|
+
}
|