@jterrazz/test 16.1.0 → 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 -2867
- 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 +4551 -2612
- 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 -3002
- 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 +6073 -5616
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6065 -5609
- 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 +374 -507
- 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 -349
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -244
- 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 -242
- 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 -305
- package/docs/19-linting.md +0 -510
- 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,132 +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
|
-
/** The golden's current content, or `null` when it does not exist yet. */
|
|
354
129
|
const goldenRead = (context, name) => {
|
|
355
130
|
const path = goldenPath(context.testPath, name);
|
|
356
131
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
357
132
|
};
|
|
358
|
-
/** Write the golden — update mode's half, and the path it wrote, for the message. */
|
|
359
133
|
const goldenWrite = (context, name, content) => {
|
|
360
134
|
const path = goldenPath(context.testPath, name);
|
|
361
135
|
mkdirSync(dirname(path), { recursive: true });
|
|
362
|
-
writeFileSync(path, content
|
|
136
|
+
writeFileSync(path, content);
|
|
363
137
|
return path;
|
|
364
138
|
};
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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. */
|
|
369
156
|
const COMPONENT_COMMANDS = {
|
|
370
157
|
ariaTree,
|
|
371
158
|
goldenRead,
|
|
372
|
-
goldenWrite
|
|
159
|
+
goldenWrite,
|
|
160
|
+
inPlaceWrite
|
|
373
161
|
};
|
|
374
162
|
//#endregion
|
|
375
|
-
//#region src/
|
|
376
|
-
|
|
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
|
|
377
202
|
const PIPELINE_KEYS = [
|
|
378
203
|
"assetsInclude",
|
|
379
204
|
"css",
|
|
@@ -381,44 +206,25 @@ const PIPELINE_KEYS = [
|
|
|
381
206
|
"envPrefix",
|
|
382
207
|
"esbuild",
|
|
383
208
|
"json",
|
|
209
|
+
"optimizeDeps",
|
|
384
210
|
"oxc",
|
|
385
211
|
"plugins",
|
|
386
212
|
"resolve"
|
|
387
213
|
];
|
|
388
|
-
|
|
389
|
-
* The seam's own runtime, pre-bundled so a cold cache never reloads mid-run.
|
|
390
|
-
*
|
|
391
|
-
* `msw` and `msw/browser` are NOT here: `@vitest/browser` already puts both in
|
|
392
|
-
* `optimizeDeps.exclude`, and Vite's esbuild optimizer (6 and 7) treats an
|
|
393
|
-
* entry that is both included and excluded as fatal — dependency optimisation
|
|
394
|
-
* throws before a single test runs.
|
|
395
|
-
*/
|
|
396
|
-
const SEAM_DEPENDENCIES = [
|
|
214
|
+
const PAGE_DEPENDENCIES = [
|
|
397
215
|
"react",
|
|
398
216
|
"react-dom/client",
|
|
399
217
|
"react/jsx-dev-runtime",
|
|
400
218
|
"react/jsx-runtime",
|
|
401
|
-
"vitest-browser-react"
|
|
219
|
+
"vitest-browser-react",
|
|
220
|
+
"vitest-mock-extended"
|
|
402
221
|
];
|
|
403
222
|
const requireFrom = createRequire(import.meta.url);
|
|
404
|
-
/** This module's own file — the frames the caller search walks past. */
|
|
405
223
|
const HELPERS_FILE = import.meta.filename;
|
|
406
|
-
/** A stack frame's file, in either of the two shapes V8 prints it in. */
|
|
407
|
-
const STACK_FRAME = /(?:\((?<wrapped>[^()]+):\d+:\d+\)|at (?<bare>[^()\s]+):\d+:\d+)$/u;
|
|
408
|
-
/** A config vite bundled before importing it — `<its name>.timestamp-<hash>.mjs`. */
|
|
409
224
|
const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
|
|
410
|
-
/** The file of the first frame below this module — who called the helper. */
|
|
411
225
|
function callerFile() {
|
|
412
|
-
|
|
413
|
-
for (const frame of stack?.split("\n").slice(1) ?? []) {
|
|
414
|
-
const groups = STACK_FRAME.exec(frame.trim())?.groups;
|
|
415
|
-
const location = groups?.wrapped ?? groups?.bare;
|
|
416
|
-
if (location === void 0 || location.startsWith("node:")) continue;
|
|
417
|
-
const file = location.startsWith("file:") ? fileURLToPath(location) : location;
|
|
418
|
-
if (file !== HELPERS_FILE) return file;
|
|
419
|
-
}
|
|
226
|
+
return callerFrames().find(({ file }) => file !== HELPERS_FILE)?.file;
|
|
420
227
|
}
|
|
421
|
-
/** The nearest directory at or above the cwd holding a file of this name. */
|
|
422
228
|
function nearestHolding(name) {
|
|
423
229
|
let directory = process.cwd();
|
|
424
230
|
for (;;) {
|
|
@@ -428,27 +234,6 @@ function nearestHolding(name) {
|
|
|
428
234
|
directory = parent;
|
|
429
235
|
}
|
|
430
236
|
}
|
|
431
|
-
/**
|
|
432
|
-
* The directory of the CONFIG that called a helper — what the relative paths
|
|
433
|
-
* it was handed mean.
|
|
434
|
-
*
|
|
435
|
-
* Not `process.cwd()`: a `vitest.config.ts` is read by more tools than the
|
|
436
|
-
* runner, and knip and `typescript check` load every config of a repository
|
|
437
|
-
* from the repository ROOT — `component({ vite: './web/vite.config.ts' })`
|
|
438
|
-
* written beside an app resolved against the wrong tree and threw before a
|
|
439
|
-
* single test ran. `import.meta` cannot answer either: it describes THIS
|
|
440
|
-
* module. And vitest hands a project neither its root nor its config file — a
|
|
441
|
-
* project function is called with `{ command, mode, isPreview, isSsrBuild }`
|
|
442
|
-
* and nothing else.
|
|
443
|
-
*
|
|
444
|
-
* So the call site is read off the stack, where a loader that keeps a config's
|
|
445
|
-
* identity (node, tsx, jiti — what those tools use) names the file itself.
|
|
446
|
-
* Vite's own loader does not: it BUNDLES the config into
|
|
447
|
-
* `node_modules/.vite-temp/` and only the config's NAME survives the move. The
|
|
448
|
-
* name is what is then looked for upward from the cwd, because that loader is
|
|
449
|
-
* the runner's and a runner is started in the tree it tests. A config loaded
|
|
450
|
-
* by the runner from another tree entirely states `root` itself.
|
|
451
|
-
*/
|
|
452
237
|
function callerDirectory() {
|
|
453
238
|
const file = callerFile();
|
|
454
239
|
if (file === void 0) return process.cwd();
|
|
@@ -456,7 +241,6 @@ function callerDirectory() {
|
|
|
456
241
|
if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
|
|
457
242
|
return nearestHolding(bundled) ?? process.cwd();
|
|
458
243
|
}
|
|
459
|
-
/** Is this specifier installed here? An absent optional peer is simply not pre-bundled. */
|
|
460
244
|
function resolves(specifier) {
|
|
461
245
|
try {
|
|
462
246
|
requireFrom.resolve(specifier);
|
|
@@ -465,20 +249,10 @@ function resolves(specifier) {
|
|
|
465
249
|
return false;
|
|
466
250
|
}
|
|
467
251
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Serve msw's worker script from the package's OWN install, at the path the
|
|
470
|
-
* worker asks for, with the scope header a worker registered at `/` needs.
|
|
471
|
-
*
|
|
472
|
-
* `@vitest/browser` resolves `/mockServiceWorker.js` too, but from ITS own
|
|
473
|
-
* directory — and it does not depend on `msw`, so that resolution only lands
|
|
474
|
-
* where the installer hoists. The package that OWNS the dependency is the one
|
|
475
|
-
* that can always find it, and it is this one. Without either, a consumer runs
|
|
476
|
-
* `msw init` and declares `msw`, which F8 forbids.
|
|
477
|
-
*/
|
|
478
252
|
function mswWorkerPlugin() {
|
|
479
253
|
return {
|
|
480
254
|
configureServer(server) {
|
|
481
|
-
const script =
|
|
255
|
+
const script = mswWorkerScript();
|
|
482
256
|
server.middlewares.use("/mockServiceWorker.js", (_request, response) => {
|
|
483
257
|
response.setHeader("content-type", "text/javascript");
|
|
484
258
|
response.setHeader("service-worker-allowed", "/");
|
|
@@ -488,16 +262,7 @@ function mswWorkerPlugin() {
|
|
|
488
262
|
name: "@jterrazz/test:msw-worker"
|
|
489
263
|
};
|
|
490
264
|
}
|
|
491
|
-
|
|
492
|
-
* Keep an app's Vite PIPELINE and drop everything that says where the app
|
|
493
|
-
* lives.
|
|
494
|
-
*
|
|
495
|
-
* An allow-list, not a drop-list: a consumer's config carries keys the
|
|
496
|
-
* framework has never heard of — `environments`, `ssr`, `appType`, `publicDir`
|
|
497
|
-
* — and one of them re-roots the run and the project collects nothing. Naming
|
|
498
|
-
* what the pipeline IS keeps an unknown key from leaking in. The consumer's
|
|
499
|
-
* `plugins` are CONCATENATED after the seam's, never assigned over them.
|
|
500
|
-
*/
|
|
265
|
+
const isPluginList = (plugins) => Array.isArray(plugins);
|
|
501
266
|
async function reroot(source, root) {
|
|
502
267
|
const environment = {
|
|
503
268
|
command: "serve",
|
|
@@ -505,144 +270,90 @@ async function reroot(source, root) {
|
|
|
505
270
|
mode: "test"
|
|
506
271
|
};
|
|
507
272
|
let loaded;
|
|
508
|
-
let
|
|
273
|
+
let found;
|
|
274
|
+
let fix;
|
|
509
275
|
if (typeof source === "string") {
|
|
510
276
|
const { loadConfigFromFile } = await import("vite");
|
|
511
|
-
from = isAbsolute(source) ? source : resolve(root, source);
|
|
277
|
+
const from = isAbsolute(source) ? source : resolve(root, source);
|
|
512
278
|
loaded = (await loadConfigFromFile(environment, from))?.config;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
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
|
+
}));
|
|
516
301
|
const inactive = transformKey() === "oxc" ? "esbuild" : "oxc";
|
|
517
302
|
const kept = {};
|
|
518
303
|
const config = { ...loaded };
|
|
519
|
-
for (const key of PIPELINE_KEYS) if (
|
|
304
|
+
for (const key of PIPELINE_KEYS) if (key !== inactive) kept[key] = config[key];
|
|
520
305
|
return kept;
|
|
521
306
|
}
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Which of the two transformer keys the INSTALLED Vite actually reads.
|
|
532
|
-
*
|
|
533
|
-
* Vite 8 transforms with oxc and prints a warning for every `esbuild` option it
|
|
534
|
-
* is handed; Vite 6 and 7 transform with esbuild and have no `oxc` key at all.
|
|
535
|
-
* The inactive one is dropped rather than carried as a second, ignored
|
|
536
|
-
* statement — a config that says the same thing twice is a config that will one
|
|
537
|
-
* day say two different things.
|
|
538
|
-
*/
|
|
539
|
-
function transformKey() {
|
|
540
|
-
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
|
+
};
|
|
541
314
|
}
|
|
542
|
-
/**
|
|
543
|
-
* The JSX default, on the key the running Vite reads, yielding to the
|
|
544
|
-
* consumer's own: a `jsxImportSource` or a classic runtime is the app's
|
|
545
|
-
* statement, not the seam's to overwrite.
|
|
546
|
-
*/
|
|
547
315
|
function jsxPipeline(pipeline) {
|
|
548
316
|
const key = transformKey();
|
|
549
317
|
const stated = pipeline[key];
|
|
550
318
|
if (key === "oxc") return { oxc: stated ?? { jsx: { runtime: "automatic" } } };
|
|
551
319
|
return { esbuild: stated ?? { jsx: "automatic" } };
|
|
552
320
|
}
|
|
553
|
-
/**
|
|
554
|
-
* `@vitest/browser-playwright` peers vitest on an EXACT version: 4.1.10 wants
|
|
555
|
-
* 4.1.10, not `^4.1`. A mismatch fails deep inside the tester with a message
|
|
556
|
-
* about a missing runner, so the pair is checked where the project is built
|
|
557
|
-
* and both versions are named.
|
|
558
|
-
*/
|
|
559
|
-
function assertProviderPin() {
|
|
560
|
-
const runner = versionOf("vitest");
|
|
561
|
-
let provider;
|
|
562
|
-
try {
|
|
563
|
-
provider = versionOf("@vitest/browser-playwright");
|
|
564
|
-
} catch {
|
|
565
|
-
throw new Error(PROVIDER_MISSING);
|
|
566
|
-
}
|
|
567
|
-
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}).`);
|
|
568
|
-
}
|
|
569
|
-
/** The framework's own update flag, read where `process` exists. */
|
|
570
|
-
function updating() {
|
|
571
|
-
return process.env.TEST_UPDATE === "1" || process.argv.includes("-u") || process.argv.includes("--update");
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Hand the project's `wrap` to the page as a STATIC import.
|
|
575
|
-
*
|
|
576
|
-
* The page could fetch the module by URL when the first render needs it, and
|
|
577
|
-
* that is what breaks: a dependency Vite never scanned is discovered mid-run,
|
|
578
|
-
* the optimizer rewrites its cache, and every module the page is holding —
|
|
579
|
-
* the test's own included — 404s behind a stale `?v=` hash. A setup file is in
|
|
580
|
-
* the graph before the first test runs, so the wrap and everything it imports
|
|
581
|
-
* are scanned, pre-bundled, and never re-optimised under the run.
|
|
582
|
-
*
|
|
583
|
-
* It is written into `.artifacts/` rather than served from memory because
|
|
584
|
-
* vitest resolves `setupFiles` as paths on disk; it is generated, so it belongs
|
|
585
|
-
* with everything else a tool generates, and never on the row.
|
|
586
|
-
*
|
|
587
|
-
* The wrapper lands on `globalThis` under a shared symbol rather than an
|
|
588
|
-
* export: the setup module and the package's browser build are separate graphs
|
|
589
|
-
* in the page and share no module instance.
|
|
590
|
-
*/
|
|
591
321
|
function writeSetupFile(wrap, root) {
|
|
592
322
|
const absolute = resolve(root, wrap);
|
|
593
323
|
const path = resolve(root, VITEST_ARTIFACTS_DIR, "component-setup.mjs");
|
|
594
324
|
mkdirSync(dirname(path), { recursive: true });
|
|
595
325
|
writeFileSync(path, [
|
|
596
|
-
"// Generated by `component({ wrap })`
|
|
326
|
+
"// Generated by `component({ wrap })` of @jterrazz/test. Do not edit.",
|
|
597
327
|
`import wrap from ${JSON.stringify(absolute)};`,
|
|
598
328
|
"globalThis[Symbol.for('@jterrazz/test:component-wrap')] = wrap;",
|
|
599
329
|
""
|
|
600
330
|
].join("\n"), "utf8");
|
|
601
331
|
return path;
|
|
602
332
|
}
|
|
603
|
-
/**
|
|
604
|
-
* What the page cannot read for itself: update mode is a `process.env`/`argv`
|
|
605
|
-
* read, and the project's clock is stated where `process` exists.
|
|
606
|
-
*/
|
|
607
333
|
function providedToPage(options) {
|
|
608
334
|
return {
|
|
609
335
|
...options.clock === void 0 ? {} : { componentClock: options.clock },
|
|
610
336
|
componentViewport: viewportOf(options),
|
|
611
|
-
update:
|
|
337
|
+
update: shouldUpdateSnapshots()
|
|
612
338
|
};
|
|
613
339
|
}
|
|
614
|
-
/** The page size every render of the project starts at. */
|
|
615
340
|
function viewportOf(options) {
|
|
616
341
|
return options.viewport ?? {
|
|
617
342
|
height: 720,
|
|
618
343
|
width: 1280
|
|
619
344
|
};
|
|
620
345
|
}
|
|
621
|
-
/**
|
|
622
|
-
function componentGlobs(options) {
|
|
623
|
-
return {
|
|
624
|
-
exclude: options.exclude ?? (options.include === void 0 ? ["specs/**"] : []),
|
|
625
|
-
include: options.include ?? ["**/*.test.tsx"]
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* A component test renders in a real Chromium — the browser the website facet
|
|
630
|
-
* already drives through the same `playwright` peer.
|
|
631
|
-
*
|
|
632
|
-
* The default `include` reaches the whole project, so the default `exclude`
|
|
633
|
-
* keeps it out of `specs/`, where the assembled product is specified. State
|
|
634
|
-
* your own `include` and the exclusion is yours to state too.
|
|
635
|
-
*/
|
|
346
|
+
/** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
|
|
636
347
|
async function component(options = {}) {
|
|
637
348
|
const root = options.root ?? callerDirectory();
|
|
638
|
-
assertProviderPin();
|
|
349
|
+
assertProviderPin(versionOf("vitest"), installedVersion("@vitest/browser-playwright"));
|
|
639
350
|
const { playwright } = await import("@vitest/browser-playwright");
|
|
640
351
|
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
641
|
-
const consumerPlugins =
|
|
352
|
+
const consumerPlugins = isPluginList(pipeline.plugins) ? pipeline.plugins : [];
|
|
642
353
|
return onProjectDefaults({
|
|
643
354
|
...pipeline,
|
|
644
355
|
...jsxPipeline(pipeline),
|
|
645
|
-
optimizeDeps:
|
|
356
|
+
optimizeDeps: optimizeDepsOf(pipeline.optimizeDeps),
|
|
646
357
|
plugins: [mswWorkerPlugin(), ...consumerPlugins],
|
|
647
358
|
test: {
|
|
648
359
|
browser: {
|
|
@@ -658,76 +369,232 @@ async function component(options = {}) {
|
|
|
658
369
|
screenshotDirectory: SCREENSHOTS_DIR,
|
|
659
370
|
viewport: viewportOf(options)
|
|
660
371
|
},
|
|
661
|
-
...
|
|
662
|
-
|
|
663
|
-
|
|
372
|
+
...projectTest("component", options, {
|
|
373
|
+
exclude: options.include === void 0 ? ["specs/**"] : [],
|
|
374
|
+
include: ["**/*.test.tsx"]
|
|
375
|
+
}),
|
|
664
376
|
provide: providedToPage(options),
|
|
665
377
|
...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] },
|
|
666
|
-
sequence: { groupOrder: 2 }
|
|
667
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
378
|
+
sequence: { groupOrder: 2 }
|
|
668
379
|
}
|
|
669
380
|
});
|
|
670
381
|
}
|
|
671
382
|
//#endregion
|
|
672
|
-
//#region src/
|
|
673
|
-
/** `integration()` — a module against real services, or against a golden. */
|
|
383
|
+
//#region src/specifications/integration/integration.project.ts
|
|
674
384
|
function integration(options = {}) {
|
|
675
|
-
return
|
|
385
|
+
return mergeConfig(inProcessProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
|
|
676
386
|
}
|
|
677
387
|
//#endregion
|
|
678
|
-
//#region src/
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return facetProject("jobs", options);
|
|
388
|
+
//#region src/specifications/job/job.project.ts
|
|
389
|
+
function job(options = {}) {
|
|
390
|
+
return mergeConfig(inProcessProject("job", options), { test: { globalSetup: [servicesSetup()] } });
|
|
682
391
|
}
|
|
683
392
|
//#endregion
|
|
684
|
-
//#region src/
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
393
|
+
//#region src/specifications/lint/lint.project.ts
|
|
394
|
+
function lint(options = {}) {
|
|
395
|
+
return specificationProject("lint", options);
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
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. */
|
|
689
400
|
function mobile(options = {}) {
|
|
690
|
-
|
|
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
|
+
} });
|
|
691
411
|
}
|
|
692
412
|
//#endregion
|
|
693
|
-
//#region src/
|
|
694
|
-
/**
|
|
695
|
-
* A website spec meets the assembled product through a served page, in the
|
|
696
|
-
* Chromium playwright drives. It opens a browser, so it is scheduled before the
|
|
697
|
-
* component project and never beside it: two Chromiums must not share a slot on
|
|
698
|
-
* a 2-vCPU runner.
|
|
699
|
-
*/
|
|
413
|
+
//#region src/specifications/website/website.project.ts
|
|
700
414
|
function website(options = {}) {
|
|
701
|
-
return
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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)
|
|
708
458
|
} });
|
|
709
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
|
+
}
|
|
710
487
|
//#endregion
|
|
711
|
-
//#region src/
|
|
712
|
-
/**
|
|
713
|
-
* A module test runs beside the module it covers: no DOM, no browser, no
|
|
714
|
-
* pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
|
|
715
|
-
* the assembled product is specified in `.spec.ts` — the two exclusions say
|
|
716
|
-
* exactly that. `.test.tsx` is the third exclusion because a rendered thing is
|
|
717
|
-
* a component test: a project's dependency scan reads every file its globs
|
|
718
|
-
* match, and a `.tsx` collected here would be optimised for a page that never
|
|
719
|
-
* opens.
|
|
720
|
-
*/
|
|
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. */
|
|
721
490
|
function unit(options = {}) {
|
|
722
491
|
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
723
492
|
return onProjectDefaults({ test: {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
...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 }
|
|
730
498
|
} });
|
|
731
499
|
}
|
|
732
500
|
//#endregion
|
|
733
|
-
|
|
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
|