Skip to content

Commit e0756a7

Browse files
authored
feat(vitest): vitest browser mode (#588)
* add possibility for required multiselect * add really basic vitest options * add `object.createFromPrimitives` * browser mode * fix suggestions * fix test and linting
1 parent 2a15c86 commit e0756a7

File tree

9 files changed

+167
-93
lines changed

9 files changed

+167
-93
lines changed

.changeset/evil-comics-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': patch
3+
---
4+
5+
feat(vitest): support vite browser mode

packages/addons/_tests/vitest/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ test.concurrent.for(variants)('core - %s', async (variant, { page, ...ctx }) =>
1010

1111
const { close } = await prepareServer({ cwd, page });
1212

13+
execSync('pnpm exec playwright install chromium', { cwd, stdio: 'pipe' });
1314
execSync('pnpm test', { cwd, stdio: 'pipe' });
1415

1516
// kill server process when we're done

packages/addons/tailwindcss/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ const options = defineAddonOptions({
3030
type: 'multiselect',
3131
question: 'Which plugins would you like to add?',
3232
options: plugins.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
33-
default: []
33+
default: [],
34+
required: false
3435
}
3536
});
3637

packages/addons/vitest-addon/index.ts

Lines changed: 86 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1-
import { dedent, defineAddon, log } from '@sveltejs/cli-core';
2-
import { array, common, exports, functions, imports, object } from '@sveltejs/cli-core/js';
1+
import { dedent, defineAddon, defineAddonOptions, log } from '@sveltejs/cli-core';
2+
import { array, exports, functions, object } from '@sveltejs/cli-core/js';
33
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
44

5+
const options = defineAddonOptions({
6+
usages: {
7+
question: 'What do you want to use vitest for?',
8+
type: 'multiselect',
9+
default: ['unit', 'component'],
10+
options: [
11+
{ value: 'unit', label: 'unit testing' },
12+
{ value: 'component', label: 'component testing' }
13+
],
14+
required: true
15+
}
16+
});
17+
518
export default defineAddon({
619
id: 'vitest',
720
shortDescription: 'unit testing',
821
homepage: 'https://vitest.dev',
9-
options: {},
10-
run: ({ sv, typescript, kit }) => {
22+
options,
23+
run: ({ sv, typescript, kit, options }) => {
1124
const ext = typescript ? 'ts' : 'js';
25+
const unitTesting = options.usages.includes('unit');
26+
const componentTesting = options.usages.includes('component');
1227

1328
sv.devDependency('vitest', '^3.2.3');
14-
sv.devDependency('@testing-library/svelte', '^5.2.4');
15-
sv.devDependency('@testing-library/jest-dom', '^6.6.3');
16-
sv.devDependency('jsdom', '^26.0.0');
29+
30+
if (componentTesting) {
31+
sv.devDependency('@vitest/browser', '^3.2.3');
32+
sv.devDependency('vitest-browser-svelte', '^0.1.0');
33+
sv.devDependency('playwright', '^1.53.0');
34+
}
1735

1836
sv.file('package.json', (content) => {
1937
const { data, generateCode } = parseJson(content);
@@ -28,108 +46,84 @@ export default defineAddon({
2846
return generateCode();
2947
});
3048

31-
sv.file(`src/demo.spec.${ext}`, (content) => {
32-
if (content) return content;
33-
34-
return dedent`
35-
import { describe, it, expect } from 'vitest';
36-
37-
describe('sum test', () => {
38-
it('adds 1 + 2 to equal 3', () => {
39-
expect(1 + 2).toBe(3);
40-
});
41-
});
42-
`;
43-
});
44-
45-
if (kit) {
46-
sv.file(`${kit.routesDirectory}/page.svelte.test.${ext}`, (content) => {
49+
if (unitTesting) {
50+
sv.file(`src/demo.spec.${ext}`, (content) => {
4751
if (content) return content;
4852

4953
return dedent`
50-
import { describe, test, expect } from 'vitest';
51-
import '@testing-library/jest-dom/vitest';
52-
import { render, screen } from '@testing-library/svelte';
53-
import Page from './+page.svelte';
54+
import { describe, it, expect } from 'vitest';
5455
55-
describe('/+page.svelte', () => {
56-
test('should render h1', () => {
57-
render(Page);
58-
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
59-
});
56+
describe('sum test', () => {
57+
it('adds 1 + 2 to equal 3', () => {
58+
expect(1 + 2).toBe(3);
6059
});
61-
`;
60+
});
61+
`;
6262
});
63-
} else {
64-
sv.file(`src/App.svelte.test.${ext}`, (content) => {
63+
}
64+
65+
if (componentTesting) {
66+
const fileName = kit
67+
? `${kit.routesDirectory}/page.svelte.test.${ext}`
68+
: `src/App.svelte.test.${ext}`;
69+
70+
sv.file(fileName, (content) => {
6571
if (content) return content;
6672

6773
return dedent`
68-
import { describe, test, expect } from 'vitest';
69-
import '@testing-library/jest-dom/vitest';
70-
import { render, screen } from '@testing-library/svelte';
71-
import App from './App.svelte';
72-
73-
describe('App.svelte', () => {
74-
test('should render h1', () => {
75-
render(App);
76-
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
74+
import { page } from '@vitest/browser/context';
75+
import { describe, expect, it } from 'vitest';
76+
import { render } from 'vitest-browser-svelte';
77+
${kit ? "import Page from './+page.svelte';" : "import App from './App.svelte';"}
78+
79+
describe('${kit ? '/+page.svelte' : 'App.svelte'}', () => {
80+
it('should render h1', async () => {
81+
render(${kit ? 'Page' : 'App'});
82+
83+
const heading = page.getByRole('heading', { level: 1 });
84+
await expect.element(heading).toBeInTheDocument();
7785
});
7886
});
7987
`;
8088
});
81-
}
8289

83-
sv.file(`vitest-setup-client.${ext}`, (content) => {
84-
if (content) return content;
85-
86-
return dedent`
87-
import '@testing-library/jest-dom/vitest';
88-
import { vi } from 'vitest';
89-
90-
// required for svelte5 + jsdom as jsdom does not support matchMedia
91-
Object.defineProperty(window, 'matchMedia', {
92-
writable: true,
93-
enumerable: true,
94-
value: vi.fn().mockImplementation(query => ({
95-
matches: false,
96-
media: query,
97-
onchange: null,
98-
addEventListener: vi.fn(),
99-
removeEventListener: vi.fn(),
100-
dispatchEvent: vi.fn(),
101-
})),
102-
})
103-
104-
// add more mocks here if you need them
90+
sv.file(`vitest-setup-client.${ext}`, (content) => {
91+
if (content) return content;
92+
93+
return dedent`
94+
/// <reference types="@vitest/browser/matchers" />
95+
/// <reference types="@vitest/browser/providers/playwright" />
10596
`;
106-
});
97+
});
98+
}
10799

108100
sv.file(`vite.config.${ext}`, (content) => {
109101
const { ast, generateCode } = parseScript(content);
110102

111-
imports.addNamed(ast, '@testing-library/svelte/vite', { svelteTesting: 'svelteTesting' });
112-
113-
const clientObjectExpression = object.create({
114-
extends: common.createLiteral(`./vite.config.${ext}`),
115-
plugins: common.expressionFromString('[svelteTesting()]'),
116-
test: object.create({
117-
name: common.createLiteral('client'),
118-
environment: common.createLiteral('jsdom'),
119-
clearMocks: common.expressionFromString('true'),
120-
include: common.expressionFromString("['src/**/*.svelte.{test,spec}.{js,ts}']"),
121-
exclude: common.expressionFromString("['src/lib/server/**']"),
122-
setupFiles: common.expressionFromString(`['./vitest-setup-client.${ext}']`)
123-
})
103+
const clientObjectExpression = object.createFromPrimitives({
104+
extends: `./vite.config.${ext}`,
105+
test: {
106+
name: 'client',
107+
environment: 'browser',
108+
browser: {
109+
enabled: true,
110+
provider: 'playwright',
111+
instances: [{ browser: 'chromium' }]
112+
},
113+
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
114+
exclude: ['src/lib/server/**'],
115+
setupFiles: [`./vitest-setup-client.${ext}`]
116+
}
124117
});
125-
const serverObjectExpression = object.create({
126-
extends: common.createLiteral(`./vite.config.${ext}`),
127-
test: object.create({
128-
name: common.createLiteral('server'),
129-
environment: common.createLiteral('node'),
130-
include: common.expressionFromString("['src/**/*.{test,spec}.{js,ts}']"),
131-
exclude: common.expressionFromString("['src/**/*.svelte.{test,spec}.{js,ts}']")
132-
})
118+
119+
const serverObjectExpression = object.createFromPrimitives({
120+
extends: `./vite.config.${ext}`,
121+
test: {
122+
name: 'server',
123+
environment: 'node',
124+
include: ['src/**/*.{test,spec}.{js,ts}'],
125+
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
126+
}
133127
});
134128

135129
const defineConfigFallback = functions.call('defineConfig', []);
@@ -142,8 +136,9 @@ export default defineAddon({
142136
const testObject = object.property(vitestConfig, 'test', object.createEmpty());
143137

144138
const workspaceArray = object.property(testObject, 'projects', array.createEmpty());
145-
array.push(workspaceArray, clientObjectExpression);
146-
array.push(workspaceArray, serverObjectExpression);
139+
140+
if (componentTesting) array.push(workspaceArray, clientObjectExpression);
141+
if (unitTesting) array.push(workspaceArray, serverObjectExpression);
147142

148143
return generateCode();
149144
});

packages/cli/commands/add/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export async function runAddCommand(
515515
answer = await p.multiselect({
516516
message,
517517
initialValues: question.default,
518-
required: false,
518+
required: question.required,
519519
options: question.options
520520
});
521521
}

packages/core/addon/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type MultiSelectQuestion<Value = any> = {
2727
type: 'multiselect';
2828
default: Value[];
2929
options: Array<{ value: Value; label?: string; hint?: string }>;
30+
required: boolean;
3031
};
3132

3233
export type BaseQuestion = {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const empty = {};
2+
3+
// prettier-ignore
4+
const created = {
5+
foo: 1,
6+
bar: 'string',
7+
object: { foo: 'hello', nested: { bar: 'world' } },
8+
array: [
9+
123,
10+
'hello',
11+
{ foo: 'bar', bool: true },
12+
[456, '789']
13+
]
14+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { variables, object, type AstTypes } from '@sveltejs/cli-core/js';
2+
3+
export function run(ast: AstTypes.Program): void {
4+
const emptyObject = object.createFromPrimitives({});
5+
const emptyVariable = variables.declaration(ast, 'const', 'empty', emptyObject);
6+
ast.body.push(emptyVariable);
7+
8+
const createdObject = object.createFromPrimitives({
9+
foo: 1,
10+
bar: 'string',
11+
baz: undefined,
12+
object: {
13+
foo: 'hello',
14+
nested: {
15+
bar: 'world'
16+
}
17+
},
18+
array: [123, 'hello', { foo: 'bar', bool: true }, [456, '789']]
19+
});
20+
const createdVariable = variables.declaration(ast, 'const', 'created', createdObject);
21+
createdVariable.leadingComments = [{ type: 'Line', value: ' prettier-ignore' }];
22+
ast.body.push(createdVariable);
23+
}

packages/core/tooling/js/object.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import type { Expression } from 'estree';
12
import type { AstTypes } from '../index.ts';
3+
import { array, common } from './index.ts';
24

35
export function property<T extends AstTypes.Expression | AstTypes.Identifier>(
46
ast: AstTypes.ObjectExpression,
@@ -103,6 +105,38 @@ export function create<T extends AstTypes.Expression>(
103105
return objExpression;
104106
}
105107

108+
type ObjectPrimitiveValues = string | number | boolean | undefined;
109+
type ObjectValues = ObjectPrimitiveValues | ObjectMap | ObjectValues[];
110+
type ObjectMap = { [property: string]: ObjectValues };
111+
112+
// todo: potentially make this the default `create` method in the future
113+
export function createFromPrimitives(obj: ObjectMap): AstTypes.ObjectExpression {
114+
const objExpression = createEmpty();
115+
116+
const getExpression = (value: ObjectValues) => {
117+
let expression: Expression;
118+
if (Array.isArray(value)) {
119+
expression = array.createEmpty();
120+
for (const v of value) {
121+
array.push(expression, getExpression(v));
122+
}
123+
} else if (typeof value === 'object' && value !== null) {
124+
expression = createFromPrimitives(value);
125+
} else {
126+
expression = common.createLiteral(value);
127+
}
128+
return expression;
129+
};
130+
131+
for (const [prop, value] of Object.entries(obj)) {
132+
if (value === undefined) continue;
133+
134+
property(objExpression, prop, getExpression(value));
135+
}
136+
137+
return objExpression;
138+
}
139+
106140
export function createEmpty(): AstTypes.ObjectExpression {
107141
const objectExpression: AstTypes.ObjectExpression = {
108142
type: 'ObjectExpression',

0 commit comments

Comments
 (0)