@lwc/vitest-preset 0.0.0-stage → 19.7.0-alpha.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 +104 -2
- package/package.json +54 -5
- package/src/aria-reflection-polyfill.js +82 -0
- package/src/index.js +165 -0
- package/src/lwc.js +140 -0
- package/src/matchers/expect-throw-in-connected-callback.js +100 -0
- package/src/scope-tokens.js +58 -0
- package/src/setup.js +39 -0
- package/src/typescript.js +39 -0
- package/types/index.d.ts +57 -0
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lwc/vitest-preset
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vitest configuration and setup for testing Lightning Web Components (LWC). It is the Vitest counterpart to [`@lwc/jest-preset`](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md).
|
|
4
|
+
|
|
5
|
+
`lwcVitestConfig()` returns a Vitest config that:
|
|
6
|
+
|
|
7
|
+
- compiles component `.js`, `.ts`, `.html` and `.css` files with `@lwc/rollup-plugin`, using the same compiler options as `@lwc/jest-transformer`
|
|
8
|
+
- mocks `@salesforce/*` scoped imports with `@lwc/vitest-plugin`
|
|
9
|
+
- runs tests in jsdom at `http://localhost/`, as Jest does
|
|
10
|
+
- loads synthetic shadow DOM, the LWC engine and the custom matchers before each test file
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Installation
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
yarn add --dev @lwc/vitest-preset vitest jsdom @lwc/engine-dom @lwc/synthetic-shadow @lwc/rollup-plugin @lwc/wire-service
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Install `@lwc/rollup-plugin` at the same version as `@lwc/engine-dom`. Otherwise the engine warns that components were compiled by a different compiler version.
|
|
21
|
+
- `@lwc/rollup-plugin` looks up `@lwc/wire-service` whenever it resolves a namespace import such as `x/child`, so install it even if you don't use it.
|
|
22
|
+
- Requires Node 20+, Vitest 4.1+ and LWC 9.1.1+.
|
|
23
|
+
|
|
24
|
+
### Configuration
|
|
25
|
+
|
|
26
|
+
Create a `vitest.config.mjs`:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { defineConfig } from 'vitest/config';
|
|
30
|
+
import { lwcVitestConfig } from '@lwc/vitest-preset';
|
|
31
|
+
|
|
32
|
+
export default defineConfig(lwcVitestConfig(import.meta.url));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`import.meta.url` makes the config's directory the project root. Tests are picked up from `**/__tests__/**/*.vitest.test.{js,ts}`.
|
|
36
|
+
|
|
37
|
+
Each test file runs in its own forked process with a fresh module registry (`pool: 'forks'`, `isolate: true`), as in Jest.
|
|
38
|
+
|
|
39
|
+
Setting `isolate: false` (or a pool other than `forks`) in your config overrides this and is **not supported**: module state such as synthetic shadow and the `@wire` registry would leak between files.
|
|
40
|
+
|
|
41
|
+
Namespace imports (`import Child from 'x/child'`, or `<x-child>` in a template) are resolved with the standard LWC module config. Point it at your components in an `lwc.config.json` next to the Vitest config, or in an `lwc` key in `package.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"modules": [{ "dir": "src/modules" }]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This replaces the `moduleNameMapper` entry used with `@lwc/jest-preset`.
|
|
50
|
+
|
|
51
|
+
#### nativeShadow
|
|
52
|
+
|
|
53
|
+
Tests run with synthetic shadow DOM by default. To use native shadow DOM instead:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
export default defineConfig(lwcVitestConfig(import.meta.url, { nativeShadow: true }));
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Tests can read the mode with `inject('lwcNativeShadow')` from `vitest`. There is no `global['lwc-jest']` under Vitest.
|
|
60
|
+
|
|
61
|
+
To run the same tests in both modes, add one project per mode. Each project needs a unique name:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { defineConfig } from 'vitest/config';
|
|
65
|
+
import { lwcVitestConfig } from '@lwc/vitest-preset';
|
|
66
|
+
|
|
67
|
+
function project(nativeShadow) {
|
|
68
|
+
const config = lwcVitestConfig(import.meta.url, { nativeShadow });
|
|
69
|
+
config.test.name = nativeShadow ? 'native shadow' : 'synthetic shadow';
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
test: { projects: [project(false), project(true)] },
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Custom matchers
|
|
79
|
+
|
|
80
|
+
`expect().toThrowInConnectedCallback()` and its alias `toThrowErrorInConnectedCallback()` are registered automatically and typed for TypeScript. They behave as they do in [`@lwc/jest-preset`](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md#custom-matchers).
|
|
81
|
+
|
|
82
|
+
## Differences from `@lwc/jest-preset`
|
|
83
|
+
|
|
84
|
+
- **No snapshot serializer yet.** Snapshots don't go through `@lwc/jest-serializer`.
|
|
85
|
+
- **Component names come from the file path.** `@lwc/jest-transformer` compiles every component as `x-test`. Here a component keeps its real name (for example `smoke-mixin`), so tag names and CSS scope tokens differ.
|
|
86
|
+
- **No SSR preset.** There is no counterpart to `@lwc/jest-preset/ssr`.
|
|
87
|
+
- **`import.meta` is not rewritten.** `@lwc/jest-transformer` turns every `import.meta.env.X` into a boolean. Vitest keeps the real `import.meta`. See [`import.meta`](#importmeta).
|
|
88
|
+
|
|
89
|
+
### `import.meta`
|
|
90
|
+
|
|
91
|
+
Under Jest, `import.meta.env.X` is `true` only when `process.env.X` matches `/true/i`, and `import.meta.url` is `undefined`. Under Vitest, `DEV` is `true`, `PROD` is `false`, `SSR` is `false` (`true` in a `node` environment), `MODE` is `'test'`, any other key is the raw `process.env` string, and `import.meta.url` is the module's `file://` URL. So string comparisons such as `import.meta.env.X === 'false'` behave differently.
|
|
92
|
+
|
|
93
|
+
Both runners read `process.env` when the code runs, but they convert `SSR` differently:
|
|
94
|
+
|
|
95
|
+
| `process.env.SSR` | Jest | Vitest |
|
|
96
|
+
| --------------------------------- | ------- | ------- |
|
|
97
|
+
| `'true'` | `true` | `true` |
|
|
98
|
+
| `'1'` | `false` | `true` |
|
|
99
|
+
| `'false'`, `false` or `undefined` | `false` | `true` |
|
|
100
|
+
| `''` or deleted | `false` | `false` |
|
|
101
|
+
|
|
102
|
+
- To turn SSR on in a test, use `vi.stubEnv('SSR', true)` and undo it with `vi.unstubAllEnvs()`. Setting `process.env.SSR = 'true'` and then `delete process.env.SSR` also works, under both runners.
|
|
103
|
+
- Never turn SSR off with `'false'`, `false` or `undefined`. Vitest reads all three as `true`.
|
|
104
|
+
- Don't set `SSR` in `test.env`. Vitest resets it from the test environment when the worker starts. Set it in a setup file instead. Use a setup file too when a module reads `import.meta.env.SSR` at the top level, because the value is captured when the module is imported.
|
|
105
|
+
- Jest only rewrites dot access (`import.meta.env.X`). Computed access, optional chaining and destructuring only work under Vitest.
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@lwc/vitest-preset",
|
|
3
|
+
"version": "19.7.0-alpha.0",
|
|
4
|
+
"description": "Vitest preset configuration and setup to help test LWC",
|
|
5
|
+
"homepage": "https://lwc.dev/",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/salesforce/lwc-test.git",
|
|
9
|
+
"directory": "packages/@lwc/vitest-preset"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/salesforce/lwc-test/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "src/index.js",
|
|
17
|
+
"types": "types/index.d.ts",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"vitest",
|
|
20
|
+
"vite",
|
|
21
|
+
"lwc"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"/src/**/*.js",
|
|
25
|
+
"!/src/__tests__",
|
|
26
|
+
"/types"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./types/index.d.ts",
|
|
31
|
+
"default": "./src/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./setup": "./src/setup.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@babel/core": "7.25.2",
|
|
37
|
+
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
38
|
+
"@babel/plugin-syntax-decorators": "7.24.7",
|
|
39
|
+
"@babel/preset-typescript": "7.24.7",
|
|
40
|
+
"@lwc/jest-shared": "19.7.0-alpha.0",
|
|
41
|
+
"@lwc/vitest-plugin": "19.7.0-alpha.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@lwc/engine-dom": "^9.1.1",
|
|
45
|
+
"@lwc/rollup-plugin": "^9.1.1",
|
|
46
|
+
"@lwc/synthetic-shadow": "^9.1.1",
|
|
47
|
+
"@lwc/wire-service": "^9.1.1",
|
|
48
|
+
"jsdom": ">=26.1.0",
|
|
49
|
+
"vitest": "^4.1.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"gitHead": "ded506234f9aac0e8195342811aae7bd22b48742"
|
|
55
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Minimal polyfill for ARIA string reflection, e.g. aria-label -> ariaLabel.
|
|
9
|
+
// As of this writing, ARIA string reflection is supported in Chrome and Safari, but not Firefox or JSDOM.
|
|
10
|
+
|
|
11
|
+
// ARIA Element properties that reflect to strings (not Element or FrozenArray<Element>)
|
|
12
|
+
// https://w3c.github.io/aria/#ARIAMixin
|
|
13
|
+
const ARIA_STRING_PROPS = [
|
|
14
|
+
'role',
|
|
15
|
+
'ariaAtomic',
|
|
16
|
+
'ariaAutoComplete',
|
|
17
|
+
'ariaBusy',
|
|
18
|
+
'ariaChecked',
|
|
19
|
+
'ariaColCount',
|
|
20
|
+
'ariaColIndex',
|
|
21
|
+
'ariaColIndexText',
|
|
22
|
+
'ariaColSpan',
|
|
23
|
+
'ariaCurrent',
|
|
24
|
+
'ariaDescription',
|
|
25
|
+
'ariaDisabled',
|
|
26
|
+
'ariaExpanded',
|
|
27
|
+
'ariaHasPopup',
|
|
28
|
+
'ariaHidden',
|
|
29
|
+
'ariaInvalid',
|
|
30
|
+
'ariaKeyShortcuts',
|
|
31
|
+
'ariaLabel',
|
|
32
|
+
'ariaLevel',
|
|
33
|
+
'ariaLive',
|
|
34
|
+
'ariaModal',
|
|
35
|
+
'ariaMultiLine',
|
|
36
|
+
'ariaMultiSelectable',
|
|
37
|
+
'ariaOrientation',
|
|
38
|
+
'ariaPlaceholder',
|
|
39
|
+
'ariaPosInSet',
|
|
40
|
+
'ariaPressed',
|
|
41
|
+
'ariaReadOnly',
|
|
42
|
+
'ariaRequired',
|
|
43
|
+
'ariaRoleDescription',
|
|
44
|
+
'ariaRowCount',
|
|
45
|
+
'ariaRowIndex',
|
|
46
|
+
'ariaRowIndexText',
|
|
47
|
+
'ariaRowSpan',
|
|
48
|
+
'ariaSelected',
|
|
49
|
+
'ariaSetSize',
|
|
50
|
+
'ariaSort',
|
|
51
|
+
'ariaValueMax',
|
|
52
|
+
'ariaValueMin',
|
|
53
|
+
'ariaValueNow',
|
|
54
|
+
'ariaValueText',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const prop of ARIA_STRING_PROPS) {
|
|
58
|
+
if (!(prop in Element.prototype)) {
|
|
59
|
+
// sniff for support
|
|
60
|
+
// e.g. `ariaPosInSet` -> `aria-posinset`
|
|
61
|
+
const attribute = prop.replace(/[A-Z]/, (letter) => `-${letter}`).toLowerCase();
|
|
62
|
+
Object.defineProperty(Element.prototype, prop, {
|
|
63
|
+
get() {
|
|
64
|
+
return this.getAttribute(attribute);
|
|
65
|
+
},
|
|
66
|
+
set(value) {
|
|
67
|
+
// Per the spec, only null is treated as removing the attribute. However, Chromium/WebKit currently
|
|
68
|
+
// differ from the spec and allow undefined as well. Here, we follow the standard.
|
|
69
|
+
// See: https://github.com/w3c/aria/issues/1858
|
|
70
|
+
if (value === null) {
|
|
71
|
+
this.removeAttribute(attribute);
|
|
72
|
+
} else {
|
|
73
|
+
this.setAttribute(attribute, value);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
// These props in both WebKit and Chromium are configurable/enumerable. This allows overriding
|
|
77
|
+
// (Try `Object.getOwnPropertyDescriptor(Element.prototype, 'ariaLabel')` in either browser)
|
|
78
|
+
configurable: true,
|
|
79
|
+
enumerable: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import lwc from '@lwc/rollup-plugin';
|
|
9
|
+
import { salesforceScopedImports } from '@lwc/vitest-plugin';
|
|
10
|
+
import { lwcKnownScopeTokens } from './scope-tokens.js';
|
|
11
|
+
import { lwcTypeScript } from './typescript.js';
|
|
12
|
+
|
|
13
|
+
// Print LWC compiler diagnostics in jest-transformer's `[LWC Warn](<file>): <message>` format;
|
|
14
|
+
// the plugin's own string warnings still go through Vite's logger.
|
|
15
|
+
function formatLwcWarnings(plugin) {
|
|
16
|
+
const { transform } = plugin;
|
|
17
|
+
return {
|
|
18
|
+
...plugin,
|
|
19
|
+
transform(src, id) {
|
|
20
|
+
const ctx = Object.create(this);
|
|
21
|
+
ctx.warn = (log) => {
|
|
22
|
+
if (!log?.pluginCode) {
|
|
23
|
+
return this.warn(log);
|
|
24
|
+
}
|
|
25
|
+
const message = log.message.replace(/^@lwc\/rollup-plugin: /, '');
|
|
26
|
+
console.warn(`\x1b[33m[LWC Warn]\x1b[0m(${id.split('?')[0]}): ${message}`);
|
|
27
|
+
};
|
|
28
|
+
return transform.call(ctx, src, id);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Vitest blanks every `.css` module and Vite's CSS pipeline would replace LWC's stylesheet output,
|
|
34
|
+
// so suffix resolved stylesheet ids to hide them from both; bypassViteCss strips it for LWC.
|
|
35
|
+
const CSS_ID = /\.css(?=$|\?)/;
|
|
36
|
+
const CSS_SUFFIX = '.lwc.js';
|
|
37
|
+
const tagCss = (id) => id.replace(CSS_ID, `.css${CSS_SUFFIX}`);
|
|
38
|
+
const untagCss = (id) => id.replace(`.css${CSS_SUFFIX}`, '.css');
|
|
39
|
+
function lwcStylesheetIds() {
|
|
40
|
+
return {
|
|
41
|
+
name: 'lwc-stylesheet-ids',
|
|
42
|
+
enforce: 'pre',
|
|
43
|
+
async resolveId(source, importer, options) {
|
|
44
|
+
const resolved = await this.resolve(source, importer && untagCss(importer), {
|
|
45
|
+
...options,
|
|
46
|
+
skipSelf: true,
|
|
47
|
+
});
|
|
48
|
+
return resolved && CSS_ID.test(resolved.id)
|
|
49
|
+
? { ...resolved, id: tagCss(resolved.id) }
|
|
50
|
+
: resolved;
|
|
51
|
+
},
|
|
52
|
+
// The watcher reports the real .css path, but the module graph holds the tagged id.
|
|
53
|
+
configureVitest({ vitest }) {
|
|
54
|
+
const { watcher } = vitest.vite;
|
|
55
|
+
watcher.on(
|
|
56
|
+
'change',
|
|
57
|
+
(file) => CSS_ID.test(file) && watcher.emit('change', tagCss(file))
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function bypassViteCss(plugin) {
|
|
63
|
+
const { load, transform } = plugin;
|
|
64
|
+
return {
|
|
65
|
+
...plugin,
|
|
66
|
+
load(id) {
|
|
67
|
+
return load.call(this, untagCss(id));
|
|
68
|
+
},
|
|
69
|
+
transform(src, id) {
|
|
70
|
+
return transform.call(this, src, untagCss(id));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parity with jest-preset's coveragePathIgnorePatterns. Vitest reads coverage only from the root
|
|
76
|
+
// config, so append to it from a hook; that also covers configs that nest this in `projects`.
|
|
77
|
+
const coverageExclude = ['**/*.css', '**/*.html'];
|
|
78
|
+
function lwcCoverageExclude() {
|
|
79
|
+
return {
|
|
80
|
+
name: 'lwc-coverage-exclude',
|
|
81
|
+
configureVitest({ vitest }) {
|
|
82
|
+
const { exclude } = vitest.config.coverage;
|
|
83
|
+
exclude.push(...coverageExclude.filter((glob) => !exclude.includes(glob)));
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Vitest config factory for LWC. Call as `lwcVitestConfig(import.meta.url)` from a vitest.config.
|
|
89
|
+
// `nativeShadow: true` skips synthetic-shadow (Jest's `globals['lwc-jest'].nativeShadow`).
|
|
90
|
+
export function lwcVitestConfig(importMetaUrl, { nativeShadow = false } = {}) {
|
|
91
|
+
const rootDir = fileURLToPath(new URL('.', importMetaUrl));
|
|
92
|
+
// Compiler options mirroring the jest-transformer config for parity.
|
|
93
|
+
// rootDir is mandatory: buildStart crashes without it under Vitest (no rollup input).
|
|
94
|
+
// Known gap: Jest hardcodes namespace 'x'/name 'test'; the plugin derives them from the path
|
|
95
|
+
// (no override), so `sel` and CSS scope tokens differ (x-test vs e.g. smoke-mixin).
|
|
96
|
+
const compilerOptions = {
|
|
97
|
+
rootDir,
|
|
98
|
+
sourcemap: true,
|
|
99
|
+
dynamicImports: { strictSpecifier: false },
|
|
100
|
+
enableDynamicComponents: true,
|
|
101
|
+
enableLwcOn: true,
|
|
102
|
+
};
|
|
103
|
+
// Compile test files too, as Jest does: they can define inline components with decorators.
|
|
104
|
+
// Like jest-preset's `transform`, compile only .js/.ts/.html/.css; Vite handles the rest (e.g. JSON).
|
|
105
|
+
const compileExclude = ['**/node_modules/**', /(?<!\.(?:js|ts|html|css))$/];
|
|
106
|
+
// enablePrivateMethods is per-namespace in Jest (lightning/interop only), but the plugin
|
|
107
|
+
// exposes only a global flag, so split compilation into two path-scoped instances.
|
|
108
|
+
const privateMethodsNamespaces = /[\\/](?:modules|jest-modules)[\\/](?:lightning|interop)[\\/]/;
|
|
109
|
+
return {
|
|
110
|
+
root: rootDir,
|
|
111
|
+
// Vite's built-in .ts transform lowers LWC decorators; lwcTypeScript() strips types instead.
|
|
112
|
+
// esbuild runs it on Vite 6/7, Oxc on Vite 8, so turn off both.
|
|
113
|
+
esbuild: false,
|
|
114
|
+
oxc: false,
|
|
115
|
+
plugins: [
|
|
116
|
+
lwcCoverageExclude(),
|
|
117
|
+
lwcStylesheetIds(),
|
|
118
|
+
salesforceScopedImports(),
|
|
119
|
+
lwcTypeScript(),
|
|
120
|
+
// Compile .js/.html/.css LWC bundles (replaces @lwc/jest-transformer's compile job).
|
|
121
|
+
bypassViteCss(
|
|
122
|
+
formatLwcWarnings(
|
|
123
|
+
lwc({
|
|
124
|
+
...compilerOptions,
|
|
125
|
+
include: [privateMethodsNamespaces],
|
|
126
|
+
exclude: compileExclude,
|
|
127
|
+
enablePrivateMethods: true,
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
bypassViteCss(
|
|
132
|
+
formatLwcWarnings(
|
|
133
|
+
lwc({
|
|
134
|
+
...compilerOptions,
|
|
135
|
+
exclude: [...compileExclude, privateMethodsNamespaces],
|
|
136
|
+
enablePrivateMethods: false,
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
),
|
|
140
|
+
// After compilation; replaces jest-transformer's addKnownScopeToken append.
|
|
141
|
+
lwcKnownScopeTokens({ exclude: compileExclude }),
|
|
142
|
+
],
|
|
143
|
+
// Resolve bare `lwc` to the engine (parity with @lwc/jest-resolver), via the @wire trap.
|
|
144
|
+
resolve: { alias: { lwc: fileURLToPath(new URL('./lwc.js', import.meta.url)) } },
|
|
145
|
+
test: {
|
|
146
|
+
environment: 'jsdom',
|
|
147
|
+
// Match Jest's jsdom URL (Vitest defaults to http://localhost:3000).
|
|
148
|
+
environmentOptions: { jsdom: { url: 'http://localhost/' } },
|
|
149
|
+
// Read by setup.js via inject().
|
|
150
|
+
provide: { lwcNativeShadow: nativeShadow },
|
|
151
|
+
globals: true,
|
|
152
|
+
include: ['**/__tests__/**/*.vitest.test.{js,ts}'],
|
|
153
|
+
setupFiles: ['@lwc/vitest-preset/setup'],
|
|
154
|
+
// Setup order is load-bearing (polyfill -> synthetic-shadow -> engine -> flag); run in order.
|
|
155
|
+
sequence: { setupFiles: 'list' },
|
|
156
|
+
// Jest runs each test file in its own worker with a fresh module registry, and LWC tests
|
|
157
|
+
// rely on it (synthetic-shadow patches globals once per load; the @wire registry is module state).
|
|
158
|
+
// These are Vitest 4's defaults; pin them so a package config or a future default can't change them.
|
|
159
|
+
pool: 'forks',
|
|
160
|
+
isolate: true,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default lwcVitestConfig;
|
package/src/lwc.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// What `lwc` resolves to: engine-dom with a wrapped registerDecorators. Jest patches the CJS
|
|
9
|
+
// engine in place; an ESM namespace can't be patched, so the local export shadows `export *`.
|
|
10
|
+
import { registerDecorators as originalRegisterDecorators } from '@lwc/engine-dom';
|
|
11
|
+
|
|
12
|
+
export * from '@lwc/engine-dom';
|
|
13
|
+
|
|
14
|
+
// Provides temporary backward compatibility for wire-protocol reform: lwc > 1.5.0
|
|
15
|
+
global.wireAdaptersRegistryHack = new Map();
|
|
16
|
+
|
|
17
|
+
function isValidWireAdapter(adapter) {
|
|
18
|
+
let isValid = false;
|
|
19
|
+
if (typeof adapter === 'function') {
|
|
20
|
+
// lets check, if it is a valid adapter
|
|
21
|
+
try {
|
|
22
|
+
const adapterInstance = new adapter(() => {});
|
|
23
|
+
isValid =
|
|
24
|
+
typeof adapterInstance.connect === 'function' &&
|
|
25
|
+
typeof adapterInstance.update === 'function' &&
|
|
26
|
+
typeof adapterInstance.disconnect === 'function';
|
|
27
|
+
} catch (e) {
|
|
28
|
+
isValid = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return isValid;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns a wire adapter mock in the shape of:
|
|
37
|
+
*
|
|
38
|
+
* fn : Used when adapter is invoked imperatively. It proxies to the original adapter function, if is callable.
|
|
39
|
+
* fn.adapter : A valid wire adapter class, consumable by @lwc/engine-dom.
|
|
40
|
+
* If the @originalAdapter.adapter or @originalAdapter is a valid wire adapter class, fn.adapter will
|
|
41
|
+
* act as a proxy on it until a spy is attached.
|
|
42
|
+
*
|
|
43
|
+
* @param originalAdapter
|
|
44
|
+
* @returns {function(...[*]): *}
|
|
45
|
+
*/
|
|
46
|
+
function createWireAdapterMockClass(originalAdapter) {
|
|
47
|
+
const noopAdapter = {
|
|
48
|
+
connect() {},
|
|
49
|
+
update() {},
|
|
50
|
+
disconnect() {},
|
|
51
|
+
};
|
|
52
|
+
let baseAdapter;
|
|
53
|
+
let baseAdapterFn = () => {};
|
|
54
|
+
const spies = [];
|
|
55
|
+
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(originalAdapter, 'adapter')) {
|
|
57
|
+
// Is more likely that the originalAdapter was registered with the wire service
|
|
58
|
+
// .adapter is the one that the engine will use, lets make it our base adapter
|
|
59
|
+
baseAdapter = originalAdapter.adapter;
|
|
60
|
+
} else if (isValidWireAdapter(originalAdapter)) {
|
|
61
|
+
// it may be the case that original adapter is a valid one, if is the case, lets use it as our base adapter
|
|
62
|
+
baseAdapter = originalAdapter;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof originalAdapter === 'function') {
|
|
66
|
+
// Mostly used in apex methods
|
|
67
|
+
baseAdapterFn = originalAdapter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Support for adapters to be called imperatively, mainly for apex.
|
|
71
|
+
const newAdapterMock = function (...args) {
|
|
72
|
+
return baseAdapterFn.call(this, ...args);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
newAdapterMock.adapter = class WireAdapterMock {
|
|
76
|
+
constructor(dataCallback) {
|
|
77
|
+
// if a test is spying these adapter, it means is overriding the implementation
|
|
78
|
+
this._originalAdapter =
|
|
79
|
+
spies.length === 0 && baseAdapter ? new baseAdapter(dataCallback) : noopAdapter;
|
|
80
|
+
this._dataCallback = dataCallback;
|
|
81
|
+
|
|
82
|
+
spies.forEach((spy) => spy.createInstance(this));
|
|
83
|
+
}
|
|
84
|
+
connect() {
|
|
85
|
+
spies.forEach((spy) => spy.connect(this));
|
|
86
|
+
this._originalAdapter.connect();
|
|
87
|
+
}
|
|
88
|
+
update(config) {
|
|
89
|
+
spies.forEach((spy) => spy.update(this, config));
|
|
90
|
+
this._originalAdapter.update(config);
|
|
91
|
+
}
|
|
92
|
+
disconnect() {
|
|
93
|
+
spies.forEach((spy) => spy.disconnect(this));
|
|
94
|
+
this._originalAdapter.disconnect();
|
|
95
|
+
}
|
|
96
|
+
emit(value) {
|
|
97
|
+
this._dataCallback(value);
|
|
98
|
+
}
|
|
99
|
+
static spyAdapter(spy) {
|
|
100
|
+
// this function is meant to be used by wire-service-jest-util library.
|
|
101
|
+
// When this is used, register* was called, thus replacing the wire behaviour.
|
|
102
|
+
// As consequence, it will stop calling the originalAdapter on new instances.
|
|
103
|
+
spies.push(spy);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return newAdapterMock;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function registerDecorators(Ctor, decoratorsMeta) {
|
|
111
|
+
const wire = decoratorsMeta.wire || {};
|
|
112
|
+
|
|
113
|
+
Object.keys(wire).forEach((adapterName) => {
|
|
114
|
+
const adapter = wire[adapterName].adapter;
|
|
115
|
+
let wireAdapterMock = global.wireAdaptersRegistryHack.get(adapter);
|
|
116
|
+
|
|
117
|
+
if (!wireAdapterMock) {
|
|
118
|
+
// Checking if the adapter is extensible, since with the wire reform,
|
|
119
|
+
// the adapterId must be:
|
|
120
|
+
// a) An extensible object (so backward compatibility is provided via register)
|
|
121
|
+
// b) A valid class.
|
|
122
|
+
|
|
123
|
+
if (!Object.isExtensible(adapter)) {
|
|
124
|
+
// if we are in case of a) lets throw now so the developer knows that they need to migrate their
|
|
125
|
+
// adapters.
|
|
126
|
+
throw new TypeError('Invalid adapterId, it must be extensible.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Lets create a whole replacement for this adapter.
|
|
130
|
+
wireAdapterMock = createWireAdapterMockClass(adapter);
|
|
131
|
+
|
|
132
|
+
global.wireAdaptersRegistryHack.set(adapter, wireAdapterMock);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// we are entirely replacing the wire adapter with one that can be spied on.
|
|
136
|
+
wire[adapterName].adapter = wireAdapterMock;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return originalRegisterDecorators(Ctor, decoratorsMeta);
|
|
140
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import { expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Based on Jest's toThrow() https://jestjs.io/docs/expect#tothrowerror
|
|
10
|
+
// regular expression: error message matches the pattern
|
|
11
|
+
// string: error message includes the substring
|
|
12
|
+
// error object: error message is equal to the message property of the object
|
|
13
|
+
// error class: error object is instance of class
|
|
14
|
+
function matches(matcher, error) {
|
|
15
|
+
if (!matcher) {
|
|
16
|
+
return true; // any error will do
|
|
17
|
+
}
|
|
18
|
+
if (typeof matcher === 'string') {
|
|
19
|
+
return error.message.includes(matcher);
|
|
20
|
+
}
|
|
21
|
+
if (matcher instanceof RegExp) {
|
|
22
|
+
return matcher.test(error.message);
|
|
23
|
+
}
|
|
24
|
+
if (typeof matcher === 'function') {
|
|
25
|
+
// Error class
|
|
26
|
+
return error instanceof matcher;
|
|
27
|
+
}
|
|
28
|
+
if (matcher.message) {
|
|
29
|
+
// Error object
|
|
30
|
+
return error.message === matcher.message;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Custom matcher to test for errors in a connectedCallback of a web component,
|
|
35
|
+
// either LWC or third-party.
|
|
36
|
+
// Due to native vs synthetic lifecycle (https://github.com/salesforce/lwc/pull/3662),
|
|
37
|
+
// the connectedCallback function may either throw a synchronous error or an async error
|
|
38
|
+
// on `window.onerror`. This function is agnostic to both and can be used as a drop-in replacement
|
|
39
|
+
// for `toThrow()`/`toThrowError`.
|
|
40
|
+
function createMatcher(matcherName) {
|
|
41
|
+
return {
|
|
42
|
+
[matcherName]: function toThrowErrorInConnectedCallback(func, matcher) {
|
|
43
|
+
if (typeof func !== 'function') {
|
|
44
|
+
throw new Error(`Expected a function, received: ${func}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// There are two cases here:
|
|
48
|
+
// 1) Error is thrown by LWC component with synthetic behavior (sync)
|
|
49
|
+
// 2) Error is thrown by third-party component or LWC with native behavior (async)
|
|
50
|
+
// We don't care which one we get; they are both treated the same.
|
|
51
|
+
let syncError;
|
|
52
|
+
let asyncError;
|
|
53
|
+
|
|
54
|
+
const listener = (errorEvent) => {
|
|
55
|
+
errorEvent.preventDefault(); // do not continue bubbling the error; we're handling it
|
|
56
|
+
asyncError = errorEvent.error;
|
|
57
|
+
};
|
|
58
|
+
window.addEventListener('error', listener);
|
|
59
|
+
try {
|
|
60
|
+
func();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
syncError = err;
|
|
63
|
+
} finally {
|
|
64
|
+
window.removeEventListener('error', listener);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { utils } = this;
|
|
68
|
+
const overallError = syncError || asyncError;
|
|
69
|
+
const pass = !!overallError && matches(matcher, overallError);
|
|
70
|
+
|
|
71
|
+
// Inspired by https://github.com/jest-community/jest-extended/blob/1f91c09/src/matchers/toThrowWithMessage.js#L25
|
|
72
|
+
// This shows a nicely-formatted error message to the user
|
|
73
|
+
const positiveHint = utils.matcherHint(`.${matcherName}`, 'function', 'expected');
|
|
74
|
+
const negativeHint = utils.matcherHint(`.not.${matcherName}`, 'function', 'expected');
|
|
75
|
+
const message =
|
|
76
|
+
`${pass ? positiveHint : negativeHint}` +
|
|
77
|
+
'\n\n' +
|
|
78
|
+
`Expected connectedCallback${pass ? ' not' : ''} to throw:\n` +
|
|
79
|
+
` ${
|
|
80
|
+
matcher ? utils.printExpected(matcher) : utils.EXPECTED_COLOR('Any error')
|
|
81
|
+
}\n` +
|
|
82
|
+
'Thrown:\n' +
|
|
83
|
+
` ${
|
|
84
|
+
overallError
|
|
85
|
+
? utils.printReceived(overallError)
|
|
86
|
+
: utils.RECEIVED_COLOR('No error')
|
|
87
|
+
}\n`;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
pass,
|
|
91
|
+
message: () => message,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
expect.extend({
|
|
97
|
+
// both names are accepted, ala Jest's `toThrow` and `toThrowError` being equivalent
|
|
98
|
+
...createMatcher('toThrowInConnectedCallback'),
|
|
99
|
+
...createMatcher('toThrowErrorInConnectedCallback'),
|
|
100
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const rollupPluginRequire = createRequire(require.resolve('@lwc/rollup-plugin'));
|
|
12
|
+
// Filter ids with the same createFilter @lwc/rollup-plugin uses, so include/exclude match exactly.
|
|
13
|
+
const { createFilter } = rollupPluginRequire('@rollup/pluginutils');
|
|
14
|
+
// Hash tokens with the template-compiler @lwc/rollup-plugin compiles with, so they always match.
|
|
15
|
+
const compilerRequire = createRequire(rollupPluginRequire.resolve('@lwc/compiler'));
|
|
16
|
+
const { generateScopeTokens } = compilerRequire('@lwc/template-compiler/dist/index.cjs');
|
|
17
|
+
// Always load @lwc/jest-shared with require(), never import: Vite's import of this CJS package
|
|
18
|
+
// loads a separate copy, and the snapshot serializer reads the require() one. So register into
|
|
19
|
+
// that copy (as jest-transformer does) via its absolute path.
|
|
20
|
+
const registerImport = [
|
|
21
|
+
`import { createRequire as __lwcCreateRequire } from 'node:module';`,
|
|
22
|
+
`const { addKnownScopeToken } = __lwcCreateRequire(import.meta.url)(${JSON.stringify(
|
|
23
|
+
require.resolve('@lwc/jest-shared')
|
|
24
|
+
)});`,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Same namespace/name @lwc/rollup-plugin passes to the compiler: an alias `?specifier=ns/name`,
|
|
28
|
+
// else the last two directories. Tokens follow whichever it is, so they stay right if module ids
|
|
29
|
+
// gain a specifier (e.g. x/test).
|
|
30
|
+
function componentNamespaceAndName(id) {
|
|
31
|
+
const [filename, query] = id.split('?', 2);
|
|
32
|
+
const specifier = new URLSearchParams(query).get('specifier');
|
|
33
|
+
const [namespace, name] =
|
|
34
|
+
specifier?.split('/') ?? path.dirname(filename).split(path.sep).slice(-2);
|
|
35
|
+
return { filename, namespace, name };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Port of jest-transformer's addKnownScopeToken append: register each compiled template's scope
|
|
39
|
+
// tokens with @lwc/jest-shared so the snapshot serializer can replace them. Pass the same
|
|
40
|
+
// `exclude` the lwc() instances get, so this registers tokens for exactly the templates they compile.
|
|
41
|
+
export function lwcKnownScopeTokens({ exclude } = {}) {
|
|
42
|
+
const filter = createFilter(undefined, exclude);
|
|
43
|
+
return {
|
|
44
|
+
name: 'lwc-known-scope-tokens',
|
|
45
|
+
transform(code, id) {
|
|
46
|
+
const { filename, namespace, name } = componentNamespaceAndName(id);
|
|
47
|
+
if (path.extname(filename) !== '.html' || !filter(filename)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { cssScopeTokens } = generateScopeTokens(filename, namespace, name);
|
|
51
|
+
const register = cssScopeTokens.map(
|
|
52
|
+
(token) => `addKnownScopeToken(${JSON.stringify(token)});`
|
|
53
|
+
);
|
|
54
|
+
// Appending moves no existing code, so `map: null` keeps the compiler's sourcemap.
|
|
55
|
+
return { code: [code, ...registerImport, ...register].join('\n'), map: null };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Vitest runtime bootstrap for LWC — port of `@lwc/jest-preset/src/setup.js`.
|
|
9
|
+
// Load order is load-bearing: ARIA polyfill -> synthetic-shadow -> engine-dom.
|
|
10
|
+
import { inject } from 'vitest';
|
|
11
|
+
import './aria-reflection-polyfill.js';
|
|
12
|
+
|
|
13
|
+
// On Node 20, Vitest doesn't copy these jsdom globals; synthetic-shadow and engine-dom read them at load.
|
|
14
|
+
for (const key of ['HTMLDocument', 'ElementInternals']) {
|
|
15
|
+
global[key] ??= global.jsdom?.window[key];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!inject('lwcNativeShadow')) {
|
|
19
|
+
// synthetic-shadow patches the global ShadowRoot/EventTarget the engine captures at load;
|
|
20
|
+
// loading it twice corrupts those globals, so guard before the (dynamic) import.
|
|
21
|
+
if (
|
|
22
|
+
ShadowRoot.prototype.constructor.toString().includes('function SyntheticShadowRoot') ||
|
|
23
|
+
EventTarget.prototype.addEventListener
|
|
24
|
+
.toString()
|
|
25
|
+
.includes('function patchedAddEventListener')
|
|
26
|
+
) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'@lwc/synthetic-shadow is being loaded twice. Please examine your vitest/jsdom configuration.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
await import('@lwc/synthetic-shadow');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Same module the `lwc` alias points at, so the @wire trap and its registry exist before any test.
|
|
35
|
+
const { setFeatureFlag } = await import('./lwc.js');
|
|
36
|
+
setFeatureFlag('ENABLE_EXPERIMENTAL_SIGNALS', true);
|
|
37
|
+
|
|
38
|
+
// Custom matchers
|
|
39
|
+
await import('./matchers/expect-throw-in-connected-callback.js');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import { transformSync } from '@babel/core';
|
|
8
|
+
import presetTypeScript from '@babel/preset-typescript';
|
|
9
|
+
import syntaxDecorators from '@babel/plugin-syntax-decorators';
|
|
10
|
+
import syntaxClassProperties from '@babel/plugin-syntax-class-properties';
|
|
11
|
+
|
|
12
|
+
// Vite plugin that strips TypeScript types from .ts files while leaving LWC decorators
|
|
13
|
+
// (@api/@track/@wire) intact for the LWC compiler. Vite's esbuild lowers those decorators
|
|
14
|
+
// into runtime __decorate calls, which the compiler can't read, so esbuild must stay off
|
|
15
|
+
// for .ts. Config mirrors @lwc/jest-transformer's BABEL_TS_CONFIG.
|
|
16
|
+
export function lwcTypeScript() {
|
|
17
|
+
return {
|
|
18
|
+
name: 'lwc-vitest:typescript',
|
|
19
|
+
enforce: 'pre',
|
|
20
|
+
transform(code, id) {
|
|
21
|
+
const [filepath] = id.split('?');
|
|
22
|
+
if (!filepath.endsWith('.ts') || filepath.includes('/node_modules/')) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const result = transformSync(code, {
|
|
26
|
+
filename: filepath,
|
|
27
|
+
sourceMaps: true,
|
|
28
|
+
babelrc: false,
|
|
29
|
+
configFile: false,
|
|
30
|
+
presets: [[presetTypeScript, { allowDeclareFields: true }]],
|
|
31
|
+
plugins: [
|
|
32
|
+
syntaxClassProperties,
|
|
33
|
+
[syntaxDecorators, { decoratorsBeforeExport: true }],
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
return { code: result.code, map: result.map };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import 'vitest';
|
|
3
|
+
import type { ViteUserConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
export interface LwcVitestOptions {
|
|
6
|
+
/** Skip synthetic-shadow and run components in native shadow DOM. */
|
|
7
|
+
nativeShadow?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Vitest config for LWC; call as `lwcVitestConfig(import.meta.url)` from a vitest config. */
|
|
11
|
+
export declare function lwcVitestConfig(
|
|
12
|
+
importMetaUrl: string,
|
|
13
|
+
options?: LwcVitestOptions
|
|
14
|
+
): ViteUserConfig;
|
|
15
|
+
export default lwcVitestConfig;
|
|
16
|
+
|
|
17
|
+
interface Constructable {
|
|
18
|
+
new (...args: any[]): any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LwcVitestMatchers<R = unknown> {
|
|
22
|
+
/**
|
|
23
|
+
* @description
|
|
24
|
+
* Assert that appending a custom element to the DOM causes an error to be thrown from that element's `connectedCallback`.
|
|
25
|
+
* @example
|
|
26
|
+
* expect(() => {
|
|
27
|
+
* document.body.appendChild(element);
|
|
28
|
+
* }).toThrowInConnectedCallback('expected error message');
|
|
29
|
+
* @see
|
|
30
|
+
* [@lwc/jest-preset docs](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md#custom-matchers)
|
|
31
|
+
*/
|
|
32
|
+
toThrowInConnectedCallback(expected?: string | Constructable | RegExp | Error): R;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @description
|
|
36
|
+
* Assert that appending a custom element to the DOM causes an error to be thrown from that element's `connectedCallback`.
|
|
37
|
+
*
|
|
38
|
+
* Equivalent to `toThrowInConnectedCallback`.
|
|
39
|
+
* @example
|
|
40
|
+
* expect(() => {
|
|
41
|
+
* document.body.appendChild(element);
|
|
42
|
+
* }).toThrowErrorInConnectedCallback('expected error message');
|
|
43
|
+
* @see
|
|
44
|
+
* [@lwc/jest-preset docs](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md#custom-matchers)
|
|
45
|
+
*/
|
|
46
|
+
toThrowErrorInConnectedCallback(expected?: string | Constructable | RegExp | Error): R;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Vitest's `Assertion` and `ExpectStatic` both extend `Matchers`, so augmenting it
|
|
50
|
+
// covers `expect(fn).toThrowInConnectedCallback(...)`.
|
|
51
|
+
declare module 'vitest' {
|
|
52
|
+
interface Matchers<T = any> extends LwcVitestMatchers<T> {}
|
|
53
|
+
// Mirrors the `nativeShadow` option; read it with `inject('lwcNativeShadow')`.
|
|
54
|
+
interface ProvidedContext {
|
|
55
|
+
lwcNativeShadow: boolean;
|
|
56
|
+
}
|
|
57
|
+
}
|