@zaunt/zest 0.1.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 +201 -0
- package/README.md +590 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.mjs +118 -0
- package/dist/quick-reference-BHuN8iCv.mjs +419 -0
- package/package.json +51 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
var e=`## Zest Scenario Testing — Quick Reference
|
|
2
|
+
|
|
3
|
+
### Imports
|
|
4
|
+
|
|
5
|
+
\`\`\`ts
|
|
6
|
+
import {
|
|
7
|
+
scenario,
|
|
8
|
+
eachRow,
|
|
9
|
+
Context,
|
|
10
|
+
FactList,
|
|
11
|
+
stringCodec,
|
|
12
|
+
intCodec,
|
|
13
|
+
floatCodec,
|
|
14
|
+
booleanCodec,
|
|
15
|
+
enumCodec,
|
|
16
|
+
mappedCodec
|
|
17
|
+
} from '@zaunt/zest';
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
### Codecs
|
|
21
|
+
|
|
22
|
+
A codec parses a string to a typed value and formats it back.
|
|
23
|
+
|
|
24
|
+
- \`stringCodec\` — identity.
|
|
25
|
+
- \`intCodec\` — \`parseInt\`/\`String()\`.
|
|
26
|
+
- \`floatCodec(fractionDigits?)\` — \`parseFloat\`; formats with \`toFixed\` if \`fractionDigits\` given.
|
|
27
|
+
- \`booleanCodec(truthy?, falsy?, ignoreCase?)\` — defaults \`'yes'\`/\`'no'\`, case-insensitive.
|
|
28
|
+
- \`enumCodec(values, ignoreCase?)\` — matches against string literal union.
|
|
29
|
+
- \`mappedCodec(parseMap, formatMap, ignoreCase?)\` — arbitrary string↔value mapping.
|
|
30
|
+
|
|
31
|
+
### Exhibits, facts and tables
|
|
32
|
+
|
|
33
|
+
Zest extracts **exhibits** from markdown in document order. An exhibit is a \`Fact\` or a \`Table\`.
|
|
34
|
+
|
|
35
|
+
**Facts** (\`kind: 'fact'\`) come from backtick-wrapped text, bold (\`**v**\`), italic (\`*v*\`), fenced code blocks, or indented code blocks. All produce a single string value. Leading/trailing blank lines are trimmed; internal whitespace is preserved. Code block facts have \`metadata: { language: 'ts' }\` (fenced with language tag) or \`metadata: {}\`; non-code-block facts always have \`metadata: {}\`.
|
|
36
|
+
|
|
37
|
+
**Tables** (\`kind: 'table'\`) come from GFM markdown tables. Backticks, bold and italic inside cells don't produce separate facts.
|
|
38
|
+
|
|
39
|
+
Nested facts (e.g. \`\`**\`value\`**\`\`) and lists (bulleted/numbered) throw errors. Do not use them.
|
|
40
|
+
|
|
41
|
+
### \`scenario\`
|
|
42
|
+
|
|
43
|
+
\`\`\`ts
|
|
44
|
+
scenario({
|
|
45
|
+
name?: string,
|
|
46
|
+
markdown: string,
|
|
47
|
+
result?: codec, // default stringCodec; only used if execute returns non-undefined
|
|
48
|
+
failFast?: boolean,
|
|
49
|
+
execute(context) { ... }
|
|
50
|
+
});
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
The \`execute\` function receives a \`Context\` with:
|
|
54
|
+
|
|
55
|
+
- \`facts: FactList\`, \`tables: Table[]\`, \`exhibits: Exhibit[]\` — views over the same underlying objects.
|
|
56
|
+
- \`sectionNames: string[]\` — direct child section names.
|
|
57
|
+
- \`allSectionNames: { name: string; level: number }[]\` — all descendant headings.
|
|
58
|
+
- \`section(name)\` / \`section(/regex/)\` — returns a sub-context scoped to that section. Throws if zero or multiple headings match.
|
|
59
|
+
|
|
60
|
+
If \`execute\` returns a non-undefined value, it's compared to the **last fact** using the \`result\` codec. Otherwise assert manually with \`fact.assertEquals(actual)\` or \`fact.assertEquals(actual, codec)\`.
|
|
61
|
+
|
|
62
|
+
\`\`\`ts
|
|
63
|
+
// Return-value assertion (last fact is expected output)
|
|
64
|
+
scenario({
|
|
65
|
+
markdown: 'When made uppercase \`fred\` becomes \`FRED\`.',
|
|
66
|
+
execute({facts: [input]}) {
|
|
67
|
+
return input.asString().toUpperCase();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Manual assertion
|
|
72
|
+
scenario({
|
|
73
|
+
name: 'Boolean negation',
|
|
74
|
+
markdown: 'The input is \`aye\` the output is \`nay\`.',
|
|
75
|
+
execute({facts: [input, output]}) {
|
|
76
|
+
const codec = booleanCodec('aye', 'nay');
|
|
77
|
+
output.assertEquals(!input.as(codec), codec);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
### Sections
|
|
83
|
+
|
|
84
|
+
Headings divide the document into sections. A section spans from a heading to the next heading at the same or higher level, or to the end. Level 1 headings (\`#\`) are ignored for section purposes (treated as document title).
|
|
85
|
+
|
|
86
|
+
\`context.section(name)\` returns a sub-context with its own \`facts\`, \`tables\`, \`exhibits\`, \`sectionNames\`, \`allSectionNames\`, and \`section()\` — all filtered to that section's content (including subsections, flattened).
|
|
87
|
+
|
|
88
|
+
\`sectionNames\` returns direct child section names. If \`###\` headings appear before any \`##\`, they're treated as direct children until a shallower heading appears.
|
|
89
|
+
|
|
90
|
+
Sections nest: \`context.section('Parent').section('Child')\`. You can iterate:
|
|
91
|
+
|
|
92
|
+
\`\`\`ts
|
|
93
|
+
for (const name of context.sectionNames) {
|
|
94
|
+
const {
|
|
95
|
+
facts: [input, expected]
|
|
96
|
+
} = context.section(name);
|
|
97
|
+
expected.assertEquals(transform(input.asString()));
|
|
98
|
+
}
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
### Prose blocks
|
|
102
|
+
|
|
103
|
+
Top-level markdown lists throw errors because they are reserved for future features. To include lists or explanatory notes without creating facts, wrap narrative text in a \`<prose>\` tag.
|
|
104
|
+
|
|
105
|
+
Zest treats everything inside a prose block as plain text: lists are permitted, and backticks, bold, italic, or code blocks do not create facts.
|
|
106
|
+
|
|
107
|
+
\`\`\`markdown
|
|
108
|
+
# calculateDiscount()
|
|
109
|
+
|
|
110
|
+
<prose>
|
|
111
|
+
|
|
112
|
+
Discounts apply under these conditions:
|
|
113
|
+
|
|
114
|
+
- Orders over \`100\` receive **10%** off
|
|
115
|
+
- Premium members always receive **15%** off
|
|
116
|
+
|
|
117
|
+
</prose>
|
|
118
|
+
|
|
119
|
+
Spend \`150\` gets discount \`15\`.
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
In this example, only \`150\` and \`15\` are extracted as facts.
|
|
123
|
+
Note the blank lines before and after the \`<prose>\` tags.
|
|
124
|
+
These are needed for the CommonMark parser to process the inside as Markdown.
|
|
125
|
+
|
|
126
|
+
### FactList API
|
|
127
|
+
|
|
128
|
+
\`FactList\` is iterable and destructurable: \`const [a, b, c] = facts\`.
|
|
129
|
+
|
|
130
|
+
- \`facts.length\`, \`facts.at(index)\` — 0-based, throws if out of range.
|
|
131
|
+
- \`facts.byLanguage(language, n)\` — *n*th (0-based) code block with given language tag. Throws if not found.
|
|
132
|
+
|
|
133
|
+
### Fact API
|
|
134
|
+
|
|
135
|
+
- \`fact.asString()\` — raw string value.
|
|
136
|
+
- \`fact.asInt()\` — parse as integer.
|
|
137
|
+
- \`fact.as(codec)\` — parse with codec.
|
|
138
|
+
- \`fact.assertEquals(actual)\` — asserts with \`stringCodec\`.
|
|
139
|
+
- \`fact.assertEquals(actual, codec)\` — asserts with given codec.
|
|
140
|
+
- \`fact.metadata\`, \`fact.kind\` (\`'fact'\`).
|
|
141
|
+
|
|
142
|
+
Each fact can only be asserted once.
|
|
143
|
+
|
|
144
|
+
### Table API
|
|
145
|
+
|
|
146
|
+
- \`table.kind\` (\`'table'\`), \`table.headers\`, \`table.rowCount\`, \`table.columnCount\`.
|
|
147
|
+
- \`table.row(index)\` — 0-based body row, returns \`Row\`.
|
|
148
|
+
- \`table.cell(col, row)\` — returns \`Fact\`. Row 0 is header row.
|
|
149
|
+
- Iterable over body rows.
|
|
150
|
+
- \`table.toMap(codecs?)\` — 2-column → \`Record<string, T>\`. 3+ columns → \`Record<string, Record<string, T>>\`. First column is key. Duplicate keys throw.
|
|
151
|
+
- \`table.toRecords(codecs?)\` — \`Record<string, T>[]\`.
|
|
152
|
+
- \`table.eachRow(execute)\` or \`table.eachRow(options)\` — see below.
|
|
153
|
+
|
|
154
|
+
### Row API
|
|
155
|
+
|
|
156
|
+
- \`row.value('Col')\` / \`row.value('Col', codec)\` — string or parsed value.
|
|
157
|
+
- \`row.fact('Col')\` — returns \`Fact\` for assertions.
|
|
158
|
+
|
|
159
|
+
### \`table.eachRow\`
|
|
160
|
+
|
|
161
|
+
\`\`\`ts
|
|
162
|
+
table.eachRow(execute);
|
|
163
|
+
table.eachRow({
|
|
164
|
+
expectedColumn?: string, // defaults to last column
|
|
165
|
+
codecs?: { Col: codec },
|
|
166
|
+
result?: codec, // default stringCodec
|
|
167
|
+
failFast?: boolean,
|
|
168
|
+
execute(...facts) { ... }
|
|
169
|
+
});
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
If \`execute\` returns non-undefined, it's compared to the expected column. Returns a \`Promise\` — must \`await\` inside scenario. When \`failFast\` is false, all rows are processed; one failure rethrows directly, multiple produce a summary.
|
|
173
|
+
|
|
174
|
+
### \`eachRow\` (top-level)
|
|
175
|
+
|
|
176
|
+
Standalone function: parses markdown, finds first table, runs callback per row. Registers a Vitest test.
|
|
177
|
+
|
|
178
|
+
\`\`\`ts
|
|
179
|
+
eachRow({
|
|
180
|
+
name?: string,
|
|
181
|
+
markdown: string,
|
|
182
|
+
expectedColumn?: string, // defaults to last column
|
|
183
|
+
codecs?: { Col: codec },
|
|
184
|
+
result?: codec, // default stringCodec
|
|
185
|
+
failFast?: boolean,
|
|
186
|
+
execute(...facts) { ... }
|
|
187
|
+
});
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
### Example: scenario with table and inline facts
|
|
191
|
+
|
|
192
|
+
\`\`\`ts
|
|
193
|
+
import {redirectToTrailingSlash} from '@src/website/common/http/redirects';
|
|
194
|
+
import {scenario} from '@zaunt/zest';
|
|
195
|
+
import {Hono} from 'hono';
|
|
196
|
+
|
|
197
|
+
scenario({
|
|
198
|
+
name: 'redirectToTrailingSlash',
|
|
199
|
+
markdown: \`
|
|
200
|
+
# redirectToTrailingSlash()
|
|
201
|
+
|
|
202
|
+
Registers a '301 Moved Permanently' redirect from a path to the same path with a trailing slash.
|
|
203
|
+
|
|
204
|
+
Path | Request Path | Expected Location
|
|
205
|
+
--------------|-------------------|-------------------
|
|
206
|
+
/foo | /foo | /foo/
|
|
207
|
+
/bar/baz | /bar/baz | /bar/baz/
|
|
208
|
+
/a | /a | /a/
|
|
209
|
+
|
|
210
|
+
Throws an exception if the specified path already ends with a slash.
|
|
211
|
+
|
|
212
|
+
For example, if you try to register the path \\\`/foo/\\\`,
|
|
213
|
+
it throws an error with message:
|
|
214
|
+
<br/>\\\`Path "/foo/" already ends with a slash\\\`
|
|
215
|
+
\`,
|
|
216
|
+
async execute({facts: [invalidPath, expectedError], tables: [redirects]}) {
|
|
217
|
+
await redirects.eachRow({
|
|
218
|
+
async execute(path, requestPath) {
|
|
219
|
+
const app = new Hono();
|
|
220
|
+
redirectToTrailingSlash(app, path.asString());
|
|
221
|
+
const res = await app.request(requestPath.asString(), {
|
|
222
|
+
redirect: 'manual'
|
|
223
|
+
});
|
|
224
|
+
return res.headers.get('Location')!;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const app = new Hono();
|
|
230
|
+
redirectToTrailingSlash(app, invalidPath.asString());
|
|
231
|
+
expectedError.assertEquals('[no error thrown]');
|
|
232
|
+
} catch (e) {
|
|
233
|
+
expectedError.assertEquals((e as Error).message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
Note how the required behavior is briefly explained before the test cases. This is important because otherwise the reader must guess the rule from examples alone.
|
|
240
|
+
|
|
241
|
+
### Example: eachRow with codec
|
|
242
|
+
|
|
243
|
+
\`\`\`ts
|
|
244
|
+
import {getFormBoolean} from '@src/website/common/http/form-util';
|
|
245
|
+
import {eachRow, booleanCodec} from '@zaunt/zest';
|
|
246
|
+
|
|
247
|
+
eachRow({
|
|
248
|
+
name: 'getFormBoolean',
|
|
249
|
+
markdown: \`
|
|
250
|
+
# getFormBoolean()
|
|
251
|
+
|
|
252
|
+
Returns \\\`true\\\` when the form field value is \\\`on\\\`, \\\`true\\\`, or \\\`1\\\`.
|
|
253
|
+
Returns \\\`false\\\` for any other value, including when the key is absent.
|
|
254
|
+
|
|
255
|
+
Value | Expected
|
|
256
|
+
----------|----------
|
|
257
|
+
on | yes
|
|
258
|
+
true | yes
|
|
259
|
+
1 | yes
|
|
260
|
+
off | no
|
|
261
|
+
false | no
|
|
262
|
+
0 | no
|
|
263
|
+
whatever | no
|
|
264
|
+
(absent) | no
|
|
265
|
+
\`,
|
|
266
|
+
result: booleanCodec(),
|
|
267
|
+
execute(value) {
|
|
268
|
+
const formData = new FormData();
|
|
269
|
+
const raw = value.asString();
|
|
270
|
+
if (raw !== '(absent)') {
|
|
271
|
+
formData.set('field', raw);
|
|
272
|
+
}
|
|
273
|
+
return getFormBoolean(formData, 'field');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
\`\`\`
|
|
277
|
+
|
|
278
|
+
### Example: sections
|
|
279
|
+
|
|
280
|
+
\`\`\`ts
|
|
281
|
+
scenario({
|
|
282
|
+
name: 'createProdViteAssetTagsService',
|
|
283
|
+
markdown: \`# createProdViteAssetTagsService()
|
|
284
|
+
|
|
285
|
+
Generates asset tags from a Vite manifest. It collects the entry's JS file,
|
|
286
|
+
any CSS dependencies, and transitively imported chunks — emitting
|
|
287
|
+
\\\`<link rel="stylesheet">\\\` for CSS, \\\`<link rel="modulepreload">\\\` for
|
|
288
|
+
imported chunks, and a \\\`<script type="module">\\\` for the entry itself.
|
|
289
|
+
|
|
290
|
+
Given this manifest:
|
|
291
|
+
|
|
292
|
+
\\\`\\\`\\\`json
|
|
293
|
+
{
|
|
294
|
+
"_shared-abc123.js": {
|
|
295
|
+
"file": "js/shared-abc123.js",
|
|
296
|
+
"css": ["css/shared-xyz.css"]
|
|
297
|
+
},
|
|
298
|
+
"src/app.ts": {
|
|
299
|
+
"file": "js/app-def456.js",
|
|
300
|
+
"imports": ["_shared-abc123.js"],
|
|
301
|
+
"css": ["css/app-uvw.css"]
|
|
302
|
+
},
|
|
303
|
+
"src/other.ts": {
|
|
304
|
+
"file": "js/other-ghi789.js"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
\\\`\\\`\\\`
|
|
308
|
+
|
|
309
|
+
## Entry with imports and CSS
|
|
310
|
+
|
|
311
|
+
Requesting entry \\\`src/app.ts\\\` produces tags that include:
|
|
312
|
+
a stylesheet link for the entry's own CSS;
|
|
313
|
+
a stylesheet link for the shared chunk's CSS;
|
|
314
|
+
a modulepreload for the shared chunk;
|
|
315
|
+
the entry script
|
|
316
|
+
|
|
317
|
+
\\\`\\\`\\\`html
|
|
318
|
+
<link rel="stylesheet" href="/css/app-uvw.css" />
|
|
319
|
+
<link rel="stylesheet" href="/css/shared-xyz.css" />
|
|
320
|
+
<link rel="modulepreload" href="/js/shared-abc123.js" />
|
|
321
|
+
<script type="module" src="/js/app-def456.js"><\/script>
|
|
322
|
+
\\\`\\\`\\\`
|
|
323
|
+
|
|
324
|
+
## Entry with no imports
|
|
325
|
+
|
|
326
|
+
Requesting entry \\\`src/other.ts\\\` produces just the entry script tag.
|
|
327
|
+
|
|
328
|
+
\\\`\\\`\\\`html
|
|
329
|
+
<script type="module" src="/js/other-ghi789.js"><\/script>
|
|
330
|
+
\\\`\\\`\\\`
|
|
331
|
+
|
|
332
|
+
## Unknown entry
|
|
333
|
+
|
|
334
|
+
Requesting an entry that doesn't exist in the manifest (e.g. \\\`src/nonexistent.ts\\\`) produces empty output.
|
|
335
|
+
|
|
336
|
+
\\\`\\\`\\\`html
|
|
337
|
+
\\\`\\\`\\\`
|
|
338
|
+
|
|
339
|
+
## Custom base path
|
|
340
|
+
|
|
341
|
+
Using a custom base path \\\`/static/\\\` for entry \\\`src/other.ts\\\` produces:
|
|
342
|
+
|
|
343
|
+
\\\`\\\`\\\`html
|
|
344
|
+
<script type="module" src="/static/js/other-ghi789.js"><\/script>
|
|
345
|
+
\\\`\\\`\\\`\`,
|
|
346
|
+
async execute(context) {
|
|
347
|
+
const manifest = JSON.parse(
|
|
348
|
+
context.facts.byLanguage('json', 0).asString()
|
|
349
|
+
) as ViteManifest;
|
|
350
|
+
const defaultService = createProdViteAssetTagsService(manifest);
|
|
351
|
+
const normalize = (s: string) =>
|
|
352
|
+
s
|
|
353
|
+
.split('\\n')
|
|
354
|
+
.map((line) => line.trim())
|
|
355
|
+
.filter((line) => line.length > 0)
|
|
356
|
+
.join('\\n');
|
|
357
|
+
|
|
358
|
+
for (const sectionName of [
|
|
359
|
+
'Entry with imports and CSS',
|
|
360
|
+
'Entry with no imports',
|
|
361
|
+
'Unknown entry'
|
|
362
|
+
]) {
|
|
363
|
+
const {
|
|
364
|
+
facts: [input, expected]
|
|
365
|
+
} = context.section(sectionName);
|
|
366
|
+
expected.assertEquals(normalize(await defaultService(input.asString())));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const {
|
|
370
|
+
facts: [basePath, entry, expected]
|
|
371
|
+
} = context.section('Custom base path');
|
|
372
|
+
const customService = createProdViteAssetTagsService(
|
|
373
|
+
manifest,
|
|
374
|
+
basePath.asString()
|
|
375
|
+
);
|
|
376
|
+
expected.assertEquals(normalize(await customService(entry.asString())));
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
\`\`\`
|
|
380
|
+
|
|
381
|
+
Note that all relevant facts are drawn from facts in the spec.
|
|
382
|
+
Nothing relevant to the behavior being tested is hardcoded in the execute method.
|
|
383
|
+
But things that are irrelevant to the behavior being tested aren't included in the scenario and may be hardcoded in the execute method.
|
|
384
|
+
|
|
385
|
+
### Hints for writing Zest tests
|
|
386
|
+
|
|
387
|
+
The point of Zest tests is to protect you when you refactor.
|
|
388
|
+
They protect you by explaining the functionality in a way that doesn't change when you refactor.
|
|
389
|
+
In other words, they explain the external behaviour rather than the implementation.
|
|
390
|
+
That lets you change the implementation and get the protection, because you can check that the behaviour still works as before.
|
|
391
|
+
Without this kind of stability and separation, you'd end up having to change the tests and code at the same time, which would be a recipe for errors.
|
|
392
|
+
|
|
393
|
+
The fixture code acts as a kind of go-between for the scenarios and the system under test.
|
|
394
|
+
Its purpose is to shield the scenarios from the details of the system under test.
|
|
395
|
+
|
|
396
|
+
The upshot is that the scenarios need to be written in a careful way to avoid introducing unnecessary implementation details that lock you into a specific implementation.
|
|
397
|
+
|
|
398
|
+
For example, rather than scripting and saying, 'Do this, then do that. Click here, click that,' the scenarios should just explain the desired state and let the fixture figure out how to get the system under test into that state.
|
|
399
|
+
|
|
400
|
+
A second advantage of this approach is that the same scenarios can be run against different levels of the implementation.
|
|
401
|
+
For example, they could be run against a particular unit, or they could be run against the system as a whole.
|
|
402
|
+
The scenarios are effectively scale-free.
|
|
403
|
+
|
|
404
|
+
Another important consideration when writing scenarios is to avoid including information that isn't necessary to describe the particular behaviour being demonstrated.
|
|
405
|
+
For example, if you are testing that passwords match a certain set of rules, then you don't need to include details of the user or the login page or anything like that.
|
|
406
|
+
You can just focus on explaining the rules of the password, to let the fixture decide how to set the system into the right state to expose that functionality to the test.
|
|
407
|
+
|
|
408
|
+
Often you will want to test each rule separately, but sometimes, as in password rules, you need to test all of the rules combined because they are all checked at once.
|
|
409
|
+
You can't just test the password length rule on its own with a bunch of different-length passwords that have no numbers, if the rules say that you also need numbers.
|
|
410
|
+
So you have to think quite carefully about exactly what you're testing and what needs to be included and what shouldn't be included, to make the test as simple as possible but no simpler.
|
|
411
|
+
|
|
412
|
+
One of the common failure modes is to include unnecessary information instead of letting the fixture code decide any details that are unimportant to the behaviour being explained.
|
|
413
|
+
|
|
414
|
+
Another failure mode is to give only the scenarios without explaining the rules behind them.
|
|
415
|
+
That forces the reader to have to infer (fallibly guess) the rules which is totally unnecessary if you just explain the rules.
|
|
416
|
+
|
|
417
|
+
Explaining the rules also helps you to figure out exactly what scenarios you need to test.
|
|
418
|
+
Sometimes scenario writers over-specify and test things that aren't necessary.
|
|
419
|
+
`;export{e as t};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zaunt/zest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/zaunt/zest.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/zaunt/zest/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/zaunt/zest",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/markdown-it": "^14.2.0",
|
|
30
|
+
"@types/node": "^25.9.8",
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
32
|
+
"@typescript-eslint/parser": "^8.70.0",
|
|
33
|
+
"eslint": "^10.11.0",
|
|
34
|
+
"prettier": "^3.9.8",
|
|
35
|
+
"prettier-plugin-curly": "^0.4.1",
|
|
36
|
+
"tsdown": "^0.23.0",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"typescript-eslint": "^8.70.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"markdown-it": "^14.3.2",
|
|
42
|
+
"vitest": "^4.1.11"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown --minify --dts",
|
|
46
|
+
"build:watch": "tsdown --watch",
|
|
47
|
+
"format": "prettier \"**/*.{mjs,html,js,json,ts}\" --write --list-different",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
50
|
+
}
|
|
51
|
+
}
|