@jterrazz/test 16.1.1 → 17.0.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/README.md +45 -171
- package/dist/browser/index.js +2 -2980
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4534 -2628
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3004
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6127 -5693
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +373 -525
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -248
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -332
- package/docs/19-linting.md +0 -512
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
package/dist/vitest.js
CHANGED
|
@@ -1,232 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as
|
|
1
|
+
import { a as callerFrames, r as writeInPlace } from "./shared-in-place.js";
|
|
2
|
+
import { t as isRecord } from "./shared-json-shape.js";
|
|
3
|
+
import { n as refusal$1, t as invariant } from "./shared-refusal.js";
|
|
4
|
+
import { i as renderText, n as literate, o as resolveSpecPath } from "./shared-literate-plugin.js";
|
|
5
|
+
import { u as shouldUpdateSnapshots } from "./shared-ledger.js";
|
|
6
|
+
import { i as COVERAGE_DIR, l as VITEST_ARTIFACTS_DIR, n as ATTACHMENTS_DIR, o as REPORTER_OUTPUT_FILES, r as BLOB_DIR, s as SCREENSHOTS_DIR, t as ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
7
|
+
import { t as derivedProjects } from "./shared-derived-projects.js";
|
|
8
|
+
import { t as TEST_SECTION } from "./shared-test-section.js";
|
|
9
|
+
import { r as servicesSetup } from "./shared-services.global-setup.js";
|
|
10
|
+
import { r as tempRootSetup } from "./shared-temp-root.global-setup.js";
|
|
11
|
+
import { inPlaceSetup } from "./in-place.global-setup.js";
|
|
12
|
+
import { t as routeCoverageSetup } from "./shared-route-coverage.global-setup.js";
|
|
13
|
+
import { mobileSessionSetup } from "./mobile-session.global-setup.js";
|
|
4
14
|
import { createRequire } from "node:module";
|
|
15
|
+
import { GROUND_GLOB, readSettings } from "@jterrazz/typescript/rules";
|
|
5
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
17
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
7
18
|
import { fileURLToPath } from "node:url";
|
|
8
19
|
import { configDefaults, mergeConfig } from "vitest/config";
|
|
9
|
-
//#region src/
|
|
10
|
-
/**
|
|
11
|
-
* The `literate()` vite plugin — door one of the spec-document format.
|
|
12
|
-
*
|
|
13
|
-
* It makes a `<case>.spec.yaml` file a TEST FILE: the glob joins the project's
|
|
14
|
-
* test include, and each document is transformed into a one-test module that
|
|
15
|
-
* runs the file through the registered `cli` runner. The runner shows the
|
|
16
|
-
* document's path as the file and its `description:` as the title, so a failing
|
|
17
|
-
* scenario is opened where it is written — not in a `.test.ts` that merely
|
|
18
|
-
* points at it.
|
|
19
|
-
*
|
|
20
|
-
* ```typescript
|
|
21
|
-
* // vitest.config.ts
|
|
22
|
-
* import { defineConfig } from 'vitest/config';
|
|
23
|
-
* import { literate } from '@jterrazz/test/vitest';
|
|
24
|
-
*
|
|
25
|
-
* export default defineConfig({
|
|
26
|
-
* plugins: [literate({ specification: './specs/cli/cli.specification.ts' })],
|
|
27
|
-
* });
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
/** The `.spec.yaml` extension, matched on the id vite hands the plugin. */
|
|
31
|
-
const SPEC_FILE = /\.spec\.yaml(?:\?.*)?$/u;
|
|
32
|
-
/**
|
|
33
|
-
* The module a `<case>.spec.yaml` becomes: one `test()` whose title is the
|
|
34
|
-
* document's `description:` and whose body runs the whole file — its ground,
|
|
35
|
-
* its servers, every run asserted.
|
|
36
|
-
*
|
|
37
|
-
* @internal Exported for unit tests.
|
|
38
|
-
*/
|
|
39
|
-
function literateModule(content, filePath, specificationPath) {
|
|
40
|
-
const title = specDescription(content, filePath);
|
|
41
|
-
return [
|
|
42
|
-
"import { test } from 'vitest';",
|
|
43
|
-
`import { cli } from ${JSON.stringify(specificationPath)};`,
|
|
44
|
-
"",
|
|
45
|
-
`test(${JSON.stringify(title)}, async () => {`,
|
|
46
|
-
` await cli.run(${JSON.stringify(filePath)});`,
|
|
47
|
-
"});",
|
|
48
|
-
""
|
|
49
|
-
].join("\n");
|
|
50
|
-
}
|
|
51
|
-
/** Strip vite's query suffix (`?v=…`) from a module id. */
|
|
52
|
-
function cleanId(id) {
|
|
53
|
-
return id.split("?")[0] ?? id;
|
|
54
|
-
}
|
|
55
|
-
function literate(options) {
|
|
56
|
-
const include = options.include ?? ["**/*.spec.yaml"];
|
|
57
|
-
let root = process.cwd();
|
|
58
|
-
return {
|
|
59
|
-
config: () => ({ test: { include } }),
|
|
60
|
-
configResolved: (resolved) => {
|
|
61
|
-
root = resolved.root;
|
|
62
|
-
},
|
|
63
|
-
enforce: "pre",
|
|
64
|
-
load: (id) => {
|
|
65
|
-
if (!SPEC_FILE.test(id)) return null;
|
|
66
|
-
const filePath = cleanId(id);
|
|
67
|
-
const specificationPath = isAbsolute(options.specification) ? options.specification : resolve(root, options.specification);
|
|
68
|
-
return literateModule(readFileSync(filePath, "utf8"), filePath, specificationPath);
|
|
69
|
-
},
|
|
70
|
-
name: "jterrazz-test:literate"
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/runner/preset.ts
|
|
75
|
-
/**
|
|
76
|
-
* `defineSpecConfig()` — the shared vitest config every repository of the
|
|
77
|
-
* ecosystem starts from.
|
|
78
|
-
*
|
|
79
|
-
* Fourteen hand-rolled `vitest.config.ts` files had converged on the same
|
|
80
|
-
* handful of settings and disagreed on the rest by accident; the preset is
|
|
81
|
-
* their common ground, stated once. A consumer writes ONE call and keeps
|
|
82
|
-
* everything vitest offers: what it passes is a plain vite/vitest config,
|
|
83
|
-
* merged OVER the defaults, so any of them is overridable in place.
|
|
84
|
-
*
|
|
85
|
-
* ```typescript
|
|
86
|
-
* // vitest.config.ts
|
|
87
|
-
* import { defineSpecConfig } from '@jterrazz/test/vitest';
|
|
88
|
-
*
|
|
89
|
-
* export default defineSpecConfig({
|
|
90
|
-
* literate: { specification: './specs/cli/cli.specification.ts' },
|
|
91
|
-
* test: {
|
|
92
|
-
* projects: [
|
|
93
|
-
* { test: { include: ['src/**\/*.test.ts'], name: 'unit' } },
|
|
94
|
-
* { test: { include: ['specs/api/**\/*.test.ts'], name: 'api' } },
|
|
95
|
-
* ],
|
|
96
|
-
* },
|
|
97
|
-
* });
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
/**
|
|
101
|
-
* A run's budget. Vitest's own 5s test / 10s hook defaults were raised by every
|
|
102
|
-
* repository that starts real infrastructure, and none of them meant 5s — a
|
|
103
|
-
* container boot, a `prisma db push` or a `next build` all outlive it. 30s is
|
|
104
|
-
* the value the ecosystem's configs already converged on; a suite that needs
|
|
105
|
-
* more (a simulator cold boot, a real image build) states its own per project.
|
|
106
|
-
*/
|
|
20
|
+
//#region src/vitest/project.ts
|
|
107
21
|
const TEST_TIMEOUT_MS = 3e4;
|
|
108
22
|
const HOOK_TIMEOUT_MS = 3e4;
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
* Merged, not replaced: vite concatenates arrays, so a consumer's `exclude`
|
|
116
|
-
* ADDS to this list and never has to spread `configDefaults.exclude` again.
|
|
117
|
-
*/
|
|
118
|
-
const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
|
|
119
|
-
/**
|
|
120
|
-
* What a test may never carry into the next one, and the one policy the
|
|
121
|
-
* framework states rather than lints.
|
|
122
|
-
*
|
|
123
|
-
* `retry: 0` is the policy: a flaky test is fixed or deleted, never re-rolled
|
|
124
|
-
* until it passes. A retry turns a real defect into a slow one and hides the
|
|
125
|
-
* day it becomes permanent.
|
|
126
|
-
*
|
|
127
|
-
* The three restores are the other half of the same idea. `vi.spyOn`,
|
|
128
|
-
* `vi.stubGlobal` and `vi.stubEnv` each own a teardown, and a test that forgets
|
|
129
|
-
* one leaks into whatever runs next — a failure that appears in a FILE that did
|
|
130
|
-
* nothing wrong and disappears when that file is run alone. Turning them on
|
|
131
|
-
* here is what lets `clock.at()` and `vi.stubEnv()` be written without an
|
|
132
|
-
* `afterEach`, which is the shape rule J6w asks for.
|
|
133
|
-
*/
|
|
23
|
+
/** Ground folders hold inputs and deliberately-wrong twins, never tests, and `.artifacts/` copies of the project (a mutation sandbox); vite concatenates, so a consumer's `exclude` adds to this. */
|
|
24
|
+
const EXCLUDE = [
|
|
25
|
+
...configDefaults.exclude,
|
|
26
|
+
`${ARTIFACTS_DIR}/**`,
|
|
27
|
+
GROUND_GLOB
|
|
28
|
+
];
|
|
134
29
|
const HYGIENE = {
|
|
135
30
|
restoreMocks: true,
|
|
136
31
|
retry: 0,
|
|
137
32
|
unstubEnvs: true,
|
|
138
33
|
unstubGlobals: true
|
|
139
34
|
};
|
|
140
|
-
/**
|
|
141
|
-
* Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
|
|
142
|
-
* options (vitest omits them from a project's config), so they are absent here
|
|
143
|
-
* on purpose — setting them would be silently dropped.
|
|
144
|
-
*
|
|
145
|
-
* Exported for the project helpers, which build their canonical project ON
|
|
146
|
-
* these rather than beside them: a helper's project must carry the same
|
|
147
|
-
* budgets and the same artefact directory as a hand-written one.
|
|
148
|
-
*
|
|
149
|
-
* @internal
|
|
150
|
-
*/
|
|
35
|
+
/** `coverage` and `reporters` are root-only: vitest drops them from a project. */
|
|
151
36
|
function projectDefaults() {
|
|
152
37
|
return {
|
|
153
38
|
cacheDir: VITEST_ARTIFACTS_DIR,
|
|
154
39
|
test: {
|
|
155
40
|
attachmentsDir: ATTACHMENTS_DIR,
|
|
156
41
|
exclude: EXCLUDE,
|
|
42
|
+
globalSetup: [inPlaceSetup(), tempRootSetup()],
|
|
157
43
|
hookTimeout: HOOK_TIMEOUT_MS,
|
|
158
44
|
...HYGIENE,
|
|
159
45
|
testTimeout: TEST_TIMEOUT_MS
|
|
160
46
|
}
|
|
161
47
|
};
|
|
162
48
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Coverage, when `--coverage` asks for it.
|
|
165
|
-
*
|
|
166
|
-
* `v8` because it is the runtime's own counter: no instrumentation pass, no
|
|
167
|
-
* second transform of every file, and the numbers do not move when the bundler
|
|
168
|
-
* does. The provider is an OPTIONAL peer (`@vitest/coverage-v8`), so a project
|
|
169
|
-
* that never asks for coverage never installs it.
|
|
170
|
-
*
|
|
171
|
-
* `json-summary` is not a taste: it is the machine-readable report the ratchet
|
|
172
|
-
* reads. `text` is for the human running the command, and `html` for the one
|
|
173
|
-
* chasing a line. All three land under `.artifacts/vitest/coverage/`, the one
|
|
174
|
-
* directory the toolchain's page owns.
|
|
175
|
-
*
|
|
176
|
-
* What is EXCLUDED is the shape of the answer, not a way to raise the number:
|
|
177
|
-
* a type-only module has nothing to execute (v8 reports it as 0/0 or, worse,
|
|
178
|
-
* as uncovered lines that cannot be covered), and the CLI entries are proven
|
|
179
|
-
* by running the binary, not by importing it.
|
|
180
|
-
*/
|
|
181
|
-
const COVERAGE = {
|
|
182
|
-
exclude: [
|
|
183
|
-
"**/node_modules/**",
|
|
184
|
-
"specs/**",
|
|
185
|
-
"**/_fixtures/**",
|
|
186
|
-
"**/*.d.ts",
|
|
187
|
-
"**/*.test-d.ts",
|
|
188
|
-
"**/*.fixtures.ts",
|
|
189
|
-
"dist/**",
|
|
190
|
-
".artifacts/**",
|
|
191
|
-
"**/*.config.ts",
|
|
192
|
-
"**/*.config.mts"
|
|
193
|
-
],
|
|
194
|
-
provider: "v8",
|
|
195
|
-
reporter: [
|
|
196
|
-
"text",
|
|
197
|
-
"html",
|
|
198
|
-
"json-summary"
|
|
199
|
-
],
|
|
200
|
-
reportsDirectory: COVERAGE_DIR
|
|
201
|
-
};
|
|
202
|
-
/** Defaults for the root config — the project ones, plus what only a root carries. */
|
|
203
|
-
function rootDefaults() {
|
|
204
|
-
return {
|
|
205
|
-
cacheDir: VITEST_ARTIFACTS_DIR,
|
|
206
|
-
test: {
|
|
207
|
-
attachmentsDir: ATTACHMENTS_DIR,
|
|
208
|
-
coverage: COVERAGE,
|
|
209
|
-
exclude: EXCLUDE,
|
|
210
|
-
hookTimeout: HOOK_TIMEOUT_MS,
|
|
211
|
-
...HYGIENE,
|
|
212
|
-
outputFile: REPORTER_OUTPUT_FILES,
|
|
213
|
-
testTimeout: TEST_TIMEOUT_MS
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
/** A glob list with its repeats taken out, or nothing where there is no list. */
|
|
218
49
|
function eachOnce(globs) {
|
|
219
50
|
return globs === void 0 ? void 0 : [...new Set(globs)];
|
|
220
51
|
}
|
|
221
|
-
/**
|
|
222
|
-
* The glob lists vite CONCATENATES rather than overrides, each glob kept once.
|
|
223
|
-
*
|
|
224
|
-
* A project built ON the defaults and then merged INTO them by
|
|
225
|
-
* `defineSpecConfig` carried every default twice. Glob matching is idempotent,
|
|
226
|
-
* so nothing collected differently — but the no-tests banner is the one place a
|
|
227
|
-
* consumer ever reads that list, and it read as three merges of the same three
|
|
228
|
-
* globs.
|
|
229
|
-
*/
|
|
52
|
+
/** Vite concatenates these lists: each glob is kept once. */
|
|
230
53
|
function statedOnce(test) {
|
|
231
54
|
const exclude = eachOnce(test.exclude);
|
|
232
55
|
const include = eachOnce(test.include);
|
|
@@ -236,11 +59,6 @@ function statedOnce(test) {
|
|
|
236
59
|
...include === void 0 ? {} : { include }
|
|
237
60
|
};
|
|
238
61
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Build a project ON the defaults — the ONE way a project is built here.
|
|
241
|
-
*
|
|
242
|
-
* @internal
|
|
243
|
-
*/
|
|
244
62
|
function onProjectDefaults(stated) {
|
|
245
63
|
const merged = mergeConfig(projectDefaults(), stated);
|
|
246
64
|
return merged.test === void 0 ? merged : {
|
|
@@ -248,151 +66,139 @@ function onProjectDefaults(stated) {
|
|
|
248
66
|
test: statedOnce(merged.test)
|
|
249
67
|
};
|
|
250
68
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
* never saw, and the root's exclusions are the only ones it gets from here.
|
|
255
|
-
*/
|
|
256
|
-
function allInline(projects) {
|
|
257
|
-
return projects.every((project) => typeof project === "object" && project !== null && !("then" in project));
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* The root's exclusions, beside projects that already carry them.
|
|
261
|
-
*
|
|
262
|
-
* With `projects`, the root collects nothing itself: its `exclude` only travels
|
|
263
|
-
* INTO each project, where vitest CONCATENATES it with the project's own — so
|
|
264
|
-
* every preset glob reached the banner twice. The preset's own are dropped
|
|
265
|
-
* here, since each inline project states them in full; what the CONSUMER stated
|
|
266
|
-
* at the root stays, because no project holds a copy of that.
|
|
267
|
-
*/
|
|
268
|
-
function rootExcludeBeside(projects, exclude) {
|
|
269
|
-
return allInline(projects) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* A project inherits NOTHING from the root `test` block — vitest resolves each
|
|
273
|
-
* project as its own config — so the defaults are merged into every inline one.
|
|
274
|
-
* A project declared as a glob string, a promise or a function is handed back
|
|
275
|
-
* untouched: there is no object to merge into.
|
|
276
|
-
*/
|
|
277
|
-
function withProjectDefaults(project) {
|
|
278
|
-
if (typeof project !== "object" || project === null || "then" in project) return project;
|
|
279
|
-
return onProjectDefaults(project);
|
|
280
|
-
}
|
|
281
|
-
function defineSpecConfig(config = {}) {
|
|
282
|
-
const { literate: literateOptions, ...userConfig } = config;
|
|
283
|
-
const stated = literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig;
|
|
284
|
-
const merged = mergeConfig(rootDefaults(), stated);
|
|
285
|
-
const root = merged.test === void 0 ? void 0 : statedOnce(merged.test);
|
|
286
|
-
if (root === void 0) return merged;
|
|
287
|
-
const { projects } = root;
|
|
288
|
-
if (projects === void 0) return {
|
|
289
|
-
...merged,
|
|
290
|
-
test: root
|
|
291
|
-
};
|
|
292
|
-
const inline = projects.map(withProjectDefaults);
|
|
69
|
+
/** A stated `include` or `exclude` replaces the canonical one; the group is 0 until a specification states its own. */
|
|
70
|
+
function projectTest(name, options, canonical) {
|
|
71
|
+
const exclude = options.exclude ?? canonical.exclude;
|
|
293
72
|
return {
|
|
294
|
-
...
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
73
|
+
...exclude === void 0 ? {} : { exclude },
|
|
74
|
+
include: options.include ?? canonical.include,
|
|
75
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
76
|
+
name,
|
|
77
|
+
sequence: { groupOrder: 0 },
|
|
78
|
+
...options.timeout === void 0 ? {} : {
|
|
79
|
+
hookTimeout: options.timeout,
|
|
80
|
+
testTimeout: options.timeout
|
|
299
81
|
}
|
|
300
82
|
};
|
|
301
83
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
name: facet,
|
|
311
|
-
sequence: { groupOrder: 0 },
|
|
312
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
84
|
+
function specificationProject(specification, options) {
|
|
85
|
+
return onProjectDefaults({ test: projectTest(specification, options, { include: [`specs/${specification}/**/*.spec.ts`] }) });
|
|
86
|
+
}
|
|
87
|
+
function inProcessProject(specification, options) {
|
|
88
|
+
const project = specificationProject(specification, options);
|
|
89
|
+
return options.runner === void 0 ? project : mergeConfig(project, { test: {
|
|
90
|
+
isolate: false,
|
|
91
|
+
setupFiles: [options.runner]
|
|
313
92
|
} });
|
|
314
93
|
}
|
|
315
94
|
//#endregion
|
|
316
|
-
//#region src/
|
|
317
|
-
/** `api()` — the app met through HTTP. */
|
|
95
|
+
//#region src/specifications/api/api.project.ts
|
|
318
96
|
function api(options = {}) {
|
|
319
|
-
return
|
|
97
|
+
return mergeConfig(inProcessProject("api", options), { test: { globalSetup: [servicesSetup(), routeCoverageSetup()] } });
|
|
320
98
|
}
|
|
321
99
|
//#endregion
|
|
322
|
-
//#region src/
|
|
323
|
-
/**
|
|
324
|
-
const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
|
|
325
|
-
const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
|
|
326
|
-
/**
|
|
327
|
-
* A cli project collects the facet's `.test.ts` files AND its documents: a
|
|
328
|
-
* `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
|
|
329
|
-
* so the plugin's glob has to join the include of the project that collects
|
|
330
|
-
* them — which is this one, never the root config.
|
|
331
|
-
*/
|
|
100
|
+
//#region src/specifications/cli/cli.project.ts
|
|
101
|
+
/** A cli project: the specification's `.spec.ts` files and its documents (docs/guides/spec-documents.md); the plugin's glob joins this project's include, never the root config. */
|
|
332
102
|
function cli(options = {}) {
|
|
333
|
-
const { literate:
|
|
334
|
-
const project =
|
|
335
|
-
if (
|
|
336
|
-
const documents = literateOptions?.include ?? CLI_DOCUMENTS;
|
|
103
|
+
const { literate: door, ...common } = options;
|
|
104
|
+
const project = specificationProject("cli", common);
|
|
105
|
+
if (door === false) return project;
|
|
337
106
|
return mergeConfig(project, { plugins: [literate({
|
|
338
|
-
include:
|
|
339
|
-
|
|
107
|
+
include: door?.include ?? ["specs/cli/**/*.spec.yaml"],
|
|
108
|
+
runner: door?.runner ?? "./specs/cli/cli.specification.ts"
|
|
340
109
|
})] });
|
|
341
110
|
}
|
|
342
111
|
//#endregion
|
|
343
|
-
//#region src/
|
|
344
|
-
/**
|
|
112
|
+
//#region src/stubs/delivery/browser-page/worker-script.ts
|
|
113
|
+
/** From this package's own install: `@vitest/browser` resolves it only where the installer hoists, and a consumer declaring `msw` is what `no-redundant-test-dependency` forbids. */
|
|
114
|
+
function mswWorkerScript() {
|
|
115
|
+
return createRequire(import.meta.url).resolve("msw/mockServiceWorker.js");
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/specifications/component/golden/commands.ts
|
|
119
|
+
function testFile(testPath) {
|
|
120
|
+
if (testPath === void 0) throw invariant("a golden command reached the server with no test path");
|
|
121
|
+
return testPath;
|
|
122
|
+
}
|
|
345
123
|
function goldenPath(testPath, name) {
|
|
346
|
-
|
|
347
|
-
return resolve(dirname(testPath), GROUND_EXPECTED, name);
|
|
124
|
+
return resolveSpecPath("toMatch", name, dirname(testFile(testPath)));
|
|
348
125
|
}
|
|
349
|
-
/** The ARIA snapshot of the rendered `<body>`, as YAML. */
|
|
350
126
|
const ariaTree = async (context) => {
|
|
351
127
|
return await (await context.frame()).locator("body").ariaSnapshot();
|
|
352
128
|
};
|
|
353
|
-
/**
|
|
354
|
-
* The ARIA snapshot of ONE node, named by the selector the page's locator
|
|
355
|
-
* carries — the accessible name, as the browser computed it, for a page that
|
|
356
|
-
* cannot compute one itself.
|
|
357
|
-
*/
|
|
358
|
-
const ariaNode = async (context, selector) => {
|
|
359
|
-
return await (await context.frame()).locator(selector).ariaSnapshot();
|
|
360
|
-
};
|
|
361
|
-
/**
|
|
362
|
-
* A line the page has for whoever is running the suite, written on the node
|
|
363
|
-
* side's stderr — the one channel vitest's default reporter passes through.
|
|
364
|
-
* The console is captured and dropped, so a page that printed to it printed to
|
|
365
|
-
* nobody.
|
|
366
|
-
*/
|
|
367
|
-
const notify = (_context, line) => {
|
|
368
|
-
process.stderr.write(`${line}\n`);
|
|
369
|
-
};
|
|
370
|
-
/** The golden's current content, or `null` when it does not exist yet. */
|
|
371
129
|
const goldenRead = (context, name) => {
|
|
372
130
|
const path = goldenPath(context.testPath, name);
|
|
373
131
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
374
132
|
};
|
|
375
|
-
/** Write the golden — update mode's half, and the path it wrote, for the message. */
|
|
376
133
|
const goldenWrite = (context, name, content) => {
|
|
377
134
|
const path = goldenPath(context.testPath, name);
|
|
378
135
|
mkdirSync(dirname(path), { recursive: true });
|
|
379
|
-
writeFileSync(path, content
|
|
136
|
+
writeFileSync(path, content);
|
|
380
137
|
return path;
|
|
381
138
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
139
|
+
const inPlaceWrite = (context, stack, text) => {
|
|
140
|
+
const file = testFile(context.testPath);
|
|
141
|
+
const frame = context.project.browser?.parseStacktrace(stack).find((candidate) => candidate.file === file);
|
|
142
|
+
if (frame === void 0) throw new Error(refusal$1({
|
|
143
|
+
fix: "call toMatch() from the test file itself, or compare to a golden file: toMatch(file(...))",
|
|
144
|
+
found: "update mode: no frame of this toMatch call is the test file, so the value in place has nowhere to be written",
|
|
145
|
+
read: "docs/reference/tokens.md#a-value-written-in-place-is-rewritten-in-the-test",
|
|
146
|
+
why: "A value written in place is rewritten in the test that states it."
|
|
147
|
+
}));
|
|
148
|
+
writeInPlace({
|
|
149
|
+
column: frame.column,
|
|
150
|
+
file,
|
|
151
|
+
line: frame.line
|
|
152
|
+
}, (indent) => renderText(text, indent), context.project.getProvidedContext().inPlaceRewrites);
|
|
153
|
+
return `${file}:${frame.line}`;
|
|
154
|
+
};
|
|
155
|
+
/** One list, so the names the page calls and the names the project registers cannot drift. */
|
|
386
156
|
const COMPONENT_COMMANDS = {
|
|
387
|
-
ariaNode,
|
|
388
157
|
ariaTree,
|
|
389
158
|
goldenRead,
|
|
390
159
|
goldenWrite,
|
|
391
|
-
|
|
160
|
+
inPlaceWrite
|
|
392
161
|
};
|
|
393
162
|
//#endregion
|
|
394
|
-
//#region src/
|
|
395
|
-
|
|
163
|
+
//#region src/specifications/component/peers/peer-versions.ts
|
|
164
|
+
const requireFrom$1 = createRequire(import.meta.url);
|
|
165
|
+
function versionOf(specifier) {
|
|
166
|
+
const path = requireFrom$1.resolve(`${specifier}/package.json`);
|
|
167
|
+
const manifest = JSON.parse(String(readFileSync(path)));
|
|
168
|
+
if (typeof manifest === "object" && manifest !== null && "version" in manifest) return String(manifest.version);
|
|
169
|
+
throw new Error(refusal$1({
|
|
170
|
+
fix: `reinstall ${specifier}: \`npm install ${specifier}\``,
|
|
171
|
+
found: `the installed ${specifier} manifest carries no version`,
|
|
172
|
+
read: "docs/start/set-up-a-project.md#what-to-install",
|
|
173
|
+
why: "The component project reads the version of the peers it configures from their installed manifests."
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
/** Vite 8 transforms with oxc and warns on every `esbuild` option; Vite 6 and 7 have no `oxc` key. */
|
|
177
|
+
function transformKey() {
|
|
178
|
+
return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
|
|
179
|
+
}
|
|
180
|
+
/** `@vitest/browser-playwright` peers vitest on an exact version; a mismatch fails deep in the tester naming neither. */
|
|
181
|
+
function assertProviderPin(runner, provider) {
|
|
182
|
+
if (provider === null) throw new Error(refusal$1("seam-is-an-optional-peer", {
|
|
183
|
+
fix: `npm i -D @vitest/browser-playwright@${runner} playwright`,
|
|
184
|
+
found: "component() requires `@vitest/browser-playwright`, and it is not installed"
|
|
185
|
+
}));
|
|
186
|
+
if (runner !== provider) throw new Error(refusal$1({
|
|
187
|
+
fix: `bump both to the same version in one change: npm i -D vitest@${provider} @vitest/browser-playwright@${provider}`,
|
|
188
|
+
found: `component(): @vitest/browser-playwright is ${provider}, and vitest is ${runner}`,
|
|
189
|
+
read: "docs/start/set-up-a-project.md#what-to-install",
|
|
190
|
+
why: "`@vitest/browser-playwright` peers vitest on an exact version."
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
function installedVersion(specifier) {
|
|
194
|
+
try {
|
|
195
|
+
return versionOf(specifier);
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/specifications/component/component.project.ts
|
|
396
202
|
const PIPELINE_KEYS = [
|
|
397
203
|
"assetsInclude",
|
|
398
204
|
"css",
|
|
@@ -400,44 +206,25 @@ const PIPELINE_KEYS = [
|
|
|
400
206
|
"envPrefix",
|
|
401
207
|
"esbuild",
|
|
402
208
|
"json",
|
|
209
|
+
"optimizeDeps",
|
|
403
210
|
"oxc",
|
|
404
211
|
"plugins",
|
|
405
212
|
"resolve"
|
|
406
213
|
];
|
|
407
|
-
|
|
408
|
-
* The seam's own runtime, pre-bundled so a cold cache never reloads mid-run.
|
|
409
|
-
*
|
|
410
|
-
* `msw` and `msw/browser` are NOT here: `@vitest/browser` already puts both in
|
|
411
|
-
* `optimizeDeps.exclude`, and Vite's esbuild optimizer (6 and 7) treats an
|
|
412
|
-
* entry that is both included and excluded as fatal — dependency optimisation
|
|
413
|
-
* throws before a single test runs.
|
|
414
|
-
*/
|
|
415
|
-
const SEAM_DEPENDENCIES = [
|
|
214
|
+
const PAGE_DEPENDENCIES = [
|
|
416
215
|
"react",
|
|
417
216
|
"react-dom/client",
|
|
418
217
|
"react/jsx-dev-runtime",
|
|
419
218
|
"react/jsx-runtime",
|
|
420
|
-
"vitest-browser-react"
|
|
219
|
+
"vitest-browser-react",
|
|
220
|
+
"vitest-mock-extended"
|
|
421
221
|
];
|
|
422
222
|
const requireFrom = createRequire(import.meta.url);
|
|
423
|
-
/** This module's own file — the frames the caller search walks past. */
|
|
424
223
|
const HELPERS_FILE = import.meta.filename;
|
|
425
|
-
/** A stack frame's file, in either of the two shapes V8 prints it in. */
|
|
426
|
-
const STACK_FRAME = /(?:\((?<wrapped>[^()]+):\d+:\d+\)|at (?<bare>[^()\s]+):\d+:\d+)$/u;
|
|
427
|
-
/** A config vite bundled before importing it — `<its name>.timestamp-<hash>.mjs`. */
|
|
428
224
|
const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
|
|
429
|
-
/** The file of the first frame below this module — who called the helper. */
|
|
430
225
|
function callerFile() {
|
|
431
|
-
|
|
432
|
-
for (const frame of stack?.split("\n").slice(1) ?? []) {
|
|
433
|
-
const groups = STACK_FRAME.exec(frame.trim())?.groups;
|
|
434
|
-
const location = groups?.wrapped ?? groups?.bare;
|
|
435
|
-
if (location === void 0 || location.startsWith("node:")) continue;
|
|
436
|
-
const file = location.startsWith("file:") ? fileURLToPath(location) : location;
|
|
437
|
-
if (file !== HELPERS_FILE) return file;
|
|
438
|
-
}
|
|
226
|
+
return callerFrames().find(({ file }) => file !== HELPERS_FILE)?.file;
|
|
439
227
|
}
|
|
440
|
-
/** The nearest directory at or above the cwd holding a file of this name. */
|
|
441
228
|
function nearestHolding(name) {
|
|
442
229
|
let directory = process.cwd();
|
|
443
230
|
for (;;) {
|
|
@@ -447,27 +234,6 @@ function nearestHolding(name) {
|
|
|
447
234
|
directory = parent;
|
|
448
235
|
}
|
|
449
236
|
}
|
|
450
|
-
/**
|
|
451
|
-
* The directory of the CONFIG that called a helper — what the relative paths
|
|
452
|
-
* it was handed mean.
|
|
453
|
-
*
|
|
454
|
-
* Not `process.cwd()`: a `vitest.config.ts` is read by more tools than the
|
|
455
|
-
* runner, and knip and `typescript check` load every config of a repository
|
|
456
|
-
* from the repository ROOT — `component({ vite: './web/vite.config.ts' })`
|
|
457
|
-
* written beside an app resolved against the wrong tree and threw before a
|
|
458
|
-
* single test ran. `import.meta` cannot answer either: it describes THIS
|
|
459
|
-
* module. And vitest hands a project neither its root nor its config file — a
|
|
460
|
-
* project function is called with `{ command, mode, isPreview, isSsrBuild }`
|
|
461
|
-
* and nothing else.
|
|
462
|
-
*
|
|
463
|
-
* So the call site is read off the stack, where a loader that keeps a config's
|
|
464
|
-
* identity (node, tsx, jiti — what those tools use) names the file itself.
|
|
465
|
-
* Vite's own loader does not: it BUNDLES the config into
|
|
466
|
-
* `node_modules/.vite-temp/` and only the config's NAME survives the move. The
|
|
467
|
-
* name is what is then looked for upward from the cwd, because that loader is
|
|
468
|
-
* the runner's and a runner is started in the tree it tests. A config loaded
|
|
469
|
-
* by the runner from another tree entirely states `root` itself.
|
|
470
|
-
*/
|
|
471
237
|
function callerDirectory() {
|
|
472
238
|
const file = callerFile();
|
|
473
239
|
if (file === void 0) return process.cwd();
|
|
@@ -475,7 +241,6 @@ function callerDirectory() {
|
|
|
475
241
|
if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
|
|
476
242
|
return nearestHolding(bundled) ?? process.cwd();
|
|
477
243
|
}
|
|
478
|
-
/** Is this specifier installed here? An absent optional peer is simply not pre-bundled. */
|
|
479
244
|
function resolves(specifier) {
|
|
480
245
|
try {
|
|
481
246
|
requireFrom.resolve(specifier);
|
|
@@ -484,20 +249,10 @@ function resolves(specifier) {
|
|
|
484
249
|
return false;
|
|
485
250
|
}
|
|
486
251
|
}
|
|
487
|
-
/**
|
|
488
|
-
* Serve msw's worker script from the package's OWN install, at the path the
|
|
489
|
-
* worker asks for, with the scope header a worker registered at `/` needs.
|
|
490
|
-
*
|
|
491
|
-
* `@vitest/browser` resolves `/mockServiceWorker.js` too, but from ITS own
|
|
492
|
-
* directory — and it does not depend on `msw`, so that resolution only lands
|
|
493
|
-
* where the installer hoists. The package that OWNS the dependency is the one
|
|
494
|
-
* that can always find it, and it is this one. Without either, a consumer runs
|
|
495
|
-
* `msw init` and declares `msw`, which F8 forbids.
|
|
496
|
-
*/
|
|
497
252
|
function mswWorkerPlugin() {
|
|
498
253
|
return {
|
|
499
254
|
configureServer(server) {
|
|
500
|
-
const script =
|
|
255
|
+
const script = mswWorkerScript();
|
|
501
256
|
server.middlewares.use("/mockServiceWorker.js", (_request, response) => {
|
|
502
257
|
response.setHeader("content-type", "text/javascript");
|
|
503
258
|
response.setHeader("service-worker-allowed", "/");
|
|
@@ -507,16 +262,7 @@ function mswWorkerPlugin() {
|
|
|
507
262
|
name: "@jterrazz/test:msw-worker"
|
|
508
263
|
};
|
|
509
264
|
}
|
|
510
|
-
|
|
511
|
-
* Keep an app's Vite PIPELINE and drop everything that says where the app
|
|
512
|
-
* lives.
|
|
513
|
-
*
|
|
514
|
-
* An allow-list, not a drop-list: a consumer's config carries keys the
|
|
515
|
-
* framework has never heard of — `environments`, `ssr`, `appType`, `publicDir`
|
|
516
|
-
* — and one of them re-roots the run and the project collects nothing. Naming
|
|
517
|
-
* what the pipeline IS keeps an unknown key from leaking in. The consumer's
|
|
518
|
-
* `plugins` are CONCATENATED after the seam's, never assigned over them.
|
|
519
|
-
*/
|
|
265
|
+
const isPluginList = (plugins) => Array.isArray(plugins);
|
|
520
266
|
async function reroot(source, root) {
|
|
521
267
|
const environment = {
|
|
522
268
|
command: "serve",
|
|
@@ -524,144 +270,90 @@ async function reroot(source, root) {
|
|
|
524
270
|
mode: "test"
|
|
525
271
|
};
|
|
526
272
|
let loaded;
|
|
527
|
-
let
|
|
273
|
+
let found;
|
|
274
|
+
let fix;
|
|
528
275
|
if (typeof source === "string") {
|
|
529
276
|
const { loadConfigFromFile } = await import("vite");
|
|
530
|
-
from = isAbsolute(source) ? source : resolve(root, source);
|
|
277
|
+
const from = isAbsolute(source) ? source : resolve(root, source);
|
|
531
278
|
loaded = (await loadConfigFromFile(environment, from))?.config;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (
|
|
279
|
+
found = `${from} exports no config`;
|
|
280
|
+
fix = "point `vite` at the app's own vite.config.ts";
|
|
281
|
+
} else if (typeof source === "function") {
|
|
282
|
+
loaded = await source(environment);
|
|
283
|
+
if (typeof loaded === "function") {
|
|
284
|
+
found = "the config function returned a function, not a config";
|
|
285
|
+
fix = "call the framework's factory and pass what it returns (for Astro, `getViteConfig({}, { root: import.meta.dirname })`, never `getViteConfig`)";
|
|
286
|
+
} else {
|
|
287
|
+
found = "the config function returned no config";
|
|
288
|
+
fix = "return the framework's config from the function (for Astro, `getViteConfig({}, { root: import.meta.dirname })`)";
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
loaded = await source;
|
|
292
|
+
found = "the config promise resolved to no config";
|
|
293
|
+
fix = "resolve the promise to the framework's config object";
|
|
294
|
+
}
|
|
295
|
+
if (typeof loaded !== "object" || loaded === null) throw new Error(refusal$1({
|
|
296
|
+
fix,
|
|
297
|
+
found: `component({ vite }): ${found}`,
|
|
298
|
+
read: "docs/guides/component-specs.md#the-constructor-there-is-none",
|
|
299
|
+
why: "A component renders through the Vite pipeline the app builds with, read from its config."
|
|
300
|
+
}));
|
|
535
301
|
const inactive = transformKey() === "oxc" ? "esbuild" : "oxc";
|
|
536
302
|
const kept = {};
|
|
537
303
|
const config = { ...loaded };
|
|
538
|
-
for (const key of PIPELINE_KEYS) if (
|
|
304
|
+
for (const key of PIPELINE_KEYS) if (key !== inactive) kept[key] = config[key];
|
|
539
305
|
return kept;
|
|
540
306
|
}
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Which of the two transformer keys the INSTALLED Vite actually reads.
|
|
551
|
-
*
|
|
552
|
-
* Vite 8 transforms with oxc and prints a warning for every `esbuild` option it
|
|
553
|
-
* is handed; Vite 6 and 7 transform with esbuild and have no `oxc` key at all.
|
|
554
|
-
* The inactive one is dropped rather than carried as a second, ignored
|
|
555
|
-
* statement — a config that says the same thing twice is a config that will one
|
|
556
|
-
* day say two different things.
|
|
557
|
-
*/
|
|
558
|
-
function transformKey() {
|
|
559
|
-
return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
|
|
307
|
+
function optimizeDepsOf(declared) {
|
|
308
|
+
const own = isRecord(declared) ? { ...declared } : {};
|
|
309
|
+
const listed = "include" in own && Array.isArray(own.include) ? own.include : [];
|
|
310
|
+
return {
|
|
311
|
+
...own,
|
|
312
|
+
include: [.../* @__PURE__ */ new Set([...PAGE_DEPENDENCIES.filter(resolves), ...listed.filter((entry) => typeof entry === "string")])]
|
|
313
|
+
};
|
|
560
314
|
}
|
|
561
|
-
/**
|
|
562
|
-
* The JSX default, on the key the running Vite reads, yielding to the
|
|
563
|
-
* consumer's own: a `jsxImportSource` or a classic runtime is the app's
|
|
564
|
-
* statement, not the seam's to overwrite.
|
|
565
|
-
*/
|
|
566
315
|
function jsxPipeline(pipeline) {
|
|
567
316
|
const key = transformKey();
|
|
568
317
|
const stated = pipeline[key];
|
|
569
318
|
if (key === "oxc") return { oxc: stated ?? { jsx: { runtime: "automatic" } } };
|
|
570
319
|
return { esbuild: stated ?? { jsx: "automatic" } };
|
|
571
320
|
}
|
|
572
|
-
/**
|
|
573
|
-
* `@vitest/browser-playwright` peers vitest on an EXACT version: 4.1.10 wants
|
|
574
|
-
* 4.1.10, not `^4.1`. A mismatch fails deep inside the tester with a message
|
|
575
|
-
* about a missing runner, so the pair is checked where the project is built
|
|
576
|
-
* and both versions are named.
|
|
577
|
-
*/
|
|
578
|
-
function assertProviderPin() {
|
|
579
|
-
const runner = versionOf("vitest");
|
|
580
|
-
let provider;
|
|
581
|
-
try {
|
|
582
|
-
provider = versionOf("@vitest/browser-playwright");
|
|
583
|
-
} catch {
|
|
584
|
-
throw new Error(PROVIDER_MISSING);
|
|
585
|
-
}
|
|
586
|
-
if (runner !== provider) throw new Error(`component(): @vitest/browser-playwright@${provider} peers vitest on an exact version, and vitest is ${runner}. Bump both to the same version in one change (npm i -D vitest@${provider} @vitest/browser-playwright@${provider}).`);
|
|
587
|
-
}
|
|
588
|
-
/** The framework's own update flag, read where `process` exists. */
|
|
589
|
-
function updating() {
|
|
590
|
-
return process.env.TEST_UPDATE === "1" || process.argv.includes("-u") || process.argv.includes("--update");
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Hand the project's `wrap` to the page as a STATIC import.
|
|
594
|
-
*
|
|
595
|
-
* The page could fetch the module by URL when the first render needs it, and
|
|
596
|
-
* that is what breaks: a dependency Vite never scanned is discovered mid-run,
|
|
597
|
-
* the optimizer rewrites its cache, and every module the page is holding —
|
|
598
|
-
* the test's own included — 404s behind a stale `?v=` hash. A setup file is in
|
|
599
|
-
* the graph before the first test runs, so the wrap and everything it imports
|
|
600
|
-
* are scanned, pre-bundled, and never re-optimised under the run.
|
|
601
|
-
*
|
|
602
|
-
* It is written into `.artifacts/` rather than served from memory because
|
|
603
|
-
* vitest resolves `setupFiles` as paths on disk; it is generated, so it belongs
|
|
604
|
-
* with everything else a tool generates, and never on the row.
|
|
605
|
-
*
|
|
606
|
-
* The wrapper lands on `globalThis` under a shared symbol rather than an
|
|
607
|
-
* export: the setup module and the package's browser build are separate graphs
|
|
608
|
-
* in the page and share no module instance.
|
|
609
|
-
*/
|
|
610
321
|
function writeSetupFile(wrap, root) {
|
|
611
322
|
const absolute = resolve(root, wrap);
|
|
612
323
|
const path = resolve(root, VITEST_ARTIFACTS_DIR, "component-setup.mjs");
|
|
613
324
|
mkdirSync(dirname(path), { recursive: true });
|
|
614
325
|
writeFileSync(path, [
|
|
615
|
-
"// Generated by `component({ wrap })`
|
|
326
|
+
"// Generated by `component({ wrap })` of @jterrazz/test. Do not edit.",
|
|
616
327
|
`import wrap from ${JSON.stringify(absolute)};`,
|
|
617
328
|
"globalThis[Symbol.for('@jterrazz/test:component-wrap')] = wrap;",
|
|
618
329
|
""
|
|
619
330
|
].join("\n"), "utf8");
|
|
620
331
|
return path;
|
|
621
332
|
}
|
|
622
|
-
/**
|
|
623
|
-
* What the page cannot read for itself: update mode is a `process.env`/`argv`
|
|
624
|
-
* read, and the project's clock is stated where `process` exists.
|
|
625
|
-
*/
|
|
626
333
|
function providedToPage(options) {
|
|
627
334
|
return {
|
|
628
335
|
...options.clock === void 0 ? {} : { componentClock: options.clock },
|
|
629
336
|
componentViewport: viewportOf(options),
|
|
630
|
-
update:
|
|
337
|
+
update: shouldUpdateSnapshots()
|
|
631
338
|
};
|
|
632
339
|
}
|
|
633
|
-
/** The page size every render of the project starts at. */
|
|
634
340
|
function viewportOf(options) {
|
|
635
341
|
return options.viewport ?? {
|
|
636
342
|
height: 720,
|
|
637
343
|
width: 1280
|
|
638
344
|
};
|
|
639
345
|
}
|
|
640
|
-
/**
|
|
641
|
-
function componentGlobs(options) {
|
|
642
|
-
return {
|
|
643
|
-
exclude: options.exclude ?? (options.include === void 0 ? ["specs/**"] : []),
|
|
644
|
-
include: options.include ?? ["**/*.test.tsx"]
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* A component test renders in a real Chromium — the browser the website facet
|
|
649
|
-
* already drives through the same `playwright` peer.
|
|
650
|
-
*
|
|
651
|
-
* The default `include` reaches the whole project, so the default `exclude`
|
|
652
|
-
* keeps it out of `specs/`, where the assembled product is specified. State
|
|
653
|
-
* your own `include` and the exclusion is yours to state too.
|
|
654
|
-
*/
|
|
346
|
+
/** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
|
|
655
347
|
async function component(options = {}) {
|
|
656
348
|
const root = options.root ?? callerDirectory();
|
|
657
|
-
assertProviderPin();
|
|
349
|
+
assertProviderPin(versionOf("vitest"), installedVersion("@vitest/browser-playwright"));
|
|
658
350
|
const { playwright } = await import("@vitest/browser-playwright");
|
|
659
351
|
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
660
|
-
const consumerPlugins =
|
|
352
|
+
const consumerPlugins = isPluginList(pipeline.plugins) ? pipeline.plugins : [];
|
|
661
353
|
return onProjectDefaults({
|
|
662
354
|
...pipeline,
|
|
663
355
|
...jsxPipeline(pipeline),
|
|
664
|
-
optimizeDeps:
|
|
356
|
+
optimizeDeps: optimizeDepsOf(pipeline.optimizeDeps),
|
|
665
357
|
plugins: [mswWorkerPlugin(), ...consumerPlugins],
|
|
666
358
|
test: {
|
|
667
359
|
browser: {
|
|
@@ -677,76 +369,232 @@ async function component(options = {}) {
|
|
|
677
369
|
screenshotDirectory: SCREENSHOTS_DIR,
|
|
678
370
|
viewport: viewportOf(options)
|
|
679
371
|
},
|
|
680
|
-
...
|
|
681
|
-
|
|
682
|
-
|
|
372
|
+
...projectTest("component", options, {
|
|
373
|
+
exclude: options.include === void 0 ? ["specs/**"] : [],
|
|
374
|
+
include: ["**/*.test.tsx"]
|
|
375
|
+
}),
|
|
683
376
|
provide: providedToPage(options),
|
|
684
377
|
...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] },
|
|
685
|
-
sequence: { groupOrder: 2 }
|
|
686
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
378
|
+
sequence: { groupOrder: 2 }
|
|
687
379
|
}
|
|
688
380
|
});
|
|
689
381
|
}
|
|
690
382
|
//#endregion
|
|
691
|
-
//#region src/
|
|
692
|
-
/** `integration()` — a module against real services, or against a golden. */
|
|
383
|
+
//#region src/specifications/integration/integration.project.ts
|
|
693
384
|
function integration(options = {}) {
|
|
694
|
-
return
|
|
385
|
+
return mergeConfig(inProcessProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/specifications/job/job.project.ts
|
|
389
|
+
function job(options = {}) {
|
|
390
|
+
return mergeConfig(inProcessProject("job", options), { test: { globalSetup: [servicesSetup()] } });
|
|
695
391
|
}
|
|
696
392
|
//#endregion
|
|
697
|
-
//#region src/
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return facetProject("jobs", options);
|
|
393
|
+
//#region src/specifications/lint/lint.project.ts
|
|
394
|
+
function lint(options = {}) {
|
|
395
|
+
return specificationProject("lint", options);
|
|
701
396
|
}
|
|
702
397
|
//#endregion
|
|
703
|
-
//#region src/
|
|
704
|
-
/**
|
|
705
|
-
* `mobile()` — the app on a simulator. A simulator is the one thing that
|
|
706
|
-
* cannot share a machine with itself, so it runs last and alone (group 3).
|
|
707
|
-
*/
|
|
398
|
+
//#region src/specifications/mobile/mobile.project.ts
|
|
399
|
+
/** A simulator cannot share a machine with itself: serial. A global setup holds one driver session for the run. */
|
|
708
400
|
function mobile(options = {}) {
|
|
709
|
-
|
|
401
|
+
const { device, root, ...common } = options;
|
|
402
|
+
return mergeConfig(specificationProject("mobile", common), { test: {
|
|
403
|
+
fileParallelism: false,
|
|
404
|
+
globalSetup: [mobileSessionSetup()],
|
|
405
|
+
...device === void 0 ? {} : { provide: { mobileAppium: {
|
|
406
|
+
device,
|
|
407
|
+
root: root ?? "."
|
|
408
|
+
} } },
|
|
409
|
+
sequence: { groupOrder: 3 }
|
|
410
|
+
} });
|
|
710
411
|
}
|
|
711
412
|
//#endregion
|
|
712
|
-
//#region src/
|
|
713
|
-
/**
|
|
714
|
-
* A website spec meets the assembled product through a served page, in the
|
|
715
|
-
* Chromium playwright drives. It opens a browser, so it is scheduled before the
|
|
716
|
-
* component project and never beside it: two Chromiums must not share a slot on
|
|
717
|
-
* a 2-vCPU runner.
|
|
718
|
-
*/
|
|
413
|
+
//#region src/specifications/website/website.project.ts
|
|
719
414
|
function website(options = {}) {
|
|
720
|
-
return
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
415
|
+
return mergeConfig(specificationProject("website", options), { test: { sequence: { groupOrder: 1 } } });
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/vitest/preset.ts
|
|
419
|
+
const COVERAGE = {
|
|
420
|
+
exclude: [
|
|
421
|
+
"**/node_modules/**",
|
|
422
|
+
"specs/**",
|
|
423
|
+
GROUND_GLOB,
|
|
424
|
+
"**/*.d.ts",
|
|
425
|
+
"**/*.test-d.ts",
|
|
426
|
+
"**/*.fixtures.ts",
|
|
427
|
+
".artifacts/**",
|
|
428
|
+
"**/*.config.ts",
|
|
429
|
+
"**/*.config.mts"
|
|
430
|
+
],
|
|
431
|
+
autoAttachSubprocess: true,
|
|
432
|
+
excludeAfterRemap: true,
|
|
433
|
+
provider: "v8",
|
|
434
|
+
reporter: [
|
|
435
|
+
"text",
|
|
436
|
+
"html",
|
|
437
|
+
"json-summary"
|
|
438
|
+
],
|
|
439
|
+
reportsDirectory: COVERAGE_DIR
|
|
440
|
+
};
|
|
441
|
+
function shardOf(argv) {
|
|
442
|
+
for (const [index, argument] of argv.entries()) {
|
|
443
|
+
if (argument.startsWith("--shard=")) return argument.slice(8);
|
|
444
|
+
if (argument === "--shard") return argv[index + 1];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function reporterOutputFiles(argv) {
|
|
448
|
+
const shard = shardOf(argv);
|
|
449
|
+
return shard === void 0 ? REPORTER_OUTPUT_FILES : {
|
|
450
|
+
...REPORTER_OUTPUT_FILES,
|
|
451
|
+
blob: `${BLOB_DIR}/blob-${shard.replace("/", "-")}.json`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function rootDefaults() {
|
|
455
|
+
return mergeConfig(projectDefaults(), { test: {
|
|
456
|
+
coverage: COVERAGE,
|
|
457
|
+
outputFile: reporterOutputFiles(process.argv)
|
|
727
458
|
} });
|
|
728
459
|
}
|
|
460
|
+
function isInline(project) {
|
|
461
|
+
return isRecord(project) && !("then" in project);
|
|
462
|
+
}
|
|
463
|
+
function rootExcludeBeside(projects, exclude) {
|
|
464
|
+
return projects.every(isInline) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
|
|
465
|
+
}
|
|
466
|
+
function withProjectDefaults(project) {
|
|
467
|
+
return isInline(project) ? onProjectDefaults(project) : project;
|
|
468
|
+
}
|
|
469
|
+
function presetConfig(stated = {}) {
|
|
470
|
+
const merged = mergeConfig(rootDefaults(), stated);
|
|
471
|
+
const root = statedOnce(merged.test ?? {});
|
|
472
|
+
const { projects } = root;
|
|
473
|
+
if (projects === void 0) return {
|
|
474
|
+
...merged,
|
|
475
|
+
test: root
|
|
476
|
+
};
|
|
477
|
+
const inline = projects.map(withProjectDefaults);
|
|
478
|
+
return {
|
|
479
|
+
...merged,
|
|
480
|
+
test: {
|
|
481
|
+
...root,
|
|
482
|
+
...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
|
|
483
|
+
projects: inline
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
729
487
|
//#endregion
|
|
730
|
-
//#region src/
|
|
731
|
-
/**
|
|
732
|
-
* A module test runs beside the module it covers: no DOM, no browser, no
|
|
733
|
-
* pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
|
|
734
|
-
* the assembled product is specified in `.spec.ts` — the two exclusions say
|
|
735
|
-
* exactly that. `.test.tsx` is the third exclusion because a rendered thing is
|
|
736
|
-
* a component test: a project's dependency scan reads every file its globs
|
|
737
|
-
* match, and a `.tsx` collected here would be optimised for a page that never
|
|
738
|
-
* opens.
|
|
739
|
-
*/
|
|
488
|
+
//#region src/vitest/unit.project.ts
|
|
489
|
+
/** `.test.tsx` is excluded: a rendered thing is a component test, and the dependency scan would optimise it for a page that never opens. */
|
|
740
490
|
function unit(options = {}) {
|
|
741
491
|
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
742
492
|
return onProjectDefaults({ test: {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
493
|
+
...projectTest("unit", options, {
|
|
494
|
+
exclude: fromRoots === void 0 ? ["specs/**", "**/*.test.tsx"] : ["**/*.test.tsx"],
|
|
495
|
+
include: fromRoots ?? ["**/*.test.ts"]
|
|
496
|
+
}),
|
|
497
|
+
...options.isolate === void 0 ? {} : { isolate: options.isolate }
|
|
749
498
|
} });
|
|
750
499
|
}
|
|
751
500
|
//#endregion
|
|
752
|
-
|
|
501
|
+
//#region src/index.vitest.ts
|
|
502
|
+
/** The shared vitest config: what you pass is merged over the defaults. */
|
|
503
|
+
function defineSpecConfig(config = {}) {
|
|
504
|
+
const { literate: literateOptions, ...userConfig } = config;
|
|
505
|
+
return presetConfig(literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig);
|
|
506
|
+
}
|
|
507
|
+
const BUILDERS = /* @__PURE__ */ new Map([
|
|
508
|
+
["unit", unit],
|
|
509
|
+
["lint", lint],
|
|
510
|
+
["cli", cli],
|
|
511
|
+
["api", api],
|
|
512
|
+
["job", job],
|
|
513
|
+
["integration", integration],
|
|
514
|
+
["website", website],
|
|
515
|
+
["mobile", mobile],
|
|
516
|
+
["component", component]
|
|
517
|
+
]);
|
|
518
|
+
const anchored = (root, path) => isAbsolute(path) ? path : resolve(root, path);
|
|
519
|
+
const anchoredTarget = (root, target) => typeof target === "string" && target.startsWith(".") ? anchored(root, target) : target;
|
|
520
|
+
function aliasesFrom(root, vite) {
|
|
521
|
+
if (!isRecord(vite) || !isRecord(vite.resolve)) return vite;
|
|
522
|
+
const stated = vite.resolve.alias;
|
|
523
|
+
if (Array.isArray(stated)) {
|
|
524
|
+
const alias = stated.map((entry) => isRecord(entry) ? {
|
|
525
|
+
...entry,
|
|
526
|
+
replacement: anchoredTarget(root, entry.replacement)
|
|
527
|
+
} : entry);
|
|
528
|
+
return {
|
|
529
|
+
...vite,
|
|
530
|
+
resolve: {
|
|
531
|
+
...vite.resolve,
|
|
532
|
+
alias
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (!isRecord(stated)) return vite;
|
|
537
|
+
const alias = Object.fromEntries(Object.entries(stated).map(([name, target]) => [name, anchoredTarget(root, target)]));
|
|
538
|
+
return {
|
|
539
|
+
...vite,
|
|
540
|
+
resolve: {
|
|
541
|
+
...vite.resolve,
|
|
542
|
+
alias
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function refused(found, fix) {
|
|
547
|
+
return new Error(refusal$1({
|
|
548
|
+
fix,
|
|
549
|
+
found,
|
|
550
|
+
read: "docs/start/set-up-a-project.md#the-vitest-config",
|
|
551
|
+
why: "a project is derived from the tree, and a declared one only states how it departs from that, with its reason"
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
function declaredProjects(declared) {
|
|
555
|
+
if (declared === void 0) return /* @__PURE__ */ new Map();
|
|
556
|
+
if (!isRecord(declared)) throw refused("`test.projects` is not a record", "key each project by its specification: `projects: { cli: { timeout: 120_000, reason: ... } }`");
|
|
557
|
+
const projects = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const [specification, project] of Object.entries(declared)) {
|
|
559
|
+
if (!BUILDERS.has(specification)) throw refused(`\`test.projects\` names the specification \`${specification}\``, `key it by one of ${[...BUILDERS.keys()].join(", ")}`);
|
|
560
|
+
const { reason, ...options } = isRecord(project) ? project : {};
|
|
561
|
+
if (typeof reason !== "string" || reason.trim() === "") throw refused(`\`test.projects.${specification}\` departs from its derived project with no reason`, `state why under \`test.projects.${specification}.reason\`, or delete what the tree already derives`);
|
|
562
|
+
projects.set(specification, options);
|
|
563
|
+
}
|
|
564
|
+
return projects;
|
|
565
|
+
}
|
|
566
|
+
async function projectOf(root, specification, { setup, ...options }) {
|
|
567
|
+
const builder = BUILDERS.get(specification);
|
|
568
|
+
if (builder === void 0) return {};
|
|
569
|
+
const componentRoot = anchored(root, typeof options.root === "string" ? options.root : ".");
|
|
570
|
+
const project = await builder({
|
|
571
|
+
...options,
|
|
572
|
+
...specification === "component" ? { root: componentRoot } : {},
|
|
573
|
+
vite: aliasesFrom(root, options.vite)
|
|
574
|
+
});
|
|
575
|
+
const setups = Array.isArray(setup) ? setup.filter((path) => typeof path === "string") : [];
|
|
576
|
+
return mergeConfig({ test: { globalSetup: setups.map((path) => anchored(root, path)) } }, project);
|
|
577
|
+
}
|
|
578
|
+
async function derivedFrom(root) {
|
|
579
|
+
const { problem, resolved } = await readSettings(root);
|
|
580
|
+
if (problem !== null) throw new Error(refusal$1({
|
|
581
|
+
fix: "correct jterrazz.config.ts as the finding says, then run vitest again",
|
|
582
|
+
found: `jterrazz.config.ts ${problem}`,
|
|
583
|
+
read: "docs/start/set-up-a-project.md#the-vitest-config",
|
|
584
|
+
why: "the test projects are declared there, and a config read without them would run none of them"
|
|
585
|
+
}));
|
|
586
|
+
const section = resolved.sections[TEST_SECTION];
|
|
587
|
+
const declared = declaredProjects(isRecord(section) ? section.projects : void 0);
|
|
588
|
+
const own = resolved.own[TEST_SECTION]?.projects;
|
|
589
|
+
const derived = derivedProjects(root);
|
|
590
|
+
const specifications = [...BUILDERS.keys()].filter((specification) => derived.has(specification) || isRecord(own) && own[specification] !== void 0);
|
|
591
|
+
return presetConfig(specifications.length === 0 ? {} : { test: { projects: await Promise.all(specifications.map(async (specification) => await projectOf(root, specification, {
|
|
592
|
+
...derived.get(specification),
|
|
593
|
+
...declared.get(specification)
|
|
594
|
+
}))) } });
|
|
595
|
+
}
|
|
596
|
+
const vitestConfig = async (configUrl) => await derivedFrom(dirname(fileURLToPath(configUrl)));
|
|
597
|
+
//#endregion
|
|
598
|
+
export { api, cli, component, defineSpecConfig, integration, job, lint, literate, mobile, unit, vitestConfig, website };
|
|
599
|
+
|
|
600
|
+
//# sourceMappingURL=vitest.js.map
|