@lwc/vitest-plugin 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 +7 -2
- package/package.json +31 -5
- package/src/index.js +230 -0
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lwc/vitest-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vite/Vitest plugin that mocks `@salesforce/*` scoped imports for LWC unit tests.
|
|
4
|
+
|
|
5
|
+
It is the Vitest counterpart to the `@salesforce/*` mocking that `@lwc/jest-transformer`
|
|
6
|
+
performed under Jest. LWC compilation itself is handled separately by `@lwc/rollup-plugin`.
|
|
7
|
+
|
|
8
|
+
> **Status:** skeleton. The scoped-import mock logic lands in a later story.
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@lwc/vitest-plugin",
|
|
3
|
+
"version": "19.7.0-alpha.0",
|
|
4
|
+
"description": "Vite/Vitest plugin that mocks @salesforce/* scoped imports for LWC unit tests",
|
|
5
|
+
"homepage": "https://lwc.dev/",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/salesforce/lwc-test.git",
|
|
9
|
+
"directory": "packages/@lwc/vitest-plugin"
|
|
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
|
+
"keywords": [
|
|
18
|
+
"vitest",
|
|
19
|
+
"vite",
|
|
20
|
+
"lwc"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"/src/**/*.js"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./src/index.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"gitHead": "ded506234f9aac0e8195342811aae7bd22b48742"
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
// Vite plugin: mocks `@salesforce/*` + `@label/` scoped imports for LWC unit tests.
|
|
9
|
+
const SCOPED_IMPORT_PREFIXES = [
|
|
10
|
+
'@salesforce/accessCheck/',
|
|
11
|
+
'@salesforce/apex', // also claims @salesforce/apexContinuation by prefix
|
|
12
|
+
'@salesforce/client/',
|
|
13
|
+
'@salesforce/contentAssetUrl/',
|
|
14
|
+
'@salesforce/customPermission/',
|
|
15
|
+
'@salesforce/i18n/',
|
|
16
|
+
'@salesforce/label/',
|
|
17
|
+
'@label/',
|
|
18
|
+
'@salesforce/messageChannel/',
|
|
19
|
+
'@salesforce/resourceUrl/',
|
|
20
|
+
'@salesforce/schema',
|
|
21
|
+
'@salesforce/user/Id',
|
|
22
|
+
'@salesforce/user/isGuest',
|
|
23
|
+
'@salesforce/userPermission/',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Rollup treats ids beginning with '\0' as virtual, so no other plugin loads them from disk.
|
|
27
|
+
const VIRTUAL_PREFIX = '\0salesforce:';
|
|
28
|
+
|
|
29
|
+
function isMockedSpecifier(source) {
|
|
30
|
+
return SCOPED_IMPORT_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Apex default-method imports (plain + continuation). Both are a callable returning a promise, so
|
|
34
|
+
// they share one fake — Jest mocks them identically. Trailing slash excludes bare `@salesforce/apex`
|
|
35
|
+
// named imports (their own story).
|
|
36
|
+
const APEX_METHOD_PREFIXES = ['@salesforce/apex/', '@salesforce/apexContinuation/'];
|
|
37
|
+
|
|
38
|
+
// Callable spy returning a promise, so both @wire and imperative calls work. Vite caches the
|
|
39
|
+
// virtual module per specifier, so all importers share one spy.
|
|
40
|
+
const APEX_METHOD_SOURCE = `import { vi } from 'vitest';\nexport default vi.fn(() => Promise.resolve());`;
|
|
41
|
+
|
|
42
|
+
// Bare `@salesforce/apex` (exact, no slash): real named exports for the module's API -- refreshApex
|
|
43
|
+
// returns a promise, getSObjectValue is a vi.fn() spy. Must be real `export` statements: Vitest's
|
|
44
|
+
// module runner builds the namespace from export statements only (syntheticNamedExports is Rollup-only).
|
|
45
|
+
const APEX_IMPORT_SPECIFIER = '@salesforce/apex';
|
|
46
|
+
const APEX_NAMED_IMPORTS_SOURCE = `import { vi } from 'vitest';\nexport const refreshApex = () => Promise.resolve();\nexport const getSObjectValue = vi.fn();`;
|
|
47
|
+
|
|
48
|
+
// Mock value is the specifier minus its matched prefix (e.g. `@salesforce/label/c.foo` -> `"c.foo"`).
|
|
49
|
+
const STRING_VALUE_PREFIXES = [
|
|
50
|
+
'@salesforce/label/',
|
|
51
|
+
'@label/', // legacy alias for @salesforce/label/
|
|
52
|
+
'@salesforce/resourceUrl/',
|
|
53
|
+
'@salesforce/contentAssetUrl/',
|
|
54
|
+
'@salesforce/messageChannel/',
|
|
55
|
+
'@salesforce/userPermission/',
|
|
56
|
+
'@salesforce/customPermission/',
|
|
57
|
+
'@salesforce/accessCheck/',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function getStringValue(specifier) {
|
|
61
|
+
const prefix = STRING_VALUE_PREFIXES.find((p) => specifier.startsWith(p));
|
|
62
|
+
return prefix === undefined ? undefined : specifier.slice(prefix.length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// user/* and client/* mock to fixed constants, not values derived from the specifier.
|
|
66
|
+
const USER_DEFAULTS = [
|
|
67
|
+
{ prefix: '@salesforce/user/Id', value: '005000000000000000' },
|
|
68
|
+
{ prefix: '@salesforce/user/isGuest', value: false },
|
|
69
|
+
];
|
|
70
|
+
const CLIENT_PREFIX = '@salesforce/client/';
|
|
71
|
+
const CLIENT_MOCK_VALUES = {
|
|
72
|
+
formFactor: 'Large',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Boxed in { value } so a legitimate `false` (isGuest) isn't mistaken for "no match".
|
|
76
|
+
function getFixedValue(specifier) {
|
|
77
|
+
const userDefault = USER_DEFAULTS.find((d) => specifier.startsWith(d.prefix));
|
|
78
|
+
if (userDefault !== undefined) {
|
|
79
|
+
return { value: userDefault.value };
|
|
80
|
+
}
|
|
81
|
+
if (specifier.startsWith(CLIENT_PREFIX)) {
|
|
82
|
+
const resource = specifier.slice(CLIENT_PREFIX.length);
|
|
83
|
+
return { value: resource in CLIENT_MOCK_VALUES ? CLIENT_MOCK_VALUES[resource] : '' };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const I18N_IMPORT_PREFIX = '@salesforce/i18n/';
|
|
89
|
+
|
|
90
|
+
// Ported from the Jest i18n transform's getMockValue -- keep in sync.
|
|
91
|
+
function mappedI18nValue(key) {
|
|
92
|
+
const parts = key.split('.');
|
|
93
|
+
switch (parts[0]) {
|
|
94
|
+
case 'lang':
|
|
95
|
+
return 'en';
|
|
96
|
+
case 'dir':
|
|
97
|
+
return 'ltr';
|
|
98
|
+
case 'locale':
|
|
99
|
+
return 'en-US';
|
|
100
|
+
case 'timeZone':
|
|
101
|
+
return 'America/Los_Angeles';
|
|
102
|
+
case 'currency':
|
|
103
|
+
return 'USD';
|
|
104
|
+
case 'firstDayOfWeek':
|
|
105
|
+
return 0;
|
|
106
|
+
case 'dateTime':
|
|
107
|
+
switch (parts[1]) {
|
|
108
|
+
case 'shortDateFormat':
|
|
109
|
+
return 'M/d/yyyy';
|
|
110
|
+
case 'mediumDateFormat':
|
|
111
|
+
return 'MMM d, yyyy';
|
|
112
|
+
case 'longDateFormat':
|
|
113
|
+
return 'MMMM d, yyyy';
|
|
114
|
+
case 'shortDateTimeFormat':
|
|
115
|
+
return 'M/d/yyyy h:mm a';
|
|
116
|
+
case 'mediumDateTimeFormat':
|
|
117
|
+
return 'MMM d, yyyy h:mm:ss a';
|
|
118
|
+
case 'shortTimeFormat':
|
|
119
|
+
return 'h:mm a';
|
|
120
|
+
case 'mediumTimeFormat':
|
|
121
|
+
return 'h:mm:ss a';
|
|
122
|
+
default:
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
case 'number':
|
|
126
|
+
switch (parts[1]) {
|
|
127
|
+
case 'numberFormat':
|
|
128
|
+
return '#,##0.###';
|
|
129
|
+
case 'percentFormat':
|
|
130
|
+
return '#,##0%';
|
|
131
|
+
case 'currencyFormat':
|
|
132
|
+
return '¤#,##0.00;(¤#,##0.00)';
|
|
133
|
+
case 'currencySymbol':
|
|
134
|
+
return '$';
|
|
135
|
+
default:
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mapped key -> its value; any other key -> ''
|
|
144
|
+
function getI18nValue(specifier) {
|
|
145
|
+
const key = specifier.slice(I18N_IMPORT_PREFIX.length);
|
|
146
|
+
const mapped = mappedI18nValue(key);
|
|
147
|
+
return mapped === undefined ? '' : mapped;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const SCHEMA_IMPORT_PREFIX = '@salesforce/schema';
|
|
151
|
+
|
|
152
|
+
function getSchemaValue(specifier) {
|
|
153
|
+
const resourcePath = specifier.slice(SCHEMA_IMPORT_PREFIX.length + 1);
|
|
154
|
+
const idx = resourcePath.indexOf('.');
|
|
155
|
+
if (idx === -1) {
|
|
156
|
+
return { objectApiName: resourcePath };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
objectApiName: resourcePath.slice(0, idx),
|
|
160
|
+
fieldApiName: resourcePath.slice(idx + 1),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Narrow claim: only known keys are mocked; unknown @salesforce/site/* pass through to real resolution.
|
|
165
|
+
const SITE_PREFIX = '@salesforce/site/';
|
|
166
|
+
const SITE_VALUES = {
|
|
167
|
+
Id: '005000000000000000',
|
|
168
|
+
activeLanguages: [{ code: 'en-US', label: 'English (US)' }],
|
|
169
|
+
defaultLanguages: { code: 'en-US', label: 'English (US)' },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function getSiteValue(specifier) {
|
|
173
|
+
if (!specifier.startsWith(SITE_PREFIX)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const key = specifier.slice(SITE_PREFIX.length);
|
|
177
|
+
return Object.hasOwn(SITE_VALUES, key) ? SITE_VALUES[key] : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function salesforceScopedImports() {
|
|
181
|
+
return {
|
|
182
|
+
name: '@lwc/vitest-plugin:salesforce-scoped-imports',
|
|
183
|
+
// Run before Vite's core `vite:resolve` so the mock wins even when the specifier also resolves to a real file on disk.
|
|
184
|
+
enforce: 'pre',
|
|
185
|
+
resolveId(source) {
|
|
186
|
+
const claimed = getSiteValue(source) !== undefined || isMockedSpecifier(source);
|
|
187
|
+
return claimed ? VIRTUAL_PREFIX + source : null;
|
|
188
|
+
},
|
|
189
|
+
load(id) {
|
|
190
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const specifier = id.slice(VIRTUAL_PREFIX.length);
|
|
194
|
+
if (APEX_METHOD_PREFIXES.some((prefix) => specifier.startsWith(prefix))) {
|
|
195
|
+
return APEX_METHOD_SOURCE;
|
|
196
|
+
}
|
|
197
|
+
if (specifier === APEX_IMPORT_SPECIFIER) {
|
|
198
|
+
return APEX_NAMED_IMPORTS_SOURCE;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const stringValue = getStringValue(specifier);
|
|
202
|
+
if (stringValue !== undefined) {
|
|
203
|
+
return `export default ${JSON.stringify(stringValue)};`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fixedValue = getFixedValue(specifier);
|
|
207
|
+
if (fixedValue !== null) {
|
|
208
|
+
return `export default ${JSON.stringify(fixedValue.value)};`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (specifier.startsWith(I18N_IMPORT_PREFIX)) {
|
|
212
|
+
return `export default ${JSON.stringify(getI18nValue(specifier))};`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (specifier.startsWith(SCHEMA_IMPORT_PREFIX)) {
|
|
216
|
+
return `export default ${JSON.stringify(getSchemaValue(specifier))};`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const siteValue = getSiteValue(specifier);
|
|
220
|
+
if (siteValue !== undefined) {
|
|
221
|
+
return `export default ${JSON.stringify(siteValue)};`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fallback for shapes without a dedicated value generator yet: echo the specifier.
|
|
225
|
+
return `export default ${JSON.stringify(specifier)};`;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export default salesforceScopedImports;
|