Skip to content

Commit 227094e

Browse files
committed
fix: make sure configs are correctly inherited
1 parent aeb1898 commit 227094e

File tree

7 files changed

+261
-51
lines changed

7 files changed

+261
-51
lines changed

packages/vitest/src/node/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ export class Vitest {
290290
this._workspaceConfigPath = workspaceConfigPath
291291

292292
if (!workspaceConfigPath) {
293-
return resolveBrowserWorkspace(this, [this._createRootProject()])
293+
return resolveBrowserWorkspace(this, new Set(), [this._createRootProject()])
294294
}
295295

296296
const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as {

packages/vitest/src/node/project.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ export class TestProject {
158158
if (!this._vite) {
159159
throw new Error('The server was not set. It means that `project.vite` was called before the Vite server was established.')
160160
}
161+
// checking it once should be enough
162+
Object.defineProperty(this, 'vite', {
163+
configurable: true,
164+
writable: true,
165+
value: this._vite,
166+
})
161167
return this._vite
162168
}
163169

@@ -168,6 +174,12 @@ export class TestProject {
168174
if (!this._config) {
169175
throw new Error('The config was not set. It means that `project.config` was called before the Vite server was established.')
170176
}
177+
// checking it once should be enough
178+
Object.defineProperty(this, 'config', {
179+
configurable: true,
180+
writable: true,
181+
value: this._config,
182+
})
171183
return this._config
172184
}
173185

packages/vitest/src/node/types/browser.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ type UnsupportedProperties =
6666
| 'server'
6767
| 'benchmark'
6868

69-
// TODO: document all options
70-
export interface BrowserConfig extends BrowserProviderOptions, Omit<ProjectConfig, UnsupportedProperties>, Pick<BrowserConfigOptions, 'locators' | 'viewport' | 'testerHtmlPath' | 'screenshotDirectory' | 'screenshotFailures'> {
69+
export interface BrowserConfig
70+
extends BrowserProviderOptions,
71+
Omit<ProjectConfig, UnsupportedProperties>,
72+
Pick<BrowserConfigOptions, 'locators' | 'viewport' | 'testerHtmlPath' | 'screenshotDirectory' | 'screenshotFailures'> {
7173
browser: string
7274
}
7375

packages/vitest/src/node/workspace/resolveWorkspace.ts

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Vitest } from '../core'
2-
import type { ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config'
2+
import type { BrowserConfig, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config'
33
import { existsSync, promises as fs } from 'node:fs'
44
import os from 'node:os'
55
import { limitConcurrency } from '@vitest/runner/utils'
@@ -97,7 +97,7 @@ export async function resolveWorkspace(
9797

9898
// pretty rare case - the glob didn't match anything and there are no inline configs
9999
if (!projectPromises.length) {
100-
return resolveBrowserWorkspace(vitest, [vitest._createRootProject()])
100+
return resolveBrowserWorkspace(vitest, new Set(), [vitest._createRootProject()])
101101
}
102102

103103
const resolvedProjects = await Promise.all(projectPromises)
@@ -127,76 +127,101 @@ export async function resolveWorkspace(
127127
names.add(name)
128128
}
129129

130-
return resolveBrowserWorkspace(vitest, resolvedProjects)
130+
return resolveBrowserWorkspace(vitest, names, resolvedProjects)
131131
}
132132

133133
export function resolveBrowserWorkspace(
134134
vitest: Vitest,
135+
names: Set<string>,
135136
resolvedProjects: TestProject[],
136137
) {
138+
const newConfigs: [project: TestProject, config: ResolvedConfig][] = []
139+
137140
resolvedProjects.forEach((project) => {
138141
const configs = project.config.browser.configs
139142
if (!project.config.browser.enabled || !configs || configs.length === 0) {
140143
return
141144
}
142145
const [firstConfig, ...restConfigs] = configs
146+
const originalName = project.config.name
143147

144-
project.config.name ||= project.config.name
145-
? `${project.config.name} (${firstConfig.browser})`
148+
const newName = originalName
149+
? `${originalName} (${firstConfig.browser})`
146150
: firstConfig.browser
151+
if (names.has(newName)) {
152+
throw new Error(`Cannot redefine the project name for a nameless project. The project name "${firstConfig.browser}" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.`)
153+
}
154+
names.add(newName)
147155

148156
if (project.config.browser.providerOptions) {
149157
vitest.logger.warn(
150-
withLabel('yellow', 'Vitest', `"providerOptions"${project.config.name ? ` in "${project.config.name}" project` : ''} is ignored because it's overriden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`),
158+
withLabel('yellow', 'Vitest', `"providerOptions"${originalName ? ` in "${originalName}" project` : ''} is ignored because it's overriden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`),
151159
)
152160
}
153161

154-
project.config.browser.name = firstConfig.browser
155-
project.config.browser.providerOptions = firstConfig
156-
157-
restConfigs.forEach(({ browser, ...capability }) => {
158-
// TODO: cover with tests
159-
// browser-only options
160-
const {
161-
locators,
162-
viewport,
163-
testerHtmlPath,
164-
screenshotDirectory,
165-
screenshotFailures,
166-
// @ts-expect-error remove just in case
167-
browser: _browser,
168-
// TODO: need a lot of tests
169-
...overrideConfig
170-
} = capability
171-
const currentConfig = project.config.browser
172-
const clonedConfig = mergeConfig<any, any>({
173-
...project.config,
174-
name: project.config.name ? `${project.config} (${browser})` : browser,
175-
browser: {
176-
...project.config.browser,
177-
locators: locators
178-
? {
179-
testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute,
180-
}
181-
: project.config.browser.locators,
182-
viewport: viewport ?? currentConfig.viewport,
183-
testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath,
184-
screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory,
185-
screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures,
186-
name: browser,
187-
providerOptions: capability,
188-
configs: undefined, // projects cannot spawn more configs
189-
},
190-
// TODO: should resolve, not merge/override
191-
} satisfies ResolvedConfig, overrideConfig) as ResolvedConfig
192-
const clone = TestProject._cloneBrowserProject(project, clonedConfig)
193-
194-
resolvedProjects.push(clone)
162+
restConfigs.forEach((config) => {
163+
const browser = config.browser
164+
const name = config.name
165+
const newName = name || (originalName ? `${originalName} (${browser})` : browser)
166+
if (names.has(newName)) {
167+
throw new Error(
168+
[
169+
`Cannot define a nested project for a ${browser} browser. The project name "${newName}" was already defined. `,
170+
'If you have multiple configs for the same browser, make sure to define a custom "name". ',
171+
'All projects in a workspace should have unique names. Make sure your configuration is correct.',
172+
].join(''),
173+
)
174+
}
175+
names.add(newName)
176+
const clonedConfig = cloneConfig(project, config)
177+
clonedConfig.name = newName
178+
newConfigs.push([project, clonedConfig])
195179
})
180+
181+
Object.assign(project.config, cloneConfig(project, firstConfig))
182+
project.config.name = newName
183+
})
184+
newConfigs.forEach(([project, clonedConfig]) => {
185+
const clone = TestProject._cloneBrowserProject(project, clonedConfig)
186+
resolvedProjects.push(clone)
196187
})
197188
return resolvedProjects
198189
}
199190

191+
function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig) {
192+
const {
193+
locators,
194+
viewport,
195+
testerHtmlPath,
196+
screenshotDirectory,
197+
screenshotFailures,
198+
// @ts-expect-error remove just in case
199+
browser: _browser,
200+
name,
201+
...overrideConfig
202+
} = config
203+
const currentConfig = project.config.browser
204+
return mergeConfig<any, any>({
205+
...project.config,
206+
browser: {
207+
...project.config.browser,
208+
locators: locators
209+
? {
210+
testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute,
211+
}
212+
: project.config.browser.locators,
213+
viewport: viewport ?? currentConfig.viewport,
214+
testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath,
215+
screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory,
216+
screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures,
217+
name: browser,
218+
providerOptions: config,
219+
configs: undefined, // projects cannot spawn more configs
220+
},
221+
// TODO: should resolve, not merge/override
222+
} satisfies ResolvedConfig, overrideConfig) as ResolvedConfig
223+
}
224+
200225
async function resolveTestProjectConfigs(
201226
vitest: Vitest,
202227
workspaceConfigPath: string | undefined,

packages/vitest/src/public/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TestModule as _TestFile } from '../node/reporters/reported-tasks'
55
export { parseCLI } from '../node/cli/cac'
66
export { startVitest } from '../node/cli/cli-api'
77
export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig'
8-
export type { Vitest } from '../node/core'
8+
export type { Vitest, VitestOptions } from '../node/core'
99
export { createVitest } from '../node/create'
1010
export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors'
1111
export type { GlobalSetupContext } from '../node/globalSetup'
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { ViteUserConfig } from 'vitest/config'
2+
import type { UserConfig, VitestOptions } from 'vitest/node'
3+
import { expect, test } from 'vitest'
4+
import { createVitest } from 'vitest/node'
5+
6+
async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) {
7+
return await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions)
8+
}
9+
10+
test('assignes names as browsers', async () => {
11+
const { projects } = await vitest({
12+
browser: {
13+
enabled: true,
14+
configs: [
15+
{ browser: 'chromium' },
16+
{ browser: 'firefox' },
17+
{ browser: 'webkit' },
18+
],
19+
},
20+
})
21+
expect(projects.map(p => p.name)).toEqual([
22+
'chromium',
23+
'firefox',
24+
'webkit',
25+
])
26+
})
27+
28+
test('assignes names as browsers in a custom project', async () => {
29+
const { projects } = await vitest({
30+
workspace: [
31+
{
32+
test: {
33+
name: 'custom',
34+
browser: {
35+
enabled: true,
36+
configs: [
37+
{ browser: 'chromium' },
38+
{ browser: 'firefox' },
39+
{ browser: 'webkit' },
40+
{ browser: 'webkit', name: 'hello-world' },
41+
],
42+
},
43+
},
44+
},
45+
],
46+
})
47+
expect(projects.map(p => p.name)).toEqual([
48+
'custom (chromium)',
49+
'custom (firefox)',
50+
'custom (webkit)',
51+
'hello-world',
52+
])
53+
})
54+
55+
test.only('inherits browser options', async () => {
56+
const { projects } = await vitest({
57+
setupFiles: ['/test/setup.ts'],
58+
provide: {
59+
browser: true,
60+
},
61+
browser: {
62+
enabled: true,
63+
headless: true,
64+
screenshotFailures: false,
65+
testerHtmlPath: '/custom-path.html',
66+
screenshotDirectory: '/custom-directory',
67+
fileParallelism: false,
68+
viewport: {
69+
width: 300,
70+
height: 900,
71+
},
72+
locators: {
73+
testIdAttribute: 'data-tid',
74+
},
75+
configs: [
76+
{
77+
browser: 'chromium',
78+
screenshotFailures: true,
79+
},
80+
{
81+
browser: 'firefox',
82+
screenshotFailures: true,
83+
locators: {
84+
testIdAttribute: 'data-custom',
85+
},
86+
viewport: {
87+
width: 900,
88+
height: 300,
89+
},
90+
testerHtmlPath: '/custom-overriden-path.html',
91+
screenshotDirectory: '/custom-overriden-directory',
92+
},
93+
],
94+
},
95+
})
96+
expect(projects.map(p => p.config)).toMatchObject([
97+
{
98+
name: 'chromium',
99+
setupFiles: ['/test/setup.ts'],
100+
provide: {
101+
browser: true,
102+
},
103+
browser: {
104+
enabled: true,
105+
headless: true,
106+
screenshotFailures: true,
107+
screenshotDirectory: '/custom-directory',
108+
viewport: {
109+
width: 300,
110+
height: 900,
111+
},
112+
locators: {
113+
testIdAttribute: 'data-tid',
114+
},
115+
fileParallelism: false,
116+
testerHtmlPath: '/custom-path.html',
117+
},
118+
},
119+
{
120+
name: 'firefox',
121+
setupFiles: ['/test/setup.ts'],
122+
provide: {
123+
browser: true,
124+
},
125+
browser: {
126+
enabled: true,
127+
headless: true,
128+
screenshotFailures: true,
129+
viewport: {
130+
width: 900,
131+
height: 300,
132+
},
133+
screenshotDirectory: '/custom-overriden-directory',
134+
locators: {
135+
testIdAttribute: 'data-custom',
136+
},
137+
testerHtmlPath: '/custom-overriden-path.html',
138+
},
139+
},
140+
])
141+
})

test/config/test/failures.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,33 @@ test('browser.name filteres all browser.configs are required', async () => {
319319
})
320320
expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config. The "browser.name" was set to "chromium" which filtered all configs (firefox). Did you mean to use another name?')
321321
})
322+
323+
test('browser.configs throws an error if no custom name is provided', async () => {
324+
const { stderr } = await runVitest({
325+
browser: {
326+
enabled: true,
327+
provider: 'playwright',
328+
configs: [
329+
{ browser: 'firefox' },
330+
{ browser: 'firefox' },
331+
],
332+
},
333+
})
334+
expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.')
335+
})
336+
337+
test('browser.configs throws an error if no custom name is provided, but the config name is inherited', async () => {
338+
const { stderr } = await runVitest({
339+
name: 'custom',
340+
browser: {
341+
enabled: true,
342+
provider: 'playwright',
343+
configs: [
344+
{ browser: 'firefox' },
345+
{ browser: 'firefox' },
346+
],
347+
},
348+
})
349+
expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.')
350+
})
351+

0 commit comments

Comments
 (0)