@jterrazz/test 16.1.1 → 18.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 +1 -2980
- package/dist/browser/services.js +1 -0
- package/dist/browser/stubs.js +1 -0
- package/dist/browser-shared-index.browser.js +3233 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +3721 -2624
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +12 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1641 -3004
- package/dist/index.js +5095 -3835
- package/dist/mobile-session.global-setup.d.ts +15 -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 +3979 -5799
- package/dist/route-coverage.global-setup.d.ts +43 -0
- package/dist/route-coverage.global-setup.js +1 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +1 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +34 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +73 -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.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-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +184 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +22 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +328 -0
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-record-key.js +13 -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 +407 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +174 -0
- package/dist/shared-session-ledger.js +305 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +303 -0
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/shared-stub-reply.js +167 -0
- package/dist/shared-stub-server.js +142 -0
- package/dist/shared-temp-root.global-setup.js +73 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +306 -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 +1 -0
- package/dist/vitest.d.ts +93 -164
- package/dist/vitest.js +382 -550
- 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 +566 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +97 -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 +44 -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 +500 -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 +228 -0
- package/package.json +56 -36
- 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 { 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 { c as SCREENSHOTS_DIR, i as COVERAGE_DIR, n as ATTACHMENTS_DIR, o as NODE_COMPILE_CACHE_DIR, r as BLOB_DIR, s as REPORTER_OUTPUT_FILES, t as ARTIFACTS_DIR, u as VITEST_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 { n 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, slotsFor } from "@jterrazz/typescript/rules";
|
|
5
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import {
|
|
17
|
+
import { 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,147 @@ function onProjectDefaults(stated) {
|
|
|
248
66
|
test: statedOnce(merged.test)
|
|
249
67
|
};
|
|
250
68
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
const PARALLEL = { groupOrder: 0 };
|
|
70
|
+
const SERIAL = { groupOrder: 1 };
|
|
71
|
+
/** A stated `include` or `exclude` replaces the canonical one; a project shares group 0 with every other, a serial one runs after them. */
|
|
72
|
+
function projectTest(name, options, canonical) {
|
|
73
|
+
const exclude = options.exclude ?? canonical.exclude;
|
|
293
74
|
return {
|
|
294
|
-
...
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
75
|
+
...exclude === void 0 ? {} : { exclude },
|
|
76
|
+
include: options.include ?? canonical.include,
|
|
77
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
78
|
+
name,
|
|
79
|
+
sequence: options.serial === true ? SERIAL : PARALLEL,
|
|
80
|
+
...options.timeout === void 0 ? {} : {
|
|
81
|
+
hookTimeout: options.timeout,
|
|
82
|
+
testTimeout: options.timeout
|
|
299
83
|
}
|
|
300
84
|
};
|
|
301
85
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
86
|
+
function specificationProject(specification, options) {
|
|
87
|
+
return onProjectDefaults({ test: projectTest(specification, options, { include: [`specs/${specification}/**/*.spec.ts`] }) });
|
|
88
|
+
}
|
|
89
|
+
/** The modules a stated runner names, one or several. */
|
|
90
|
+
const runnersOf = (runner) => typeof runner === "string" ? [runner] : [...runner];
|
|
91
|
+
function runnerProject(specification, options) {
|
|
92
|
+
const project = specificationProject(specification, options);
|
|
93
|
+
return options.runner === void 0 ? project : mergeConfig(project, { test: {
|
|
94
|
+
isolate: false,
|
|
95
|
+
setupFiles: runnersOf(options.runner)
|
|
313
96
|
} });
|
|
314
97
|
}
|
|
315
98
|
//#endregion
|
|
316
|
-
//#region src/
|
|
317
|
-
/** `api()` — the app met through HTTP. */
|
|
99
|
+
//#region src/specifications/api/api.project.ts
|
|
318
100
|
function api(options = {}) {
|
|
319
|
-
return
|
|
101
|
+
return mergeConfig(runnerProject("api", options), { test: { globalSetup: [servicesSetup(), routeCoverageSetup()] } });
|
|
320
102
|
}
|
|
321
103
|
//#endregion
|
|
322
|
-
//#region src/
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
*/
|
|
104
|
+
//#region src/specifications/cli/cli.project.ts
|
|
105
|
+
function runnerBeside(common, documents) {
|
|
106
|
+
const { runner, ...isolated } = common;
|
|
107
|
+
return runner !== void 0 && runnersOf(runner).some((module) => resolve(module) === resolve(documents)) ? common : isolated;
|
|
108
|
+
}
|
|
109
|
+
/** 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
110
|
function cli(options = {}) {
|
|
333
|
-
const { literate:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
specification: literateOptions?.specification ?? CLI_SPECIFICATION
|
|
111
|
+
const { literate: door, ...common } = options;
|
|
112
|
+
if (door === false) return runnerProject("cli", common);
|
|
113
|
+
const documents = door?.runner ?? "./specs/cli/cli.specification.ts";
|
|
114
|
+
return mergeConfig(runnerProject("cli", runnerBeside(common, documents)), { plugins: [literate({
|
|
115
|
+
include: door?.include ?? ["specs/cli/**/*.spec.yaml"],
|
|
116
|
+
runner: documents
|
|
340
117
|
})] });
|
|
341
118
|
}
|
|
342
119
|
//#endregion
|
|
343
|
-
//#region src/
|
|
344
|
-
/**
|
|
120
|
+
//#region src/stubs/delivery/browser-page/worker-script.ts
|
|
121
|
+
/** 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. */
|
|
122
|
+
function mswWorkerScript() {
|
|
123
|
+
return createRequire(import.meta.url).resolve("msw/mockServiceWorker.js");
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/specifications/component/golden/commands.ts
|
|
127
|
+
function testFile(testPath) {
|
|
128
|
+
if (testPath === void 0) throw invariant("a golden command reached the server with no test path");
|
|
129
|
+
return testPath;
|
|
130
|
+
}
|
|
345
131
|
function goldenPath(testPath, name) {
|
|
346
|
-
|
|
347
|
-
return resolve(dirname(testPath), GROUND_EXPECTED, name);
|
|
132
|
+
return resolveSpecPath("toMatch", name, dirname(testFile(testPath)));
|
|
348
133
|
}
|
|
349
|
-
/** The ARIA snapshot of the rendered `<body>`, as YAML. */
|
|
350
134
|
const ariaTree = async (context) => {
|
|
351
135
|
return await (await context.frame()).locator("body").ariaSnapshot();
|
|
352
136
|
};
|
|
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
137
|
const goldenRead = (context, name) => {
|
|
372
138
|
const path = goldenPath(context.testPath, name);
|
|
373
139
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
374
140
|
};
|
|
375
|
-
/** Write the golden — update mode's half, and the path it wrote, for the message. */
|
|
376
141
|
const goldenWrite = (context, name, content) => {
|
|
377
142
|
const path = goldenPath(context.testPath, name);
|
|
378
143
|
mkdirSync(dirname(path), { recursive: true });
|
|
379
|
-
writeFileSync(path, content
|
|
144
|
+
writeFileSync(path, content);
|
|
380
145
|
return path;
|
|
381
146
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
147
|
+
const inPlaceWrite = (context, stack, text) => {
|
|
148
|
+
const file = testFile(context.testPath);
|
|
149
|
+
const frame = context.project.browser?.parseStacktrace(stack).find((candidate) => candidate.file === file);
|
|
150
|
+
if (frame === void 0) throw new Error(refusal$1({
|
|
151
|
+
fix: "call toMatch() from the test file itself, or compare to a golden file: toMatch(file(...))",
|
|
152
|
+
found: "update mode: no frame of this toMatch call is the test file, so the value in place has nowhere to be written",
|
|
153
|
+
read: "docs/reference/tokens.md#a-value-written-in-place-is-rewritten-in-the-test",
|
|
154
|
+
why: "A value written in place is rewritten in the test that states it."
|
|
155
|
+
}));
|
|
156
|
+
writeInPlace({
|
|
157
|
+
column: frame.column,
|
|
158
|
+
file,
|
|
159
|
+
line: frame.line
|
|
160
|
+
}, (indent) => renderText(text, indent), context.project.getProvidedContext().inPlaceRewrites);
|
|
161
|
+
return `${file}:${frame.line}`;
|
|
162
|
+
};
|
|
163
|
+
/** One list, so the names the page calls and the names the project registers cannot drift. */
|
|
386
164
|
const COMPONENT_COMMANDS = {
|
|
387
|
-
ariaNode,
|
|
388
165
|
ariaTree,
|
|
389
166
|
goldenRead,
|
|
390
167
|
goldenWrite,
|
|
391
|
-
|
|
168
|
+
inPlaceWrite
|
|
392
169
|
};
|
|
393
170
|
//#endregion
|
|
394
|
-
//#region src/
|
|
395
|
-
|
|
171
|
+
//#region src/specifications/component/peers/peer-versions.ts
|
|
172
|
+
const requireFrom$1 = createRequire(import.meta.url);
|
|
173
|
+
function versionOf(specifier) {
|
|
174
|
+
const path = requireFrom$1.resolve(`${specifier}/package.json`);
|
|
175
|
+
const manifest = JSON.parse(String(readFileSync(path)));
|
|
176
|
+
if (typeof manifest === "object" && manifest !== null && "version" in manifest) return String(manifest.version);
|
|
177
|
+
throw new Error(refusal$1({
|
|
178
|
+
fix: `reinstall ${specifier}: \`npm install ${specifier}\``,
|
|
179
|
+
found: `the installed ${specifier} manifest carries no version`,
|
|
180
|
+
read: "docs/start/set-up-a-project.md#what-to-install",
|
|
181
|
+
why: "The component project reads the version of the peers it configures from their installed manifests."
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/** Vite 8 transforms with oxc and warns on every `esbuild` option; Vite 6 and 7 have no `oxc` key. */
|
|
185
|
+
function transformKey() {
|
|
186
|
+
return Number.parseInt(versionOf("vite").split(".")[0] ?? "", 10) >= 8 ? "oxc" : "esbuild";
|
|
187
|
+
}
|
|
188
|
+
/** `@vitest/browser-playwright` peers vitest on an exact version; a mismatch fails deep in the tester naming neither. */
|
|
189
|
+
function assertProviderPin(runner, provider) {
|
|
190
|
+
if (provider === null) throw new Error(refusal$1("seam-is-an-optional-peer", {
|
|
191
|
+
fix: `npm i -D @vitest/browser-playwright@${runner} playwright`,
|
|
192
|
+
found: "component() requires `@vitest/browser-playwright`, and it is not installed"
|
|
193
|
+
}));
|
|
194
|
+
if (runner !== provider) throw new Error(refusal$1({
|
|
195
|
+
fix: `bump both to the same version in one change: npm i -D vitest@${provider} @vitest/browser-playwright@${provider}`,
|
|
196
|
+
found: `component(): @vitest/browser-playwright is ${provider}, and vitest is ${runner}`,
|
|
197
|
+
read: "docs/start/set-up-a-project.md#what-to-install",
|
|
198
|
+
why: "`@vitest/browser-playwright` peers vitest on an exact version."
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
function installedVersion(specifier) {
|
|
202
|
+
try {
|
|
203
|
+
return versionOf(specifier);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/specifications/component/component.project.ts
|
|
396
210
|
const PIPELINE_KEYS = [
|
|
397
211
|
"assetsInclude",
|
|
398
212
|
"css",
|
|
@@ -400,82 +214,20 @@ const PIPELINE_KEYS = [
|
|
|
400
214
|
"envPrefix",
|
|
401
215
|
"esbuild",
|
|
402
216
|
"json",
|
|
217
|
+
"optimizeDeps",
|
|
403
218
|
"oxc",
|
|
404
219
|
"plugins",
|
|
405
220
|
"resolve"
|
|
406
221
|
];
|
|
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 = [
|
|
222
|
+
const PAGE_DEPENDENCIES = [
|
|
416
223
|
"react",
|
|
417
224
|
"react-dom/client",
|
|
418
225
|
"react/jsx-dev-runtime",
|
|
419
226
|
"react/jsx-runtime",
|
|
420
|
-
"vitest-browser-react"
|
|
227
|
+
"vitest-browser-react",
|
|
228
|
+
"vitest-mock-extended"
|
|
421
229
|
];
|
|
422
230
|
const requireFrom = createRequire(import.meta.url);
|
|
423
|
-
/** This module's own file — the frames the caller search walks past. */
|
|
424
|
-
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
|
-
const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
|
|
429
|
-
/** The file of the first frame below this module — who called the helper. */
|
|
430
|
-
function callerFile() {
|
|
431
|
-
const { stack } = /* @__PURE__ */ new Error("@jterrazz/test: reading the call site of a project helper");
|
|
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
|
-
}
|
|
439
|
-
}
|
|
440
|
-
/** The nearest directory at or above the cwd holding a file of this name. */
|
|
441
|
-
function nearestHolding(name) {
|
|
442
|
-
let directory = process.cwd();
|
|
443
|
-
for (;;) {
|
|
444
|
-
if (existsSync(resolve(directory, name))) return directory;
|
|
445
|
-
const parent = dirname(directory);
|
|
446
|
-
if (parent === directory) return;
|
|
447
|
-
directory = parent;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
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
|
-
function callerDirectory() {
|
|
472
|
-
const file = callerFile();
|
|
473
|
-
if (file === void 0) return process.cwd();
|
|
474
|
-
const bundled = BUNDLED_CONFIG.exec(basename(file))?.groups?.name;
|
|
475
|
-
if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
|
|
476
|
-
return nearestHolding(bundled) ?? process.cwd();
|
|
477
|
-
}
|
|
478
|
-
/** Is this specifier installed here? An absent optional peer is simply not pre-bundled. */
|
|
479
231
|
function resolves(specifier) {
|
|
480
232
|
try {
|
|
481
233
|
requireFrom.resolve(specifier);
|
|
@@ -484,20 +236,10 @@ function resolves(specifier) {
|
|
|
484
236
|
return false;
|
|
485
237
|
}
|
|
486
238
|
}
|
|
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
239
|
function mswWorkerPlugin() {
|
|
498
240
|
return {
|
|
499
241
|
configureServer(server) {
|
|
500
|
-
const script =
|
|
242
|
+
const script = mswWorkerScript();
|
|
501
243
|
server.middlewares.use("/mockServiceWorker.js", (_request, response) => {
|
|
502
244
|
response.setHeader("content-type", "text/javascript");
|
|
503
245
|
response.setHeader("service-worker-allowed", "/");
|
|
@@ -507,16 +249,7 @@ function mswWorkerPlugin() {
|
|
|
507
249
|
name: "@jterrazz/test:msw-worker"
|
|
508
250
|
};
|
|
509
251
|
}
|
|
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
|
-
*/
|
|
252
|
+
const isPluginList = (plugins) => Array.isArray(plugins);
|
|
520
253
|
async function reroot(source, root) {
|
|
521
254
|
const environment = {
|
|
522
255
|
command: "serve",
|
|
@@ -524,144 +257,90 @@ async function reroot(source, root) {
|
|
|
524
257
|
mode: "test"
|
|
525
258
|
};
|
|
526
259
|
let loaded;
|
|
527
|
-
let
|
|
260
|
+
let found;
|
|
261
|
+
let fix;
|
|
528
262
|
if (typeof source === "string") {
|
|
529
263
|
const { loadConfigFromFile } = await import("vite");
|
|
530
|
-
from = isAbsolute(source) ? source : resolve(root, source);
|
|
264
|
+
const from = isAbsolute(source) ? source : resolve(root, source);
|
|
531
265
|
loaded = (await loadConfigFromFile(environment, from))?.config;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (
|
|
266
|
+
found = `${from} exports no config`;
|
|
267
|
+
fix = "point `vite` at the app's own vite.config.ts";
|
|
268
|
+
} else if (typeof source === "function") {
|
|
269
|
+
loaded = await source(environment);
|
|
270
|
+
if (typeof loaded === "function") {
|
|
271
|
+
found = "the config function returned a function, not a config";
|
|
272
|
+
fix = "call the framework's factory and pass what it returns (for Astro, `getViteConfig({}, { root: import.meta.dirname })`, never `getViteConfig`)";
|
|
273
|
+
} else {
|
|
274
|
+
found = "the config function returned no config";
|
|
275
|
+
fix = "return the framework's config from the function (for Astro, `getViteConfig({}, { root: import.meta.dirname })`)";
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
loaded = await source;
|
|
279
|
+
found = "the config promise resolved to no config";
|
|
280
|
+
fix = "resolve the promise to the framework's config object";
|
|
281
|
+
}
|
|
282
|
+
if (typeof loaded !== "object" || loaded === null) throw new Error(refusal$1({
|
|
283
|
+
fix,
|
|
284
|
+
found: `component({ vite }): ${found}`,
|
|
285
|
+
read: "docs/guides/component-specs.md#the-constructor-there-is-none",
|
|
286
|
+
why: "A component renders through the Vite pipeline the app builds with, read from its config."
|
|
287
|
+
}));
|
|
535
288
|
const inactive = transformKey() === "oxc" ? "esbuild" : "oxc";
|
|
536
289
|
const kept = {};
|
|
537
290
|
const config = { ...loaded };
|
|
538
|
-
for (const key of PIPELINE_KEYS) if (
|
|
291
|
+
for (const key of PIPELINE_KEYS) if (key !== inactive) kept[key] = config[key];
|
|
539
292
|
return kept;
|
|
540
293
|
}
|
|
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";
|
|
294
|
+
function optimizeDepsOf(declared) {
|
|
295
|
+
const own = isRecord(declared) ? { ...declared } : {};
|
|
296
|
+
const listed = "include" in own && Array.isArray(own.include) ? own.include : [];
|
|
297
|
+
return {
|
|
298
|
+
...own,
|
|
299
|
+
include: [.../* @__PURE__ */ new Set([...PAGE_DEPENDENCIES.filter(resolves), ...listed.filter((entry) => typeof entry === "string")])]
|
|
300
|
+
};
|
|
560
301
|
}
|
|
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
302
|
function jsxPipeline(pipeline) {
|
|
567
303
|
const key = transformKey();
|
|
568
304
|
const stated = pipeline[key];
|
|
569
305
|
if (key === "oxc") return { oxc: stated ?? { jsx: { runtime: "automatic" } } };
|
|
570
306
|
return { esbuild: stated ?? { jsx: "automatic" } };
|
|
571
307
|
}
|
|
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
308
|
function writeSetupFile(wrap, root) {
|
|
611
309
|
const absolute = resolve(root, wrap);
|
|
612
310
|
const path = resolve(root, VITEST_ARTIFACTS_DIR, "component-setup.mjs");
|
|
613
311
|
mkdirSync(dirname(path), { recursive: true });
|
|
614
312
|
writeFileSync(path, [
|
|
615
|
-
"// Generated by `component({ wrap })`
|
|
313
|
+
"// Generated by `component({ wrap })` of @jterrazz/test. Do not edit.",
|
|
616
314
|
`import wrap from ${JSON.stringify(absolute)};`,
|
|
617
315
|
"globalThis[Symbol.for('@jterrazz/test:component-wrap')] = wrap;",
|
|
618
316
|
""
|
|
619
317
|
].join("\n"), "utf8");
|
|
620
318
|
return path;
|
|
621
319
|
}
|
|
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
320
|
function providedToPage(options) {
|
|
627
321
|
return {
|
|
628
322
|
...options.clock === void 0 ? {} : { componentClock: options.clock },
|
|
629
323
|
componentViewport: viewportOf(options),
|
|
630
|
-
update:
|
|
324
|
+
update: shouldUpdateSnapshots()
|
|
631
325
|
};
|
|
632
326
|
}
|
|
633
|
-
/** The page size every render of the project starts at. */
|
|
634
327
|
function viewportOf(options) {
|
|
635
328
|
return options.viewport ?? {
|
|
636
329
|
height: 720,
|
|
637
330
|
width: 1280
|
|
638
331
|
};
|
|
639
332
|
}
|
|
640
|
-
/**
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
*/
|
|
655
|
-
async function component(options = {}) {
|
|
656
|
-
const root = options.root ?? callerDirectory();
|
|
657
|
-
assertProviderPin();
|
|
333
|
+
/** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
|
|
334
|
+
async function component(options) {
|
|
335
|
+
const { root } = options;
|
|
336
|
+
assertProviderPin(versionOf("vitest"), installedVersion("@vitest/browser-playwright"));
|
|
658
337
|
const { playwright } = await import("@vitest/browser-playwright");
|
|
659
338
|
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
660
|
-
const consumerPlugins =
|
|
339
|
+
const consumerPlugins = isPluginList(pipeline.plugins) ? pipeline.plugins : [];
|
|
661
340
|
return onProjectDefaults({
|
|
662
341
|
...pipeline,
|
|
663
342
|
...jsxPipeline(pipeline),
|
|
664
|
-
optimizeDeps:
|
|
343
|
+
optimizeDeps: optimizeDepsOf(pipeline.optimizeDeps),
|
|
665
344
|
plugins: [mswWorkerPlugin(), ...consumerPlugins],
|
|
666
345
|
test: {
|
|
667
346
|
browser: {
|
|
@@ -677,76 +356,229 @@ async function component(options = {}) {
|
|
|
677
356
|
screenshotDirectory: SCREENSHOTS_DIR,
|
|
678
357
|
viewport: viewportOf(options)
|
|
679
358
|
},
|
|
680
|
-
...
|
|
681
|
-
|
|
682
|
-
|
|
359
|
+
...projectTest("component", options, {
|
|
360
|
+
exclude: options.include === void 0 ? ["specs/**"] : [],
|
|
361
|
+
include: ["**/*.test.tsx"]
|
|
362
|
+
}),
|
|
683
363
|
provide: providedToPage(options),
|
|
684
|
-
...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] }
|
|
685
|
-
sequence: { groupOrder: 2 },
|
|
686
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
364
|
+
...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] }
|
|
687
365
|
}
|
|
688
366
|
});
|
|
689
367
|
}
|
|
690
368
|
//#endregion
|
|
691
|
-
//#region src/
|
|
692
|
-
/** `integration()` — a module against real services, or against a golden. */
|
|
369
|
+
//#region src/specifications/integration/integration.project.ts
|
|
693
370
|
function integration(options = {}) {
|
|
694
|
-
return
|
|
371
|
+
return mergeConfig(runnerProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/specifications/job/job.project.ts
|
|
375
|
+
function job(options = {}) {
|
|
376
|
+
return mergeConfig(runnerProject("job", options), { test: { globalSetup: [servicesSetup()] } });
|
|
695
377
|
}
|
|
696
378
|
//#endregion
|
|
697
|
-
//#region src/
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return facetProject("jobs", options);
|
|
379
|
+
//#region src/specifications/lint/lint.project.ts
|
|
380
|
+
function lint(options = {}) {
|
|
381
|
+
return specificationProject("lint", options);
|
|
701
382
|
}
|
|
702
383
|
//#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
|
-
*/
|
|
384
|
+
//#region src/specifications/mobile/mobile.project.ts
|
|
385
|
+
/** A simulator cannot share a machine with itself: serial, unless `serial: false` states a server that gives every session an app of its own. A global setup holds the run's driver sessions, one per worker. */
|
|
708
386
|
function mobile(options = {}) {
|
|
709
|
-
|
|
387
|
+
const { device, root, ...common } = options;
|
|
388
|
+
return mergeConfig(specificationProject("mobile", {
|
|
389
|
+
...common,
|
|
390
|
+
serial: common.serial !== false
|
|
391
|
+
}), { test: {
|
|
392
|
+
globalSetup: [mobileSessionSetup()],
|
|
393
|
+
...device === void 0 ? {} : { provide: { mobileAppium: {
|
|
394
|
+
device,
|
|
395
|
+
root: root ?? "."
|
|
396
|
+
} } }
|
|
397
|
+
} });
|
|
710
398
|
}
|
|
711
399
|
//#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
|
-
*/
|
|
400
|
+
//#region src/specifications/website/website.project.ts
|
|
719
401
|
function website(options = {}) {
|
|
720
|
-
return
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
402
|
+
return runnerProject("website", options);
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/vitest/preset.ts
|
|
406
|
+
const COVERAGE = {
|
|
407
|
+
exclude: [
|
|
408
|
+
"**/node_modules/**",
|
|
409
|
+
"specs/**",
|
|
410
|
+
GROUND_GLOB,
|
|
411
|
+
"**/*.d.ts",
|
|
412
|
+
"**/*.test-d.ts",
|
|
413
|
+
"**/*.fixtures.ts",
|
|
414
|
+
".artifacts/**",
|
|
415
|
+
"**/*.config.ts",
|
|
416
|
+
"**/*.config.mts"
|
|
417
|
+
],
|
|
418
|
+
autoAttachSubprocess: true,
|
|
419
|
+
excludeAfterRemap: true,
|
|
420
|
+
provider: "v8",
|
|
421
|
+
reporter: [
|
|
422
|
+
"text",
|
|
423
|
+
"html",
|
|
424
|
+
"json-summary"
|
|
425
|
+
],
|
|
426
|
+
reportsDirectory: COVERAGE_DIR
|
|
427
|
+
};
|
|
428
|
+
function shardOf(argv) {
|
|
429
|
+
for (const [index, argument] of argv.entries()) {
|
|
430
|
+
if (argument.startsWith("--shard=")) return argument.slice(8);
|
|
431
|
+
if (argument === "--shard") return argv[index + 1];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function reporterOutputFiles(argv) {
|
|
435
|
+
const shard = shardOf(argv);
|
|
436
|
+
return shard === void 0 ? REPORTER_OUTPUT_FILES : {
|
|
437
|
+
...REPORTER_OUTPUT_FILES,
|
|
438
|
+
blob: `${BLOB_DIR}/blob-${shard.replace("/", "-")}.json`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function rootDefaults() {
|
|
442
|
+
return mergeConfig(projectDefaults(), { test: {
|
|
443
|
+
coverage: COVERAGE,
|
|
444
|
+
env: { NODE_COMPILE_CACHE: resolve(NODE_COMPILE_CACHE_DIR) },
|
|
445
|
+
maxWorkers: slotsFor(resolve("."), "vitest"),
|
|
446
|
+
outputFile: reporterOutputFiles(process.argv)
|
|
727
447
|
} });
|
|
728
448
|
}
|
|
449
|
+
function isInline(project) {
|
|
450
|
+
return isRecord(project) && !("then" in project);
|
|
451
|
+
}
|
|
452
|
+
function rootExcludeBeside(projects, exclude) {
|
|
453
|
+
return projects.every(isInline) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
|
|
454
|
+
}
|
|
455
|
+
function withProjectDefaults(project) {
|
|
456
|
+
return isInline(project) ? onProjectDefaults(project) : project;
|
|
457
|
+
}
|
|
458
|
+
function presetConfig(stated = {}) {
|
|
459
|
+
const merged = mergeConfig(rootDefaults(), stated);
|
|
460
|
+
const root = statedOnce(merged.test ?? {});
|
|
461
|
+
const { projects } = root;
|
|
462
|
+
if (projects === void 0) return {
|
|
463
|
+
...merged,
|
|
464
|
+
test: root
|
|
465
|
+
};
|
|
466
|
+
const inline = projects.map(withProjectDefaults);
|
|
467
|
+
return {
|
|
468
|
+
...merged,
|
|
469
|
+
test: {
|
|
470
|
+
...root,
|
|
471
|
+
...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
|
|
472
|
+
projects: inline
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
729
476
|
//#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
|
-
*/
|
|
477
|
+
//#region src/vitest/unit.project.ts
|
|
478
|
+
/** `.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
479
|
function unit(options = {}) {
|
|
741
480
|
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
742
481
|
return onProjectDefaults({ test: {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
482
|
+
...projectTest("unit", options, {
|
|
483
|
+
exclude: fromRoots === void 0 ? ["specs/**", "**/*.test.tsx"] : ["**/*.test.tsx"],
|
|
484
|
+
include: fromRoots ?? ["**/*.test.ts"]
|
|
485
|
+
}),
|
|
486
|
+
...options.isolate === void 0 ? {} : { isolate: options.isolate }
|
|
749
487
|
} });
|
|
750
488
|
}
|
|
751
489
|
//#endregion
|
|
752
|
-
|
|
490
|
+
//#region src/index.vitest.ts
|
|
491
|
+
const BUILDERS = /* @__PURE__ */ new Map([
|
|
492
|
+
["unit", unit],
|
|
493
|
+
["lint", lint],
|
|
494
|
+
["cli", cli],
|
|
495
|
+
["api", api],
|
|
496
|
+
["job", job],
|
|
497
|
+
["integration", integration],
|
|
498
|
+
["website", website],
|
|
499
|
+
["mobile", mobile],
|
|
500
|
+
["component", component]
|
|
501
|
+
]);
|
|
502
|
+
const anchored = (root, path) => isAbsolute(path) ? path : resolve(root, path);
|
|
503
|
+
const anchoredTarget = (root, target) => typeof target === "string" && target.startsWith(".") ? anchored(root, target) : target;
|
|
504
|
+
function aliasesFrom(root, vite) {
|
|
505
|
+
if (!isRecord(vite) || !isRecord(vite.resolve)) return vite;
|
|
506
|
+
const stated = vite.resolve.alias;
|
|
507
|
+
if (Array.isArray(stated)) {
|
|
508
|
+
const alias = stated.map((entry) => isRecord(entry) ? {
|
|
509
|
+
...entry,
|
|
510
|
+
replacement: anchoredTarget(root, entry.replacement)
|
|
511
|
+
} : entry);
|
|
512
|
+
return {
|
|
513
|
+
...vite,
|
|
514
|
+
resolve: {
|
|
515
|
+
...vite.resolve,
|
|
516
|
+
alias
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (!isRecord(stated)) return vite;
|
|
521
|
+
const alias = Object.fromEntries(Object.entries(stated).map(([name, target]) => [name, anchoredTarget(root, target)]));
|
|
522
|
+
return {
|
|
523
|
+
...vite,
|
|
524
|
+
resolve: {
|
|
525
|
+
...vite.resolve,
|
|
526
|
+
alias
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function refused(found, fix) {
|
|
531
|
+
return new Error(refusal$1({
|
|
532
|
+
fix,
|
|
533
|
+
found,
|
|
534
|
+
read: "docs/start/set-up-a-project.md#the-vitest-config",
|
|
535
|
+
why: "a project is derived from the tree, and a declared one only states how it departs from that, with its reason"
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
function declaredProjects(declared) {
|
|
539
|
+
if (declared === void 0) return /* @__PURE__ */ new Map();
|
|
540
|
+
if (!isRecord(declared)) throw refused("`test.projects` is not a record", "key each project by its specification: `projects: { cli: { timeout: 120_000, reason: ... } }`");
|
|
541
|
+
const projects = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const [specification, project] of Object.entries(declared)) {
|
|
543
|
+
if (!BUILDERS.has(specification)) throw refused(`\`test.projects\` names the specification \`${specification}\``, `key it by one of ${[...BUILDERS.keys()].join(", ")}`);
|
|
544
|
+
const { reason, ...options } = isRecord(project) ? project : {};
|
|
545
|
+
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`);
|
|
546
|
+
projects.set(specification, options);
|
|
547
|
+
}
|
|
548
|
+
return projects;
|
|
549
|
+
}
|
|
550
|
+
async function projectOf(root, specification, { setup, ...options }) {
|
|
551
|
+
const builder = BUILDERS.get(specification);
|
|
552
|
+
if (builder === void 0) return {};
|
|
553
|
+
const componentRoot = anchored(root, typeof options.root === "string" ? options.root : ".");
|
|
554
|
+
const project = await builder({
|
|
555
|
+
...options,
|
|
556
|
+
...specification === "component" ? { root: componentRoot } : {},
|
|
557
|
+
vite: aliasesFrom(root, options.vite)
|
|
558
|
+
});
|
|
559
|
+
const setups = Array.isArray(setup) ? setup.filter((path) => typeof path === "string") : [];
|
|
560
|
+
return mergeConfig({ test: { globalSetup: setups.map((path) => anchored(root, path)) } }, project);
|
|
561
|
+
}
|
|
562
|
+
async function derivedFrom(root) {
|
|
563
|
+
const { problem, resolved } = await readSettings(root);
|
|
564
|
+
if (problem !== null) throw new Error(refusal$1({
|
|
565
|
+
fix: "correct jterrazz.config.ts as the finding says, then run vitest again",
|
|
566
|
+
found: `jterrazz.config.ts ${problem}`,
|
|
567
|
+
read: "docs/start/set-up-a-project.md#the-vitest-config",
|
|
568
|
+
why: "the test projects are declared there, and a config read without them would run none of them"
|
|
569
|
+
}));
|
|
570
|
+
const section = resolved.sections[TEST_SECTION];
|
|
571
|
+
const declared = declaredProjects(isRecord(section) ? section.projects : void 0);
|
|
572
|
+
const own = resolved.own[TEST_SECTION]?.projects;
|
|
573
|
+
const derived = derivedProjects(root);
|
|
574
|
+
const specifications = [...BUILDERS.keys()].filter((specification) => derived.has(specification) || isRecord(own) && own[specification] !== void 0);
|
|
575
|
+
return presetConfig(specifications.length === 0 ? {} : { test: { projects: await Promise.all(specifications.map(async (specification) => await projectOf(root, specification, {
|
|
576
|
+
...derived.get(specification),
|
|
577
|
+
...declared.get(specification)
|
|
578
|
+
}))) } });
|
|
579
|
+
}
|
|
580
|
+
const vitestConfig = async (configUrl) => await derivedFrom(dirname(fileURLToPath(configUrl)));
|
|
581
|
+
//#endregion
|
|
582
|
+
export { api, cli, component, integration, job, lint, literate, mobile, unit, vitestConfig, website };
|
|
583
|
+
|
|
584
|
+
//# sourceMappingURL=vitest.js.map
|