@stencil/vitest 1.12.1 → 1.13.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 +4 -4
- package/dist/__tests__/wizard.spec.d.ts +2 -0
- package/dist/__tests__/wizard.spec.d.ts.map +1 -0
- package/dist/__tests__/wizard.spec.js +359 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -14
- package/dist/environments/env/happy-dom.d.ts.map +1 -1
- package/dist/environments/env/happy-dom.js +27 -0
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +40 -6
- package/dist/wizard.d.ts +3 -0
- package/dist/wizard.d.ts.map +1 -0
- package/dist/wizard.js +488 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -479,7 +479,7 @@ All examples so far have mentioned setting up tests against **pre-built dist out
|
|
|
479
479
|
1. `vi.mock()` cannot intercept imports made by your components, because the dependency is already bundled away before Vitest gets involved.
|
|
480
480
|
2. Coverage reports will not work out-of-the-box without additional configuration (`sourceMap: true` / [3rd party tools](https://github.com/cenfun/vitest-monocart-coverage)) and even then, may not be accurate.
|
|
481
481
|
|
|
482
|
-
The experimental `stencilVitestPlugin` solves this by hooking into Vite's transform pipeline: Stencil files are compiled on-the-fly before Vitest imports them; each component file becomes its own entry in Vitest's module graph
|
|
482
|
+
The experimental `stencilVitestPlugin` solves this by hooking into Vite's transform pipeline: Stencil files are compiled on-the-fly before Vitest imports them; each component file becomes its own entry in Vitest's module graph - and its imports are independently resolvable and mockable.
|
|
483
483
|
|
|
484
484
|
### Setup
|
|
485
485
|
|
|
@@ -497,7 +497,7 @@ export default defineVitestConfig({
|
|
|
497
497
|
test: {
|
|
498
498
|
name: 'plugin',
|
|
499
499
|
include: ['src/**/*.plugin.spec.{ts,tsx}'],
|
|
500
|
-
// No dist setup file needed
|
|
500
|
+
// No dist setup file needed - each component source file registers
|
|
501
501
|
|
|
502
502
|
environment: 'stencil',
|
|
503
503
|
// ^^ you can use the plugin with any setup - even browser tests!
|
|
@@ -535,7 +535,7 @@ It can then be imported and tested with mocked dependencies:
|
|
|
535
535
|
import { describe, it, expect, vi } from 'vitest';
|
|
536
536
|
import { render, h } from '@stencil/vitest';
|
|
537
537
|
|
|
538
|
-
// vi.mock() is hoisted
|
|
538
|
+
// vi.mock() is hoisted - the mock is in place before any imports resolve
|
|
539
539
|
vi.mock('../utils/index.js', () => ({
|
|
540
540
|
capitalize: vi.fn((s: string) => `[mocked:${s}]`),
|
|
541
541
|
}));
|
|
@@ -561,7 +561,7 @@ it('renders using the mocked utility', async () => {
|
|
|
561
561
|
In Stencil v4 `transpile()` (used within the plugin) is a single-file compiler. When a component class `extends` a base class that lives in a separate file, `transpile()` cannot follow the import to merge the parent's metadata and will throw an error.
|
|
562
562
|
|
|
563
563
|
```tsx
|
|
564
|
-
// ❌ Will fail
|
|
564
|
+
// ❌ Will fail - base class is in a separate file
|
|
565
565
|
import { FormBase } from './form-base.js';
|
|
566
566
|
|
|
567
567
|
@Component({ tag: 'my-input', shadow: true })
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/wizard.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { wizard } from '../wizard.js';
|
|
6
|
+
function makeTmpDir() {
|
|
7
|
+
return mkdtempSync(join(tmpdir(), 'wizard-test-'));
|
|
8
|
+
}
|
|
9
|
+
const noCancel = () => false;
|
|
10
|
+
async function callFileTemplates(ctx) {
|
|
11
|
+
const ft = wizard.generate.fileTemplates;
|
|
12
|
+
return typeof ft === 'function' ? [...(await ft(ctx))] : [...(ft ?? [])];
|
|
13
|
+
}
|
|
14
|
+
function makeSpinner() {
|
|
15
|
+
return { start: vi.fn(), stop: vi.fn() };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds a minimal mock WizardContext. `text`, `select`, and `confirm` are
|
|
19
|
+
* called in the order the wizard invokes them, so their return values must be
|
|
20
|
+
* queued with `.mockResolvedValueOnce(...)` before calling `run`.
|
|
21
|
+
*/
|
|
22
|
+
function makeInitCtx(rootDir, outputTargets = [], isNewProject = false) {
|
|
23
|
+
return {
|
|
24
|
+
config: { rootDir, fsNamespace: 'my-lib', outputTargets },
|
|
25
|
+
isNewProject,
|
|
26
|
+
nypm: { addDependency: vi.fn().mockResolvedValue(undefined) },
|
|
27
|
+
prompts: {
|
|
28
|
+
intro: vi.fn(),
|
|
29
|
+
outro: vi.fn(),
|
|
30
|
+
text: vi.fn(),
|
|
31
|
+
select: vi.fn(),
|
|
32
|
+
confirm: vi.fn(),
|
|
33
|
+
isCancel: noCancel,
|
|
34
|
+
cancel: vi.fn(),
|
|
35
|
+
spinner: vi.fn().mockReturnValue(makeSpinner()),
|
|
36
|
+
log: { warn: vi.fn(), info: vi.fn(), error: vi.fn() },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe('wizard.generate.fileTemplates', () => {
|
|
41
|
+
let tmpDir;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tmpDir = makeTmpDir();
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
it('returns two default templates when no vitest.config.ts exists', async () => {
|
|
49
|
+
const ctx = {
|
|
50
|
+
prompts: { text: vi.fn().mockResolvedValue(''), isCancel: noCancel },
|
|
51
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
52
|
+
};
|
|
53
|
+
const templates = await callFileTemplates(ctx);
|
|
54
|
+
expect(templates).toHaveLength(2);
|
|
55
|
+
expect(templates[0].extension).toBe('spec.tsx');
|
|
56
|
+
expect(templates[0].selectedByDefault).toBe(true);
|
|
57
|
+
expect(templates[1].extension).toBe('browser.spec.ts');
|
|
58
|
+
expect(templates[1].selectedByDefault).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('default spec template includes a component import', async () => {
|
|
61
|
+
const ctx = {
|
|
62
|
+
prompts: { text: vi.fn().mockResolvedValue(''), isCancel: noCancel },
|
|
63
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
64
|
+
};
|
|
65
|
+
const templates = await callFileTemplates(ctx);
|
|
66
|
+
const content = templates[0].template('my-button');
|
|
67
|
+
expect(content).toContain("import './my-button'");
|
|
68
|
+
expect(content).toContain("describe('my-button'");
|
|
69
|
+
expect(content).toContain('<my-button />');
|
|
70
|
+
});
|
|
71
|
+
it('default browser template has no component import', async () => {
|
|
72
|
+
const ctx = {
|
|
73
|
+
prompts: { text: vi.fn().mockResolvedValue(''), isCancel: noCancel },
|
|
74
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
75
|
+
};
|
|
76
|
+
const templates = await callFileTemplates(ctx);
|
|
77
|
+
const content = templates[1].template('my-button');
|
|
78
|
+
expect(content).not.toContain("import './my-button'");
|
|
79
|
+
expect(content).toContain("describe('my-button'");
|
|
80
|
+
});
|
|
81
|
+
it('passes subdirectory to templates', async () => {
|
|
82
|
+
const ctx = {
|
|
83
|
+
prompts: { text: vi.fn().mockResolvedValue('__tests__'), isCancel: noCancel },
|
|
84
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
85
|
+
};
|
|
86
|
+
const templates = await callFileTemplates(ctx);
|
|
87
|
+
expect(templates[0].subdirectory).toBe('__tests__');
|
|
88
|
+
});
|
|
89
|
+
it('uses project-based templates when vitest.config.ts has a node project', async () => {
|
|
90
|
+
writeFileSync(join(tmpDir, 'vitest.config.ts'), `export default {
|
|
91
|
+
test: {
|
|
92
|
+
projects: [
|
|
93
|
+
{
|
|
94
|
+
plugins: [{}],
|
|
95
|
+
test: { name: 'unit', include: ['**/*.spec.{ts,tsx}'] },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};\n`);
|
|
100
|
+
const ctx = {
|
|
101
|
+
prompts: { text: vi.fn().mockResolvedValue(''), isCancel: noCancel },
|
|
102
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
103
|
+
};
|
|
104
|
+
const templates = await callFileTemplates(ctx);
|
|
105
|
+
expect(templates).toHaveLength(1);
|
|
106
|
+
expect(templates[0].extension).toBe('spec.tsx');
|
|
107
|
+
expect(templates[0].label).toContain('Unit');
|
|
108
|
+
expect(templates[0].selectedByDefault).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it('marks browser project templates as not selected by default', async () => {
|
|
111
|
+
writeFileSync(join(tmpDir, 'vitest.config.ts'), `export default {
|
|
112
|
+
test: {
|
|
113
|
+
projects: [
|
|
114
|
+
{
|
|
115
|
+
test: {
|
|
116
|
+
name: 'browser',
|
|
117
|
+
include: ['**/*.browser.spec.{ts,tsx}'],
|
|
118
|
+
browser: { enabled: true },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
};\n`);
|
|
124
|
+
const ctx = {
|
|
125
|
+
prompts: { text: vi.fn().mockResolvedValue(''), isCancel: noCancel },
|
|
126
|
+
config: { rootDir: tmpDir, fsNamespace: 'my-lib', outputTargets: [] },
|
|
127
|
+
};
|
|
128
|
+
const templates = await callFileTemplates(ctx);
|
|
129
|
+
expect(templates).toHaveLength(1);
|
|
130
|
+
expect(templates[0].selectedByDefault).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('wizard.init.run', () => {
|
|
134
|
+
let tmpDir;
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
tmpDir = makeTmpDir();
|
|
137
|
+
writeFileSync(join(tmpDir, 'package.json'), JSON.stringify({ name: 'test-lib', scripts: {} }, null, 2) + '\n');
|
|
138
|
+
});
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
it('cancels when existing config exists and user declines overwrite', async () => {
|
|
143
|
+
writeFileSync(join(tmpDir, 'vitest.config.ts'), 'export default {};\n');
|
|
144
|
+
const ctx = makeInitCtx(tmpDir);
|
|
145
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false); // decline overwrite
|
|
146
|
+
await wizard.init.run(ctx);
|
|
147
|
+
expect(ctx.prompts.cancel).toHaveBeenCalledWith('Skipping Vitest setup - existing config kept.');
|
|
148
|
+
expect(readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8')).toBe('export default {};\n');
|
|
149
|
+
});
|
|
150
|
+
it('proceeds when existing config exists and user accepts overwrite', async () => {
|
|
151
|
+
writeFileSync(join(tmpDir, 'vitest.config.ts'), 'export default {};\n');
|
|
152
|
+
const ctx = makeInitCtx(tmpDir);
|
|
153
|
+
ctx.prompts.confirm
|
|
154
|
+
.mockResolvedValueOnce(true) // accept overwrite
|
|
155
|
+
.mockResolvedValueOnce(false); // don't add another
|
|
156
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
157
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
158
|
+
await wizard.init.run(ctx);
|
|
159
|
+
expect(ctx.prompts.cancel).not.toHaveBeenCalled();
|
|
160
|
+
const configContent = readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8');
|
|
161
|
+
expect(configContent).toContain("name: 'unit'");
|
|
162
|
+
});
|
|
163
|
+
it('creates vitest.config.ts with stencilVitestPlugin for a node+plugin project', async () => {
|
|
164
|
+
const ctx = makeInitCtx(tmpDir);
|
|
165
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
166
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
167
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
168
|
+
await wizard.init.run(ctx);
|
|
169
|
+
const config = readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8');
|
|
170
|
+
expect(config).toContain('stencilVitestPlugin()');
|
|
171
|
+
expect(config).toContain("name: 'unit'");
|
|
172
|
+
expect(config).toContain("include: ['**/*.spec.{ts,tsx}']");
|
|
173
|
+
expect(config).not.toContain('setupFiles');
|
|
174
|
+
});
|
|
175
|
+
it('creates vitest.config.ts without plugin for a node+full-build project', async () => {
|
|
176
|
+
const ctx = makeInitCtx(tmpDir, [{ type: 'loader-bundle', dir: join(tmpDir, 'dist/loader-bundle') }]);
|
|
177
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
178
|
+
ctx.prompts.select
|
|
179
|
+
.mockResolvedValueOnce('node')
|
|
180
|
+
.mockResolvedValueOnce('happy-dom')
|
|
181
|
+
.mockResolvedValueOnce('full-build');
|
|
182
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
183
|
+
await wizard.init.run(ctx);
|
|
184
|
+
const config = readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8');
|
|
185
|
+
expect(config).not.toContain('stencilVitestPlugin');
|
|
186
|
+
expect(config).toContain("setupFiles: ['./vitest-setup.ts']");
|
|
187
|
+
expect(config).toContain("domEnvironment: 'happy-dom'");
|
|
188
|
+
});
|
|
189
|
+
it('creates vitest-setup.ts with loader import for a full-build+loader-bundle project', async () => {
|
|
190
|
+
const ctx = makeInitCtx(tmpDir, [{ type: 'loader-bundle', dir: join(tmpDir, 'dist/loader-bundle') }]);
|
|
191
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
192
|
+
ctx.prompts.select
|
|
193
|
+
.mockResolvedValueOnce('node')
|
|
194
|
+
.mockResolvedValueOnce('mock-doc')
|
|
195
|
+
.mockResolvedValueOnce('full-build');
|
|
196
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
197
|
+
await wizard.init.run(ctx);
|
|
198
|
+
expect(existsSync(join(tmpDir, 'vitest-setup.ts'))).toBe(true);
|
|
199
|
+
const setup = readFileSync(join(tmpDir, 'vitest-setup.ts'), 'utf8');
|
|
200
|
+
expect(setup).toContain('my-lib/my-lib.js');
|
|
201
|
+
});
|
|
202
|
+
it('does not overwrite an existing vitest-setup.ts', async () => {
|
|
203
|
+
const originalSetup = "import './existing-loader';\n";
|
|
204
|
+
writeFileSync(join(tmpDir, 'vitest-setup.ts'), originalSetup);
|
|
205
|
+
const ctx = makeInitCtx(tmpDir, [{ type: 'loader-bundle', dir: join(tmpDir, 'dist/loader-bundle') }]);
|
|
206
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
207
|
+
ctx.prompts.select
|
|
208
|
+
.mockResolvedValueOnce('node')
|
|
209
|
+
.mockResolvedValueOnce('mock-doc')
|
|
210
|
+
.mockResolvedValueOnce('full-build');
|
|
211
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
212
|
+
await wizard.init.run(ctx);
|
|
213
|
+
expect(readFileSync(join(tmpDir, 'vitest-setup.ts'), 'utf8')).toBe(originalSetup);
|
|
214
|
+
});
|
|
215
|
+
it('installs jsdom for a jsdom node project', async () => {
|
|
216
|
+
const ctx = makeInitCtx(tmpDir);
|
|
217
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
218
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('jsdom').mockResolvedValueOnce('plugin');
|
|
219
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
220
|
+
await wizard.init.run(ctx);
|
|
221
|
+
expect(ctx.nypm.addDependency).toHaveBeenCalledWith(expect.arrayContaining(['vitest', 'jsdom']), expect.objectContaining({ dev: true }));
|
|
222
|
+
});
|
|
223
|
+
it('installs playwright packages for a browser+playwright project', async () => {
|
|
224
|
+
const ctx = makeInitCtx(tmpDir);
|
|
225
|
+
ctx.prompts.text.mockResolvedValueOnce('browser').mockResolvedValueOnce('**/*.browser.spec.{ts,tsx}');
|
|
226
|
+
ctx.prompts.select.mockResolvedValueOnce('browser').mockResolvedValueOnce('playwright');
|
|
227
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
228
|
+
await wizard.init.run(ctx);
|
|
229
|
+
expect(ctx.nypm.addDependency).toHaveBeenCalledWith(expect.arrayContaining(['vitest', '@vitest/browser', '@vitest/browser-playwright', 'playwright']), expect.objectContaining({ dev: true }));
|
|
230
|
+
});
|
|
231
|
+
it('installs wdio packages for a browser+webdriverio project', async () => {
|
|
232
|
+
const ctx = makeInitCtx(tmpDir);
|
|
233
|
+
ctx.prompts.text.mockResolvedValueOnce('browser').mockResolvedValueOnce('**/*.browser.spec.{ts,tsx}');
|
|
234
|
+
ctx.prompts.select.mockResolvedValueOnce('browser').mockResolvedValueOnce('wdio');
|
|
235
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
236
|
+
await wizard.init.run(ctx);
|
|
237
|
+
expect(ctx.nypm.addDependency).toHaveBeenCalledWith(expect.arrayContaining(['vitest', '@vitest/browser', '@vitest/browser-webdriverio']), expect.objectContaining({ dev: true }));
|
|
238
|
+
expect(ctx.nypm.addDependency).not.toHaveBeenCalledWith(expect.arrayContaining(['playwright']), expect.anything());
|
|
239
|
+
});
|
|
240
|
+
it('writes test and test:<name> scripts to package.json', async () => {
|
|
241
|
+
const ctx = makeInitCtx(tmpDir);
|
|
242
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
243
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
244
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
245
|
+
await wizard.init.run(ctx);
|
|
246
|
+
const pkg = JSON.parse(readFileSync(join(tmpDir, 'package.json'), 'utf8'));
|
|
247
|
+
expect(pkg.scripts.test).toBe('vitest run');
|
|
248
|
+
expect(pkg.scripts['test:unit']).toBe('vitest run --project unit');
|
|
249
|
+
});
|
|
250
|
+
it('uses stencil-test script for a full-build project', async () => {
|
|
251
|
+
const ctx = makeInitCtx(tmpDir, [{ type: 'loader-bundle', dir: join(tmpDir, 'dist/loader-bundle') }]);
|
|
252
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
253
|
+
ctx.prompts.select
|
|
254
|
+
.mockResolvedValueOnce('node')
|
|
255
|
+
.mockResolvedValueOnce('mock-doc')
|
|
256
|
+
.mockResolvedValueOnce('full-build');
|
|
257
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
258
|
+
await wizard.init.run(ctx);
|
|
259
|
+
const pkg = JSON.parse(readFileSync(join(tmpDir, 'package.json'), 'utf8'));
|
|
260
|
+
expect(pkg.scripts.test).toBe('stencil-test');
|
|
261
|
+
expect(pkg.scripts['test:unit']).toBe('stencil-test --project unit');
|
|
262
|
+
});
|
|
263
|
+
it('does not overwrite existing package.json test scripts', async () => {
|
|
264
|
+
writeFileSync(join(tmpDir, 'package.json'), JSON.stringify({ name: 'test-lib', scripts: { test: 'my-custom-test' } }, null, 2) + '\n');
|
|
265
|
+
const ctx = makeInitCtx(tmpDir);
|
|
266
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
267
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
268
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
269
|
+
await wizard.init.run(ctx);
|
|
270
|
+
const pkg = JSON.parse(readFileSync(join(tmpDir, 'package.json'), 'utf8'));
|
|
271
|
+
expect(pkg.scripts.test).toBe('my-custom-test');
|
|
272
|
+
});
|
|
273
|
+
it('generates example spec files for components when isNewProject is true', async () => {
|
|
274
|
+
const componentDir = join(tmpDir, 'src', 'components', 'my-button');
|
|
275
|
+
mkdirSync(componentDir, { recursive: true });
|
|
276
|
+
writeFileSync(join(componentDir, 'my-button.tsx'), `import { Component, h } from '@stencil/core';
|
|
277
|
+
@Component({ tag: 'my-button', shadow: true })
|
|
278
|
+
export class MyButton { render() { return <button />; } }
|
|
279
|
+
`);
|
|
280
|
+
const ctx = makeInitCtx(tmpDir, [], true);
|
|
281
|
+
ctx.prompts.text
|
|
282
|
+
.mockResolvedValueOnce('unit') // project name
|
|
283
|
+
.mockResolvedValueOnce('**/*.spec.{ts,tsx}') // pattern
|
|
284
|
+
.mockResolvedValueOnce(''); // subdirectory (empty = co-locate)
|
|
285
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
286
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
287
|
+
await wizard.init.run(ctx);
|
|
288
|
+
const specFile = join(componentDir, 'my-button.spec.tsx');
|
|
289
|
+
expect(existsSync(specFile)).toBe(true);
|
|
290
|
+
const spec = readFileSync(specFile, 'utf8');
|
|
291
|
+
expect(spec).toContain("describe('my-button'");
|
|
292
|
+
expect(spec).toContain('<my-button />');
|
|
293
|
+
});
|
|
294
|
+
it('generates example spec files into a subdirectory for new projects', async () => {
|
|
295
|
+
const componentDir = join(tmpDir, 'src', 'components', 'my-button');
|
|
296
|
+
mkdirSync(componentDir, { recursive: true });
|
|
297
|
+
writeFileSync(join(componentDir, 'my-button.tsx'), `import { Component, h } from '@stencil/core';
|
|
298
|
+
@Component({ tag: 'my-button', shadow: true })
|
|
299
|
+
export class MyButton { render() { return <button />; } }
|
|
300
|
+
`);
|
|
301
|
+
const ctx = makeInitCtx(tmpDir, [], true);
|
|
302
|
+
ctx.prompts.text
|
|
303
|
+
.mockResolvedValueOnce('unit')
|
|
304
|
+
.mockResolvedValueOnce('**/*.spec.{ts,tsx}')
|
|
305
|
+
.mockResolvedValueOnce('__tests__');
|
|
306
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
307
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
308
|
+
await wizard.init.run(ctx);
|
|
309
|
+
const specFile = join(componentDir, '__tests__', 'my-button.spec.tsx');
|
|
310
|
+
expect(existsSync(specFile)).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it('skips example test generation when isNewProject is false', async () => {
|
|
313
|
+
const componentDir = join(tmpDir, 'src', 'components', 'my-button');
|
|
314
|
+
mkdirSync(componentDir, { recursive: true });
|
|
315
|
+
writeFileSync(join(componentDir, 'my-button.tsx'), `import { Component, h } from '@stencil/core';
|
|
316
|
+
@Component({ tag: 'my-button', shadow: true })
|
|
317
|
+
export class MyButton { render() { return <button />; } }
|
|
318
|
+
`);
|
|
319
|
+
const ctx = makeInitCtx(tmpDir, [], false);
|
|
320
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
321
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
322
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
323
|
+
await wizard.init.run(ctx);
|
|
324
|
+
expect(existsSync(join(componentDir, 'my-button.spec.tsx'))).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
it('includes stencilConfig option when stencil.config.ts is present', async () => {
|
|
327
|
+
writeFileSync(join(tmpDir, 'stencil.config.ts'), 'export const config = {};\n');
|
|
328
|
+
const ctx = makeInitCtx(tmpDir);
|
|
329
|
+
ctx.prompts.text.mockResolvedValueOnce('unit').mockResolvedValueOnce('**/*.spec.{ts,tsx}');
|
|
330
|
+
ctx.prompts.select.mockResolvedValueOnce('node').mockResolvedValueOnce('mock-doc').mockResolvedValueOnce('plugin');
|
|
331
|
+
ctx.prompts.confirm.mockResolvedValueOnce(false);
|
|
332
|
+
await wizard.init.run(ctx);
|
|
333
|
+
const config = readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8');
|
|
334
|
+
expect(config).toContain("stencilConfig: './stencil.config.ts'");
|
|
335
|
+
});
|
|
336
|
+
it('generates correct config for two projects', async () => {
|
|
337
|
+
const ctx = makeInitCtx(tmpDir);
|
|
338
|
+
ctx.prompts.text
|
|
339
|
+
.mockResolvedValueOnce('unit')
|
|
340
|
+
.mockResolvedValueOnce('**/*.spec.{ts,tsx}')
|
|
341
|
+
.mockResolvedValueOnce('browser')
|
|
342
|
+
.mockResolvedValueOnce('**/*.browser.spec.{ts,tsx}');
|
|
343
|
+
ctx.prompts.select
|
|
344
|
+
.mockResolvedValueOnce('node')
|
|
345
|
+
.mockResolvedValueOnce('mock-doc')
|
|
346
|
+
.mockResolvedValueOnce('plugin')
|
|
347
|
+
.mockResolvedValueOnce('browser')
|
|
348
|
+
.mockResolvedValueOnce('playwright');
|
|
349
|
+
ctx.prompts.confirm
|
|
350
|
+
.mockResolvedValueOnce(true) // add another
|
|
351
|
+
.mockResolvedValueOnce(false); // stop
|
|
352
|
+
await wizard.init.run(ctx);
|
|
353
|
+
const config = readFileSync(join(tmpDir, 'vitest.config.ts'), 'utf8');
|
|
354
|
+
expect(config).toContain("name: 'unit'");
|
|
355
|
+
expect(config).toContain("name: 'browser'");
|
|
356
|
+
expect(config).toContain('stencilVitestPlugin()');
|
|
357
|
+
expect(config).toContain('playwright()');
|
|
358
|
+
});
|
|
359
|
+
});
|
package/dist/config.d.ts
CHANGED
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAQlE,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,eAAe,CAAC;AAM7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,GAAE,cAAc,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,GAAG,aAAa,CAAA;CAAO,GACvE,OAAO,CAAC,cAAc,CAAC,CAqBzB"}
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { defineConfig } from 'vitest/config';
|
|
3
5
|
import { loadStencilConfig, getStencilSrcDir, getStencilOutputDirs, getStencilResolveAliases, getStencilHydratedFlag, } from './setup/config-loader.js';
|
|
@@ -118,6 +120,30 @@ function generateCoverageExcludes(testIncludes, srcDir) {
|
|
|
118
120
|
}
|
|
119
121
|
return excludes;
|
|
120
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Read JSX options from the nearest tsconfig.json.
|
|
125
|
+
* Returns 'automatic' mode when the project uses `"jsx": "react-jsx"` so that
|
|
126
|
+
* Vite's transformer honours jsxImportSource instead of forcing classic `h`.
|
|
127
|
+
*/
|
|
128
|
+
function readTsConfigJsxOptions(cwd) {
|
|
129
|
+
const tsconfigPath = join(cwd, 'tsconfig.json');
|
|
130
|
+
if (!existsSync(tsconfigPath))
|
|
131
|
+
return { mode: 'classic' };
|
|
132
|
+
try {
|
|
133
|
+
// tsconfig is JSONC — strip line and block comments before parsing
|
|
134
|
+
const raw = readFileSync(tsconfigPath, 'utf-8');
|
|
135
|
+
const stripped = raw.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
136
|
+
const tsconfig = JSON.parse(stripped);
|
|
137
|
+
const jsx = tsconfig?.compilerOptions?.jsx;
|
|
138
|
+
if (jsx === 'react-jsx' || jsx === 'react-jsxdev') {
|
|
139
|
+
return { mode: 'automatic', importSource: tsconfig?.compilerOptions?.jsxImportSource };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// ignore unreadable / malformed tsconfig
|
|
144
|
+
}
|
|
145
|
+
return { mode: 'classic' };
|
|
146
|
+
}
|
|
121
147
|
/**
|
|
122
148
|
* Apply Stencil-specific defaults to Vitest config
|
|
123
149
|
*/
|
|
@@ -142,24 +168,29 @@ function applyStencilDefaults(config, stencilConfig) {
|
|
|
142
168
|
...result.define,
|
|
143
169
|
};
|
|
144
170
|
}
|
|
145
|
-
// Add JSX config for the active transformer
|
|
146
|
-
// Vite 7+ uses oxc by default, older versions use esbuild
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
// Add JSX config for the active transformer, honouring the project's tsconfig.
|
|
172
|
+
// Vite 7+ uses oxc by default, older versions use esbuild.
|
|
173
|
+
const jsxOpts = readTsConfigJsxOptions(process.cwd());
|
|
174
|
+
// oxc is Vite 8+'s default transformer; esbuild is the fallback for older Vite.
|
|
175
|
+
// Set oxc jsx unless the user explicitly disabled it (`oxc: false`), in which
|
|
176
|
+
// case esbuild is still active and needs the jsx settings instead.
|
|
177
|
+
const oxcDisabled = result.oxc === false;
|
|
178
|
+
if (!oxcDisabled) {
|
|
179
|
+
if (!result.oxc) {
|
|
180
|
+
result.oxc = {};
|
|
181
|
+
}
|
|
149
182
|
if (!result.oxc.jsx) {
|
|
150
|
-
result.oxc.jsx =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
183
|
+
result.oxc.jsx =
|
|
184
|
+
jsxOpts.mode === 'automatic'
|
|
185
|
+
? { runtime: 'automatic', importSource: jsxOpts.importSource }
|
|
186
|
+
: { runtime: 'classic', pragma: 'h', pragmaFrag: 'Fragment' };
|
|
155
187
|
}
|
|
156
188
|
}
|
|
157
189
|
else if (!result.esbuild) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
190
|
+
result.esbuild =
|
|
191
|
+
jsxOpts.mode === 'automatic'
|
|
192
|
+
? { jsx: 'automatic', jsxImportSource: jsxOpts.importSource }
|
|
193
|
+
: { jsxFactory: 'h', jsxFragment: 'Fragment' };
|
|
163
194
|
}
|
|
164
195
|
// Add resolve aliases from Stencil config if not present
|
|
165
196
|
if (!result.resolve) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"happy-dom.d.ts","sourceRoot":"","sources":["../../../src/environments/env/happy-dom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;wBAEtC,kBAAkB;AAAlC,
|
|
1
|
+
{"version":3,"file":"happy-dom.d.ts","sourceRoot":"","sources":["../../../src/environments/env/happy-dom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;wBAEtC,kBAAkB;AAAlC,wBAiDE"}
|
|
@@ -6,6 +6,33 @@ export default (async function (_global, options) {
|
|
|
6
6
|
...options.happyDom,
|
|
7
7
|
};
|
|
8
8
|
const window = new (GlobalWindow || Window)(happyDomOptions);
|
|
9
|
+
// happy-dom does not implement the Custom Element upgrade algorithm.
|
|
10
|
+
// When the lazy-loader pattern is used (elements created before customElements.define
|
|
11
|
+
// is called), browsers and jsdom automatically upgrade those elements by changing their
|
|
12
|
+
// prototype and firing connectedCallback. Patch define() to emulate that step.
|
|
13
|
+
const origDefine = window.customElements.define.bind(window.customElements);
|
|
14
|
+
window.customElements.define = function (name, elementClass, options) {
|
|
15
|
+
console.log(`[happy-dom-upgrade] customElements.define called for <${name}>`);
|
|
16
|
+
origDefine(name, elementClass, options);
|
|
17
|
+
const existing = Array.from(window.document.querySelectorAll(name));
|
|
18
|
+
console.log(`[happy-dom-upgrade] found ${existing.length} existing <${name}> element(s) to upgrade`);
|
|
19
|
+
for (const el of existing) {
|
|
20
|
+
console.log(`[happy-dom-upgrade] upgrading <${name}>: isConnected=${el.isConnected}, has __s_ghr=${!!el.__s_ghr}, proto=${Object.getPrototypeOf(el)?.constructor?.name}`);
|
|
21
|
+
if (el.__s_ghr) {
|
|
22
|
+
console.log(`[happy-dom-upgrade] skipping — already initialised by Stencil`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
Object.setPrototypeOf(el, elementClass.prototype);
|
|
26
|
+
console.log(`[happy-dom-upgrade] prototype set, has __registerHost=${typeof el.__registerHost === 'function'}, has __attachShadow=${typeof el.__attachShadow === 'function'}, has connectedCallback=${typeof el.connectedCallback === 'function'}`);
|
|
27
|
+
if (typeof el.__registerHost === 'function')
|
|
28
|
+
el.__registerHost();
|
|
29
|
+
if (typeof el.__attachShadow === 'function')
|
|
30
|
+
el.__attachShadow();
|
|
31
|
+
if (el.isConnected && typeof el.connectedCallback === 'function')
|
|
32
|
+
el.connectedCallback();
|
|
33
|
+
console.log(`[happy-dom-upgrade] done upgrading <${name}>`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
9
36
|
return {
|
|
10
37
|
window,
|
|
11
38
|
teardown() {
|
package/dist/plugin.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Plugin } from 'vitest/config';
|
|
|
5
5
|
*
|
|
6
6
|
* The compiled output uses `componentExport: 'customelement'`, which appends a
|
|
7
7
|
* `customElements.define()` call at the end of the transformed file. The component
|
|
8
|
-
* registers itself the moment the module is imported
|
|
8
|
+
* registers itself the moment the module is imported - no dist loader or setup file
|
|
9
9
|
* required. Works with `@stencil/core` v4 and v5.
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -23,7 +23,7 @@ import type { Plugin } from 'vitest/config';
|
|
|
23
23
|
* name: 'stencil',
|
|
24
24
|
* environment: 'stencil',
|
|
25
25
|
* include: ['**\/*.spec.tsx'],
|
|
26
|
-
* // No dist loader needed
|
|
26
|
+
* // No dist loader needed - import components from source directly
|
|
27
27
|
* },
|
|
28
28
|
* },
|
|
29
29
|
* ],
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAkC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAkGxE"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
import { transpile } from '@stencil/core/compiler';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
// TypeScript JsxEmit enum values — stable across versions
|
|
5
|
+
const JSX_EMIT = {
|
|
6
|
+
react: 2,
|
|
7
|
+
'react-jsx': 4,
|
|
8
|
+
'react-jsxdev': 5,
|
|
9
|
+
preserve: 1,
|
|
10
|
+
'react-native': 3,
|
|
11
|
+
};
|
|
12
|
+
let cachedJsxOpts;
|
|
13
|
+
function readJsxOptsFromTsConfig(cwd) {
|
|
14
|
+
if (cachedJsxOpts !== undefined)
|
|
15
|
+
return cachedJsxOpts;
|
|
16
|
+
const tsconfigPath = join(cwd, 'tsconfig.json');
|
|
17
|
+
if (!existsSync(tsconfigPath))
|
|
18
|
+
return (cachedJsxOpts = {});
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(tsconfigPath, 'utf-8');
|
|
21
|
+
const stripped = raw.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
22
|
+
const tsconfig = JSON.parse(stripped);
|
|
23
|
+
const jsxStr = tsconfig?.compilerOptions?.jsx?.toLowerCase();
|
|
24
|
+
if (jsxStr && jsxStr in JSX_EMIT) {
|
|
25
|
+
return (cachedJsxOpts = {
|
|
26
|
+
jsx: JSX_EMIT[jsxStr],
|
|
27
|
+
jsxImportSource: tsconfig?.compilerOptions?.jsxImportSource,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore unreadable / malformed tsconfig
|
|
33
|
+
}
|
|
34
|
+
return (cachedJsxOpts = {});
|
|
35
|
+
}
|
|
4
36
|
/**
|
|
5
37
|
* A Vite/Vitest plugin that transforms Stencil component source files (.tsx) on-the-fly,
|
|
6
38
|
* enabling module mocking and direct source imports during tests.
|
|
7
39
|
*
|
|
8
40
|
* The compiled output uses `componentExport: 'customelement'`, which appends a
|
|
9
41
|
* `customElements.define()` call at the end of the transformed file. The component
|
|
10
|
-
* registers itself the moment the module is imported
|
|
42
|
+
* registers itself the moment the module is imported - no dist loader or setup file
|
|
11
43
|
* required. Works with `@stencil/core` v4 and v5.
|
|
12
44
|
*
|
|
13
45
|
* @example
|
|
@@ -25,7 +57,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
25
57
|
* name: 'stencil',
|
|
26
58
|
* environment: 'stencil',
|
|
27
59
|
* include: ['**\/*.spec.tsx'],
|
|
28
|
-
* // No dist loader needed
|
|
60
|
+
* // No dist loader needed - import components from source directly
|
|
29
61
|
* },
|
|
30
62
|
* },
|
|
31
63
|
* ],
|
|
@@ -88,10 +120,11 @@ export function stencilVitestPlugin(opts = {}) {
|
|
|
88
120
|
return null;
|
|
89
121
|
}
|
|
90
122
|
try {
|
|
123
|
+
const jsxOpts = readJsxOptsFromTsConfig(process.cwd());
|
|
91
124
|
const result = await transpile(code, {
|
|
92
125
|
file: id,
|
|
93
126
|
// 'customelement' appends a customElements.define() call so the component
|
|
94
|
-
// self-registers the moment this module is imported
|
|
127
|
+
// self-registers the moment this module is imported - no loader needed.
|
|
95
128
|
componentExport: 'customelement',
|
|
96
129
|
componentMetadata: 'runtimestatic',
|
|
97
130
|
currentDirectory: process.cwd(),
|
|
@@ -101,8 +134,9 @@ export function stencilVitestPlugin(opts = {}) {
|
|
|
101
134
|
style: opts.css ? 'static' : null,
|
|
102
135
|
styleImportData: opts.css ? 'queryparams' : null,
|
|
103
136
|
target: 'es2017',
|
|
104
|
-
// Don't rewrite import paths
|
|
137
|
+
// Don't rewrite import paths - let Vite handle resolution via aliases
|
|
105
138
|
transformAliasedImportPaths: false,
|
|
139
|
+
...jsxOpts,
|
|
106
140
|
});
|
|
107
141
|
const errors = result.diagnostics?.filter((d) => d.level === 'error') ?? [];
|
|
108
142
|
if (errors.length > 0) {
|
package/dist/wizard.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../src/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkC,mBAAmB,EAAiB,MAAM,cAAc,CAAC;AAqcvG,eAAO,MAAM,MAAM,EAAE,mBAkIpB,CAAC"}
|
package/dist/wizard.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
/** Reads the existing vitest.config.ts (if any) and extracts project name/pattern/type metadata. */
|
|
4
|
+
async function loadVitestProjects(rootDir) {
|
|
5
|
+
const configPath = join(rootDir, 'vitest.config.ts');
|
|
6
|
+
if (!(await fileExists(configPath)))
|
|
7
|
+
return [];
|
|
8
|
+
try {
|
|
9
|
+
const { createJiti } = await import('jiti');
|
|
10
|
+
const jiti = createJiti(rootDir, { interopDefault: true, moduleCache: false });
|
|
11
|
+
const mod = (await jiti.import(configPath));
|
|
12
|
+
const config = await (mod?.default ?? mod);
|
|
13
|
+
const projects = config?.test?.projects ?? [];
|
|
14
|
+
return projects.flatMap((p) => {
|
|
15
|
+
const test = p?.test ?? {};
|
|
16
|
+
const name = test.name;
|
|
17
|
+
const pattern = test.include?.[0];
|
|
18
|
+
if (!name || !pattern)
|
|
19
|
+
return [];
|
|
20
|
+
if (test.browser?.enabled === true) {
|
|
21
|
+
return [{ type: 'browser', name, pattern }];
|
|
22
|
+
}
|
|
23
|
+
const loadMethod = Array.isArray(p.plugins) && p.plugins.length > 0 ? 'plugin' : 'full-build';
|
|
24
|
+
return [{ type: 'node', name, pattern, loadMethod }];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function fileExists(path) {
|
|
32
|
+
try {
|
|
33
|
+
await access(path);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the relative import path for the component loader to use in vitest-setup.ts.
|
|
42
|
+
* Prefers loader-bundle (works in dev + prod); falls back to standalone's dev-mode loader.
|
|
43
|
+
* Returns null if neither output target is configured or the standalone loader is disabled.
|
|
44
|
+
*/
|
|
45
|
+
function resolveLoaderImport(config) {
|
|
46
|
+
const ns = config.fsNamespace;
|
|
47
|
+
const outputs = config.outputTargets;
|
|
48
|
+
const loaderBundle = outputs.find((o) => o.type === 'loader-bundle');
|
|
49
|
+
if (loaderBundle) {
|
|
50
|
+
// buildDir resolves to dir by default; the browser CDN build lands at {buildDir}/{namespace}/
|
|
51
|
+
const buildDir = loaderBundle.buildDir ?? loaderBundle.dir ?? 'dist/loader-bundle';
|
|
52
|
+
return `./${relative(config.rootDir, buildDir)}/${ns}/${ns}.js`;
|
|
53
|
+
}
|
|
54
|
+
const standalone = outputs.find((o) => o.type === 'standalone');
|
|
55
|
+
if (standalone) {
|
|
56
|
+
if (standalone.autoLoader === false)
|
|
57
|
+
return null;
|
|
58
|
+
const dir = standalone.dir ?? 'dist/standalone';
|
|
59
|
+
const fileName = typeof standalone.autoLoader === 'object' ? (standalone.autoLoader.fileName ?? 'loader') : 'loader';
|
|
60
|
+
return `./${relative(config.rootDir, dir)}/${fileName}.js`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Checks which distribution output targets are present, to drive the load-method prompt. */
|
|
65
|
+
function detectOutputTargets(config) {
|
|
66
|
+
const outputs = config.outputTargets;
|
|
67
|
+
const hasLoaderBundle = outputs.some((o) => o.type === 'loader-bundle' || o.type === 'dist');
|
|
68
|
+
const hasStandalone = outputs.some((o) => o.type === 'standalone' || o.type === 'dist-custom-elements');
|
|
69
|
+
if (!hasLoaderBundle && !hasStandalone)
|
|
70
|
+
return { hasLoaderBundle: true, hasStandalone: false };
|
|
71
|
+
return { hasLoaderBundle, hasStandalone };
|
|
72
|
+
}
|
|
73
|
+
function defaultPattern(name) {
|
|
74
|
+
if (name === 'unit')
|
|
75
|
+
return '**/*.spec.{ts,tsx}';
|
|
76
|
+
return `**/*.${name}.spec.{ts,tsx}`;
|
|
77
|
+
}
|
|
78
|
+
/** Runs the interactive prompts for a single Vitest project and returns its configuration. */
|
|
79
|
+
async function promptProject(prompts, usedPatterns, outputTargets, isFirst, askSubdirectory) {
|
|
80
|
+
const { text, select, isCancel, cancel, log } = prompts;
|
|
81
|
+
const rawName = await text({
|
|
82
|
+
message: 'Project name? (label for this group of tests - e.g. unit, browser, jsdom)',
|
|
83
|
+
placeholder: isFirst ? 'unit' : 'e.g. unit, browser, jsdom',
|
|
84
|
+
defaultValue: isFirst ? 'unit' : '',
|
|
85
|
+
validate: (v) => (!v?.trim() && !isFirst ? 'Name is required' : undefined),
|
|
86
|
+
});
|
|
87
|
+
if (isCancel(rawName)) {
|
|
88
|
+
cancel('Setup cancelled.');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const name = rawName;
|
|
92
|
+
const projectType = await select({
|
|
93
|
+
message: 'Node-based or browser-based?',
|
|
94
|
+
options: [
|
|
95
|
+
{ value: 'node', label: 'Node', hint: 'unit / component tests - mock or emulated DOM' },
|
|
96
|
+
{ value: 'browser', label: 'Browser', hint: 'real Chromium - interactions, screenshots, visual' },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
if (isCancel(projectType)) {
|
|
100
|
+
cancel('Setup cancelled.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const patternDefault = defaultPattern(name);
|
|
104
|
+
const rawPattern = await text({
|
|
105
|
+
message: 'Test file pattern?',
|
|
106
|
+
placeholder: patternDefault,
|
|
107
|
+
defaultValue: patternDefault,
|
|
108
|
+
});
|
|
109
|
+
if (isCancel(rawPattern)) {
|
|
110
|
+
cancel('Setup cancelled.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
const pattern = rawPattern;
|
|
114
|
+
if (usedPatterns.has(pattern)) {
|
|
115
|
+
log.warn(`"${pattern}" is already claimed by another project - files will run in both.`);
|
|
116
|
+
}
|
|
117
|
+
usedPatterns.add(pattern);
|
|
118
|
+
let subdirectory;
|
|
119
|
+
if (askSubdirectory) {
|
|
120
|
+
const rawSubdir = await text({
|
|
121
|
+
message: 'Test file subdirectory? (leave blank to co-locate with component)',
|
|
122
|
+
placeholder: 'e.g. __tests__',
|
|
123
|
+
defaultValue: '',
|
|
124
|
+
});
|
|
125
|
+
if (isCancel(rawSubdir)) {
|
|
126
|
+
cancel('Setup cancelled.');
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
subdirectory = rawSubdir.trim() || undefined;
|
|
130
|
+
}
|
|
131
|
+
if (projectType === 'browser') {
|
|
132
|
+
const provider = await select({
|
|
133
|
+
message: 'Browser provider?',
|
|
134
|
+
options: [
|
|
135
|
+
{ value: 'playwright', label: 'Playwright', hint: 'chromium / firefox / webkit' },
|
|
136
|
+
{ value: 'wdio', label: 'WebdriverIO', hint: 'selenium-compatible, more browser variety' },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
if (isCancel(provider)) {
|
|
140
|
+
cancel('Setup cancelled.');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
return { type: 'browser', name, pattern, subdirectory, provider: provider };
|
|
144
|
+
}
|
|
145
|
+
// Node-based
|
|
146
|
+
const env = await select({
|
|
147
|
+
message: 'DOM environment?',
|
|
148
|
+
options: [
|
|
149
|
+
{ value: 'mock-doc', label: 'mock-doc', hint: 'Fastest - Stencil-native' },
|
|
150
|
+
{ value: 'jsdom', label: 'jsdom', hint: 'Full DOM emulation' },
|
|
151
|
+
{ value: 'happy-dom', label: 'happy-dom', hint: 'Faster than jsdom' },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
if (isCancel(env)) {
|
|
155
|
+
cancel('Setup cancelled.');
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
const loadMethod = await select({
|
|
159
|
+
message: 'How should components be loaded?',
|
|
160
|
+
options: [
|
|
161
|
+
{ value: 'plugin', label: 'Plugin (source)', hint: 'compile on-the-fly - module mocking, accurate coverage' },
|
|
162
|
+
{ value: 'full-build', label: 'Full build (dist)', hint: 'test the actual output - no mocking, no coverage' },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
if (isCancel(loadMethod)) {
|
|
166
|
+
cancel('Setup cancelled.');
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
let outputTarget = null;
|
|
170
|
+
if (loadMethod === 'full-build') {
|
|
171
|
+
if (outputTargets.hasLoaderBundle && outputTargets.hasStandalone) {
|
|
172
|
+
const ot = await select({
|
|
173
|
+
message: 'Which output to load from?',
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'loader-bundle', label: 'loader-bundle', hint: 'lazy loader path - defineCustomElements()' },
|
|
176
|
+
{ value: 'standalone', label: 'standalone', hint: 'direct imports - components auto-register on import' },
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
if (isCancel(ot)) {
|
|
180
|
+
cancel('Setup cancelled.');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
outputTarget = ot;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
outputTarget = outputTargets.hasStandalone ? 'standalone' : 'loader-bundle';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
type: 'node',
|
|
191
|
+
name,
|
|
192
|
+
pattern,
|
|
193
|
+
subdirectory,
|
|
194
|
+
env: env,
|
|
195
|
+
loadMethod: loadMethod,
|
|
196
|
+
outputTarget,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Returns the set of npm packages that need to be installed for the chosen projects. */
|
|
200
|
+
function collectDeps(projects) {
|
|
201
|
+
const deps = new Set(['vitest']);
|
|
202
|
+
for (const p of projects) {
|
|
203
|
+
if (p.type === 'node') {
|
|
204
|
+
if (p.env === 'jsdom')
|
|
205
|
+
deps.add('jsdom');
|
|
206
|
+
if (p.env === 'happy-dom')
|
|
207
|
+
deps.add('happy-dom');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
deps.add('@vitest/browser');
|
|
211
|
+
if (p.provider === 'playwright') {
|
|
212
|
+
deps.add('@vitest/browser-playwright');
|
|
213
|
+
deps.add('playwright');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
deps.add('@vitest/browser-webdriverio');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [...deps];
|
|
221
|
+
}
|
|
222
|
+
/** Renders a single Vitest workspace project block for vitest.config.ts. */
|
|
223
|
+
function generateProjectBlock(project, excludePatterns) {
|
|
224
|
+
const excludeLine = excludePatterns.length > 0 ? `\n exclude: [${excludePatterns.map((p) => `'${p}'`).join(', ')}],` : '';
|
|
225
|
+
if (project.type === 'browser') {
|
|
226
|
+
const providerExpr = project.provider === 'playwright' ? 'playwright()' : 'webdriverio()';
|
|
227
|
+
return ` {
|
|
228
|
+
test: {
|
|
229
|
+
name: '${project.name}',
|
|
230
|
+
include: ['${project.pattern}'],${excludeLine}
|
|
231
|
+
setupFiles: ['./vitest-setup.ts'],
|
|
232
|
+
browser: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
provider: ${providerExpr},
|
|
235
|
+
headless: true,
|
|
236
|
+
instances: [{ browser: 'chromium' }],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
}`;
|
|
240
|
+
}
|
|
241
|
+
const envOptions = project.env !== 'mock-doc'
|
|
242
|
+
? `
|
|
243
|
+
environmentOptions: {
|
|
244
|
+
stencil: { domEnvironment: '${project.env}' },
|
|
245
|
+
},`
|
|
246
|
+
: '';
|
|
247
|
+
const pluginsLine = project.loadMethod === 'plugin' ? '\n plugins: [stencilVitestPlugin()],' : '';
|
|
248
|
+
const setupLine = project.loadMethod === 'full-build' ? `\n setupFiles: ['./vitest-setup.ts'],` : '';
|
|
249
|
+
return ` {${pluginsLine}
|
|
250
|
+
test: {
|
|
251
|
+
name: '${project.name}',
|
|
252
|
+
environment: 'stencil',${envOptions}${setupLine}
|
|
253
|
+
include: ['${project.pattern}'],${excludeLine}
|
|
254
|
+
},
|
|
255
|
+
}`;
|
|
256
|
+
}
|
|
257
|
+
/** Generates the full vitest.config.ts file content for the chosen set of projects. */
|
|
258
|
+
function generateVitestConfig(projects, hasStencilConfig) {
|
|
259
|
+
const needsPlugin = projects.some((p) => p.type === 'node' && p.loadMethod === 'plugin');
|
|
260
|
+
const needsPlaywright = projects.some((p) => p.type === 'browser' && p.provider === 'playwright');
|
|
261
|
+
const needsWdio = projects.some((p) => p.type === 'browser' && p.provider === 'wdio');
|
|
262
|
+
const imports = [
|
|
263
|
+
`import { defineVitestConfig } from '@stencil/vitest/config';`,
|
|
264
|
+
needsPlugin ? `import { stencilVitestPlugin } from '@stencil/vitest/plugin';` : null,
|
|
265
|
+
needsPlaywright ? `import { playwright } from '@vitest/browser-playwright';` : null,
|
|
266
|
+
needsWdio ? `import { webdriverio } from '@vitest/browser-webdriverio';` : null,
|
|
267
|
+
]
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.join('\n');
|
|
270
|
+
const stencilConfigOption = hasStencilConfig ? `\n stencilConfig: './stencil.config.ts',` : '';
|
|
271
|
+
const projectBlocks = projects
|
|
272
|
+
.map((project, i) => {
|
|
273
|
+
const currentBase = parsePattern(project.pattern)?.replace(/[^.]+$/, '');
|
|
274
|
+
const excludePatterns = projects
|
|
275
|
+
.filter((_, j) => j !== i)
|
|
276
|
+
.filter((other) => {
|
|
277
|
+
if (!currentBase)
|
|
278
|
+
return false;
|
|
279
|
+
const otherBase = parsePattern(other.pattern)?.replace(/[^.]+$/, '');
|
|
280
|
+
return !!otherBase && otherBase.endsWith(currentBase);
|
|
281
|
+
})
|
|
282
|
+
.map((p) => p.pattern);
|
|
283
|
+
return generateProjectBlock(project, excludePatterns);
|
|
284
|
+
})
|
|
285
|
+
.join(',\n');
|
|
286
|
+
return `${imports}
|
|
287
|
+
|
|
288
|
+
export default defineVitestConfig({${stencilConfigOption}
|
|
289
|
+
test: {
|
|
290
|
+
projects: [
|
|
291
|
+
${projectBlocks},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
function specTemplate(tagName, importComponent = false, subdirectory) {
|
|
298
|
+
const importPath = subdirectory ? `'../${tagName}'` : `'./${tagName}'`;
|
|
299
|
+
const componentImport = importComponent ? `import ${importPath};\n` : '';
|
|
300
|
+
return `import { describe, it, expect, render } from '@stencil/vitest';
|
|
301
|
+
${componentImport}
|
|
302
|
+
describe('${tagName}', () => {
|
|
303
|
+
it('renders', async () => {
|
|
304
|
+
const { root } = await render(<${tagName} />);
|
|
305
|
+
expect(root).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
// Returns the file suffix for a pattern like **/*.browser.spec.{ts,tsx},
|
|
311
|
+
// preferring tsx when available. Returns null for patterns that can't be
|
|
312
|
+
// reduced to a simple **/* glob (e.g. tests/**/*.spec.ts).
|
|
313
|
+
function parsePattern(pattern) {
|
|
314
|
+
const match = pattern.match(/^\*\*\/\*([^{*]+)(?:\{([^}]+)\})?$/);
|
|
315
|
+
if (!match)
|
|
316
|
+
return null;
|
|
317
|
+
const base = match[1];
|
|
318
|
+
const exts = match[2];
|
|
319
|
+
if (!exts)
|
|
320
|
+
return base;
|
|
321
|
+
const extList = exts.split(',').map((e) => e.trim());
|
|
322
|
+
return base + (extList.includes('tsx') ? 'tsx' : extList[0]);
|
|
323
|
+
}
|
|
324
|
+
/** Writes a starter spec file alongside each existing component for new projects. */
|
|
325
|
+
async function generateExampleTests(rootDir, projects) {
|
|
326
|
+
const componentsDir = join(rootDir, 'src', 'components');
|
|
327
|
+
const entries = await readdir(componentsDir, { withFileTypes: true }).catch(() => []);
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (!entry.isDirectory())
|
|
330
|
+
continue;
|
|
331
|
+
const componentFile = join(componentsDir, entry.name, `${entry.name}.tsx`);
|
|
332
|
+
if (!(await fileExists(componentFile)))
|
|
333
|
+
continue;
|
|
334
|
+
const source = await readFile(componentFile, 'utf8');
|
|
335
|
+
if (!source.includes('@Component'))
|
|
336
|
+
continue;
|
|
337
|
+
const tagMatch = source.match(/tag:\s*['"]([^'"]+)['"]/);
|
|
338
|
+
const tagName = tagMatch?.[1] ?? entry.name;
|
|
339
|
+
for (const project of projects) {
|
|
340
|
+
const suffix = parsePattern(project.pattern);
|
|
341
|
+
if (!suffix)
|
|
342
|
+
continue;
|
|
343
|
+
const dir = project.subdirectory
|
|
344
|
+
? join(componentsDir, entry.name, project.subdirectory)
|
|
345
|
+
: join(componentsDir, entry.name);
|
|
346
|
+
const specFile = join(dir, `${tagName}${suffix}`);
|
|
347
|
+
if (await fileExists(specFile))
|
|
348
|
+
continue;
|
|
349
|
+
if (project.subdirectory)
|
|
350
|
+
await mkdir(dir, { recursive: true });
|
|
351
|
+
const content = project.type === 'browser'
|
|
352
|
+
? specTemplate(tagName, false, project.subdirectory)
|
|
353
|
+
: specTemplate(tagName, project.loadMethod === 'plugin', project.subdirectory);
|
|
354
|
+
await writeFile(specFile, content, 'utf8');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function needsBuild(project) {
|
|
359
|
+
return project.type === 'browser' || (project.type === 'node' && project.loadMethod === 'full-build');
|
|
360
|
+
}
|
|
361
|
+
/** Adds `test` and `test:<name>` scripts to package.json if they don't already exist. */
|
|
362
|
+
async function updatePackageJsonScripts(rootDir, projects) {
|
|
363
|
+
const pkgPath = join(rootDir, 'package.json');
|
|
364
|
+
const content = await readFile(pkgPath, 'utf8');
|
|
365
|
+
const pkg = JSON.parse(content);
|
|
366
|
+
pkg['scripts'] ??= {};
|
|
367
|
+
const anyFullBuild = projects.some(needsBuild);
|
|
368
|
+
if (!pkg['scripts']['test'])
|
|
369
|
+
pkg['scripts']['test'] = anyFullBuild ? 'stencil-test' : 'vitest run';
|
|
370
|
+
for (const p of projects) {
|
|
371
|
+
const key = `test:${p.name}`;
|
|
372
|
+
const cmd = needsBuild(p) ? `stencil-test --project ${p.name}` : `vitest run --project ${p.name}`;
|
|
373
|
+
if (!pkg['scripts'][key])
|
|
374
|
+
pkg['scripts'][key] = cmd;
|
|
375
|
+
}
|
|
376
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
377
|
+
}
|
|
378
|
+
export const wizard = {
|
|
379
|
+
init: {
|
|
380
|
+
id: '@stencil/vitest',
|
|
381
|
+
displayName: 'Vitest',
|
|
382
|
+
description: 'Unit + component testing',
|
|
383
|
+
async run({ config, isNewProject, prompts, nypm }) {
|
|
384
|
+
const { intro, outro, confirm, isCancel, cancel, spinner } = prompts;
|
|
385
|
+
const rootDir = config.rootDir;
|
|
386
|
+
intro('Vitest - unit + component testing for Stencil');
|
|
387
|
+
const vitestConfigPath = join(rootDir, 'vitest.config.ts');
|
|
388
|
+
if (!isNewProject && (await fileExists(vitestConfigPath))) {
|
|
389
|
+
const overwrite = await confirm({
|
|
390
|
+
message: 'vitest.config.ts already exists. Overwrite it?',
|
|
391
|
+
initialValue: false,
|
|
392
|
+
});
|
|
393
|
+
if (isCancel(overwrite) || !overwrite) {
|
|
394
|
+
cancel('Skipping Vitest setup - existing config kept.');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const outputTargets = detectOutputTargets(config);
|
|
399
|
+
const hasStencilConfig = await fileExists(join(rootDir, 'stencil.config.ts'));
|
|
400
|
+
const projects = [];
|
|
401
|
+
const usedPatterns = new Set();
|
|
402
|
+
let isFirst = true;
|
|
403
|
+
let keepAdding = true;
|
|
404
|
+
while (keepAdding) {
|
|
405
|
+
const project = await promptProject(prompts, usedPatterns, outputTargets, isFirst, isNewProject);
|
|
406
|
+
projects.push(project);
|
|
407
|
+
isFirst = false;
|
|
408
|
+
const addAnother = await confirm({ message: 'Add another project?', initialValue: false });
|
|
409
|
+
keepAdding = !isCancel(addAnother) && addAnother;
|
|
410
|
+
}
|
|
411
|
+
const deps = collectDeps(projects);
|
|
412
|
+
if (deps.length > 0) {
|
|
413
|
+
const s = spinner();
|
|
414
|
+
s.start('Installing dependencies');
|
|
415
|
+
await nypm.addDependency(deps, { cwd: rootDir, dev: true });
|
|
416
|
+
s.stop('Dependencies installed');
|
|
417
|
+
}
|
|
418
|
+
const needsSetupFile = projects.some((p) => p.type === 'browser' || (p.type === 'node' && p.loadMethod === 'full-build'));
|
|
419
|
+
if (needsSetupFile) {
|
|
420
|
+
const setupPath = join(rootDir, 'vitest-setup.ts');
|
|
421
|
+
if (!(await fileExists(setupPath))) {
|
|
422
|
+
const loaderImport = resolveLoaderImport(config);
|
|
423
|
+
if (loaderImport === null) {
|
|
424
|
+
prompts.log.warn('Could not determine a loader path — no loader-bundle or standalone output target found.\n' +
|
|
425
|
+
'Create vitest-setup.ts manually and import your component loader.');
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
await writeFile(setupPath, `import '${loaderImport}';\n`, 'utf8');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
await writeFile(vitestConfigPath, generateVitestConfig(projects, hasStencilConfig), 'utf8');
|
|
433
|
+
await updatePackageJsonScripts(rootDir, projects);
|
|
434
|
+
if (isNewProject) {
|
|
435
|
+
await generateExampleTests(rootDir, projects);
|
|
436
|
+
}
|
|
437
|
+
outro('Vitest configured');
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
generate: {
|
|
441
|
+
fileTemplates: async (ctx) => {
|
|
442
|
+
const { prompts, config } = ctx;
|
|
443
|
+
const projects = await loadVitestProjects(config.rootDir);
|
|
444
|
+
const rawSubdir = await prompts.text({
|
|
445
|
+
message: 'Test file subdirectory? (leave blank to co-locate with component)',
|
|
446
|
+
placeholder: 'e.g. __tests__',
|
|
447
|
+
defaultValue: '',
|
|
448
|
+
});
|
|
449
|
+
const subdirectory = !prompts.isCancel(rawSubdir) ? rawSubdir.trim() || undefined : undefined;
|
|
450
|
+
if (projects.length === 0) {
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
label: 'Spec test (.spec.tsx)',
|
|
454
|
+
extension: 'spec.tsx',
|
|
455
|
+
subdirectory,
|
|
456
|
+
selectedByDefault: true,
|
|
457
|
+
template: (tagName) => specTemplate(tagName, true, subdirectory),
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
label: 'Browser test (.browser.spec.ts)',
|
|
461
|
+
extension: 'browser.spec.ts',
|
|
462
|
+
subdirectory,
|
|
463
|
+
selectedByDefault: false,
|
|
464
|
+
template: (tagName) => specTemplate(tagName, false, subdirectory),
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
return projects.flatMap((project) => {
|
|
469
|
+
const suffix = parsePattern(project.pattern);
|
|
470
|
+
if (!suffix)
|
|
471
|
+
return [];
|
|
472
|
+
const ext = suffix.replace(/^\./, '');
|
|
473
|
+
const label = `${project.name[0].toUpperCase()}${project.name.slice(1)} (${ext})`;
|
|
474
|
+
return [
|
|
475
|
+
{
|
|
476
|
+
label,
|
|
477
|
+
extension: ext,
|
|
478
|
+
subdirectory,
|
|
479
|
+
selectedByDefault: project.type === 'node',
|
|
480
|
+
template: project.type === 'browser'
|
|
481
|
+
? (tagName) => specTemplate(tagName, false, subdirectory)
|
|
482
|
+
: (tagName) => specTemplate(tagName, project.loadMethod === 'plugin', subdirectory),
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
};
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "https://github.com/stenciljs/vitest"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.13.0",
|
|
8
8
|
"description": "First-class testing utilities for Stencil design systems with Vitest",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"stencil-test": "./dist/bin/stencil-test.js"
|
|
15
15
|
},
|
|
16
|
+
"stencil": {
|
|
17
|
+
"wizard": "./dist/wizard.js"
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
20
|
+
"./wizard": {
|
|
21
|
+
"types": "./dist/wizard.d.ts",
|
|
22
|
+
"import": "./dist/wizard.js"
|
|
23
|
+
},
|
|
17
24
|
".": {
|
|
18
25
|
"types": "./dist/index.d.ts",
|
|
19
26
|
"import": "./dist/index.js",
|
|
@@ -104,7 +111,7 @@
|
|
|
104
111
|
"dependencies": {
|
|
105
112
|
"jiti": "^2.6.1",
|
|
106
113
|
"local-pkg": "^1.1.2",
|
|
107
|
-
"vitest-environment-stencil": "1.
|
|
114
|
+
"vitest-environment-stencil": "1.13.0"
|
|
108
115
|
},
|
|
109
116
|
"devDependencies": {
|
|
110
117
|
"@eslint/js": "^9.39.2",
|
|
@@ -112,6 +119,7 @@
|
|
|
112
119
|
"@semantic-release/exec": "^6.0.3",
|
|
113
120
|
"@semantic-release/git": "^10.0.1",
|
|
114
121
|
"@stencil/core": "^4.0.0",
|
|
122
|
+
"@stencil/cli": "5.0.0-alpha.8",
|
|
115
123
|
"@types/node": "24.10.4",
|
|
116
124
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
117
125
|
"@typescript-eslint/parser": "^8.50.0",
|
|
@@ -119,7 +127,6 @@
|
|
|
119
127
|
"eslint": "^9.39.2",
|
|
120
128
|
"installed-check": "^9.3.0",
|
|
121
129
|
"knip": "^5.76.3",
|
|
122
|
-
"playwright-core": "^1.57.0",
|
|
123
130
|
"prettier": "^3.7.4",
|
|
124
131
|
"semantic-release": "^25.0.2",
|
|
125
132
|
"typescript": "~5.8.3",
|