|
| 1 | +import { describe, expect, it } from 'vitest'; |
| 2 | + |
| 3 | +import { SourceEditor, parseSource } from '../vitest-v5/ast.ts'; |
| 4 | +import { migrateVitestV5Config } from '../vitest-v5/config.ts'; |
| 5 | +import { migrateVitestV5Source } from '../vitest-v5/source.ts'; |
| 6 | + |
| 7 | +const options = { preserveV4: true, browser: true, globals: true }; |
| 8 | + |
| 9 | +describe('Oxc migration analysis', () => { |
| 10 | + it.each([ |
| 11 | + ['test.js', 'const element = <div />;'], |
| 12 | + ['test.jsx', 'export const element = <div />;'], |
| 13 | + ['test.ts', 'const identity = <T>(value: T): T => value;'], |
| 14 | + ['test.tsx', 'const element = <div />; interface Props { value: string }'], |
| 15 | + ['test.mts', 'export const value: number = 1;'], |
| 16 | + ['test.cts', 'const value: number = 1; module.exports = value;'], |
| 17 | + ['test.js', '@decorate class Example { method() {} }'], |
| 18 | + ['test.ts', 'const value = 123n; const expression = /partial/v;'], |
| 19 | + ])('parses %s syntax without another parser dependency', (file, content) => { |
| 20 | + expect(parseSource(file, content).program.type).toBe('Program'); |
| 21 | + }); |
| 22 | + |
| 23 | + it('rejects Flow and malformed syntax instead of returning a partial AST', () => { |
| 24 | + expect(() => parseSource('test.js', '// @flow\nconst value: string = "x";')).toThrow( |
| 25 | + 'Flow is not supported', |
| 26 | + ); |
| 27 | + expect(() => parseSource('test.ts', 'const = ;')).toThrow(); |
| 28 | + }); |
| 29 | + |
| 30 | + it('preserves shadowed globals in destructuring, catch clauses, and hoisted declarations', () => { |
| 31 | + const shadowed = `function parameter({ expect }) { expect(() => {}).toThrow(''); } |
| 32 | +try {} catch (expect) { expect(() => {}).toThrow(''); } |
| 33 | +function hoisted() { expect(() => {}).toThrow(''); var expect; } |
| 34 | +{ expect(() => {}).toThrow(''); let expect; }`; |
| 35 | + const input = `${shadowed}\nexpect(() => {}).toThrow('');`; |
| 36 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 37 | + expect(result.content).toBe(`${shadowed}\nexpect(() => {}).toThrow(/^$/);`); |
| 38 | + expect(result.findings).toEqual([]); |
| 39 | + }); |
| 40 | + |
| 41 | + it('does not follow reassigned runner aliases', () => { |
| 42 | + const input = `import { createVitest } from 'vitest/node'; |
| 43 | +let runner = await createVitest('test', {}); |
| 44 | +runner = other; |
| 45 | +runner.collect();`; |
| 46 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 47 | + expect(result.content).toBe(input); |
| 48 | + expect(result.findings).toEqual([expect.objectContaining({ code: 'static-collect' })]); |
| 49 | + }); |
| 50 | + |
| 51 | + it('resolves constructor references before and after their declaration', () => { |
| 52 | + const input = `import { vi } from 'vitest'; |
| 53 | +new mock(); const mock = vi.fn(); new mock();`; |
| 54 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 55 | + expect(result.findings).toEqual([expect.objectContaining({ code: 'class-mock' })]); |
| 56 | + }); |
| 57 | + |
| 58 | + it('reserves generated names across bindings, unresolved references, and earlier edits', () => { |
| 59 | + const input = `import { getFn } from '@vitest/runner'; |
| 60 | +import { getHooks } from 'vitest/suite'; |
| 61 | +const _VitestTestRunner = 1; use(_VitestTestRunner2);`; |
| 62 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 63 | + expect(result.content).toContain('TestRunner as _VitestTestRunner3'); |
| 64 | + expect(result.content).toContain('TestRunner as _VitestTestRunner4'); |
| 65 | + expect(result.findings).toEqual([]); |
| 66 | + expect(parseSource('test.ts', result.content).program.type).toBe('Program'); |
| 67 | + }); |
| 68 | + |
| 69 | + it.each([ |
| 70 | + `import type * as v from 'vitest'; v.expect(() => {}).toThrow('');`, |
| 71 | + `import type * as expect from 'vitest'; expect(() => {}).toThrow('');`, |
| 72 | + `import type { expect } from 'vitest'; expect(() => {}).toThrow('');`, |
| 73 | + `import { type expect } from 'vitest'; expect(() => {}).toThrow('');`, |
| 74 | + ])('does not mistake a type-only import for a runtime API or global: %s', (input) => { |
| 75 | + expect(migrateVitestV5Source('test.ts', input, options).content).toBe(input); |
| 76 | + }); |
| 77 | + |
| 78 | + it.each([ |
| 79 | + `import { expect } from 'vitest'; expect?.(() => {}).toThrow('');`, |
| 80 | + `import { expect } from 'vitest'; expect(() => {})?.toThrow('');`, |
| 81 | + `import * as v from 'vitest'; v?.expect(() => {}).toThrow('');`, |
| 82 | + ])('preserves optional API calls: %s', (input) => { |
| 83 | + expect(migrateVitestV5Source('test.ts', input, options).content).toBe(input); |
| 84 | + }); |
| 85 | + |
| 86 | + it('retains dynamic imports, CommonJS imports, and TS import-type diagnostics', () => { |
| 87 | + const input = `const runner = await import('@vitest/runner'); |
| 88 | +const other = require('vitest/runners'); |
| 89 | +type Runner = import('vitest/internal/module-runner').ModuleRunner; |
| 90 | +function local(require) { return require('vitest/runners'); }`; |
| 91 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 92 | + expect(result.content).toBe(input); |
| 93 | + expect(result.findings.map(({ code, severity }) => [code, severity])).toEqual([ |
| 94 | + ['removed-api', 'block'], |
| 95 | + ['removed-api', 'block'], |
| 96 | + ['removed-api', 'review'], |
| 97 | + ]); |
| 98 | + }); |
| 99 | + |
| 100 | + it('preserves non-ASCII text and reports UTF-16 columns with CRLF line endings', () => { |
| 101 | + const input = `// 中文 😀\r\nimport { expect } from 'vitest';\r\nconst label = '😀'; expect.poll(() => label).toBe('x');`; |
| 102 | + const result = migrateVitestV5Source('test.ts', input, options); |
| 103 | + expect(result.content).toBe(input); |
| 104 | + expect(result.findings).toContainEqual( |
| 105 | + expect.objectContaining({ |
| 106 | + code: 'poll-timeout', |
| 107 | + line: 3, |
| 108 | + column: "const label = '😀'; ".length + 1, |
| 109 | + }), |
| 110 | + ); |
| 111 | + const edited = migrateVitestV5Source( |
| 112 | + 'test.ts', |
| 113 | + `${input}\r\nexpect(() => {}).toThrow('');`, |
| 114 | + options, |
| 115 | + ); |
| 116 | + expect(edited.content).toBe(`${input}\r\nexpect(() => {}).toThrow(/^$/);`); |
| 117 | + }); |
| 118 | + |
| 119 | + it.each([ |
| 120 | + 'api: { port: 1 } /* keep , 😀 */, enabled: true', |
| 121 | + 'enabled: true, /* keep , 😀 */ api: { port: 1 }', |
| 122 | + "enabled: true, api: { port: 1 }, /* keep , 😀 */ name: 'browser'", |
| 123 | + 'api: { port: 1 } /* keep , 😀 */,', |
| 124 | + 'api: { port: 1 } // keep , 😀\n, enabled: true', |
| 125 | + ])('removes property punctuation without consuming comments: %s', (properties) => { |
| 126 | + const input = `// 😀\nexport default { test: { browser: { ${properties} } } };`; |
| 127 | + const result = migrateVitestV5Config('vite.config.ts', input, { preserveV4: false }); |
| 128 | + expect(result.findings).toEqual([]); |
| 129 | + expect(result.content).toContain('keep , 😀'); |
| 130 | + expect(result.content.match(/api:/g)).toHaveLength(1); |
| 131 | + expect(parseSource('vite.config.ts', result.content).program.type).toBe('Program'); |
| 132 | + expect( |
| 133 | + migrateVitestV5Config('vite.config.ts', result.content, { preserveV4: false }).content, |
| 134 | + ).toBe(result.content); |
| 135 | + }); |
| 136 | + |
| 137 | + it('does not treat methods or getters as static config properties', () => { |
| 138 | + const input = 'export default { test: { get browser() { return settings; } } };'; |
| 139 | + expect(migrateVitestV5Config('vite.config.ts', input, options).content).toBe(input); |
| 140 | + }); |
| 141 | + |
| 142 | + it('returns the original source when an edit would produce invalid syntax', () => { |
| 143 | + const editor = new SourceEditor('test.ts', 'const value = 1;'); |
| 144 | + editor.edit(0, 5, 'const ='); |
| 145 | + expect(editor.finish()).toEqual({ |
| 146 | + content: 'const value = 1;', |
| 147 | + findings: [expect.objectContaining({ code: 'unsafe-syntax' })], |
| 148 | + }); |
| 149 | + }); |
| 150 | +}); |
0 commit comments