diff --git a/code/core/src/cli/AddonVitestService.test.ts b/code/core/src/cli/AddonVitestService.test.ts index f457886880ba..6c2f9fa19d2a 100644 --- a/code/core/src/cli/AddonVitestService.test.ts +++ b/code/core/src/cli/AddonVitestService.test.ts @@ -601,5 +601,48 @@ describe('AddonVitestService', () => { expect(result.reasons).toBeDefined(); expect(result.reasons!.length).toBe(2); }); + + it('should validate mergeConfig with plain object literal', async () => { + vi.mocked(find.any) + .mockReturnValueOnce(undefined) // workspace + .mockReturnValueOnce('vitest.config.ts'); // config + vi.mocked(fs.readFile).mockResolvedValue( + 'export default mergeConfig(viteConfig, { test: { name: "node" } })' + ); + const result = await service.validateConfigFiles('.storybook'); + expect(result.compatible).toBe(true); + }); + + it('should validate mergeConfig with defineConfig call', async () => { + vi.mocked(find.any) + .mockReturnValueOnce(undefined) // workspace + .mockReturnValueOnce('vitest.config.ts'); // config + vi.mocked(fs.readFile).mockResolvedValue( + 'export default mergeConfig(viteConfig, defineConfig({ test: { name: "node" } }))' + ); + const result = await service.validateConfigFiles('.storybook'); + expect(result.compatible).toBe(true); + }); + + it('should validate mergeConfig with multiple plain objects', async () => { + vi.mocked(find.any) + .mockReturnValueOnce(undefined) // workspace + .mockReturnValueOnce('vitest.config.ts'); // config + vi.mocked(fs.readFile).mockResolvedValue( + 'export default mergeConfig({ test: {} }, { plugins: [] })' + ); + const result = await service.validateConfigFiles('.storybook'); + expect(result.compatible).toBe(true); + }); + + it('should reject mergeConfig with invalid object (non-object argument)', async () => { + vi.mocked(find.any) + .mockReturnValueOnce(undefined) // workspace + .mockReturnValueOnce('vitest.config.ts'); // config + vi.mocked(fs.readFile).mockResolvedValue('export default mergeConfig(viteConfig, "string")'); + const result = await service.validateConfigFiles('.storybook'); + expect(result.compatible).toBe(false); + expect(result.reasons!.some((r) => r.includes('invalid Vitest config'))).toBe(true); + }); }); }); diff --git a/code/core/src/cli/AddonVitestService.ts b/code/core/src/cli/AddonVitestService.ts index a8375d0fa5f0..a9771ca53438 100644 --- a/code/core/src/cli/AddonVitestService.ts +++ b/code/core/src/cli/AddonVitestService.ts @@ -326,9 +326,7 @@ export class AddonVitestService { babel.traverse(parsedConfig, { ExportDefaultDeclaration: (path: any) => { if (this.isDefineConfigExpression(path.node.declaration)) { - isValidVitestConfig = this.isSafeToExtendWorkspace( - path.node.declaration as CallExpression - ); + isValidVitestConfig = this.isSafeToExtendWorkspace(path.node.declaration); } else if (this.isMergeConfigExpression(path.node.declaration)) { // the config could be anywhere in the mergeConfig call, so we need to check each argument const mergeCall = path.node.declaration as CallExpression; @@ -372,20 +370,34 @@ export class AddonVitestService { return babel.types.isCallExpression(path) && (path.callee as any)?.name === 'mergeConfig'; } - private isSafeToExtendWorkspace(node: CallExpression): boolean { - return ( - babel.types.isCallExpression(node) && - node.arguments.length > 0 && - babel.types.isObjectExpression(node.arguments?.[0]) && - node.arguments[0]?.properties.every( - (p: any) => - p.key?.name !== 'test' || - (babel.types.isObjectExpression(p.value) && - p.value.properties.every( - ({ key, value }: any) => - key?.name !== 'workspace' || babel.types.isArrayExpression(value) - )) - ) + private isSafeToExtendWorkspace(node: babel.types.Node): boolean { + // Extract the object expression to validate + let objectToValidate: babel.types.ObjectExpression | null = null; + + if (babel.types.isCallExpression(node)) { + // Handle function calls like defineConfig({...}) + if (node.arguments.length > 0 && babel.types.isObjectExpression(node.arguments[0])) { + objectToValidate = node.arguments[0]; + } + } else if (babel.types.isObjectExpression(node)) { + // Handle plain object literals like {...} + objectToValidate = node; + } + + // If we couldn't extract a valid object, it's not safe + if (!objectToValidate) { + return false; + } + + // Check that the object doesn't have problematic test.workspace properties + return objectToValidate.properties.every( + (p: any) => + p.key?.name !== 'test' || + (babel.types.isObjectExpression(p.value) && + p.value.properties.every( + ({ key, value }: any) => + key?.name !== 'workspace' || babel.types.isArrayExpression(value) + )) ); } }