Files
vtj/packages/parser/tests/scriptSetup.test.ts
“chenhuachun” f19cd22bb1 refactor(parser): 优化.then回调函数提取逻辑
- 在vue/scripts.ts中新增extractThenCallback函数,准确提取.then()的回调内容
- 替换原先使用正则匹配.then()方法的写法,提升匹配准确性
- 在vue/scriptSetup.ts中同步新增并使用extractThenCallback函数
- 更新相关单元测试,覆盖含.catch和.finally链式调用场景下的回调函数提取
- 改进代码可读性和健壮性,确保对复杂.then回调的正确解析
2026-06-14 21:39:27 +08:00

369 lines
9.7 KiB
TypeScript

import { expect, test, describe } from 'vitest';
import { parseScriptSetup } from '../src/vue/scriptSetup';
import { project } from './sources/project';
describe('extractDataSourceFromCode - transform field', () => {
test('should extract transform correctly with .catch and .finally', () => {
const code = `
const fetchUserInfo = async (...args) => {
return await __provider.apis['getUserInfo']
.apply(null, args)
.then((res) => {
__state.userInfo = res.data;
})
.catch((e) => {})
.finally(() => {
__state.loading = false;
});
}
`;
// 模拟 extractThenCallback 的逻辑
const extractThenCallback = (code: string): string | null => {
const thenIndex = code.indexOf('.then(');
if (thenIndex === -1) return null;
let depth = 1;
let pos = thenIndex + 6;
while (pos < code.length && depth > 0) {
const char = code[pos];
if (char === '(') depth++;
else if (char === ')') depth--;
pos++;
}
return code.substring(thenIndex + 6, pos - 1).trim();
};
const transform = extractThenCallback(code);
expect(transform).toBe(
'(res) => {\n __state.userInfo = res.data;\n }'
);
});
test('should extract transform without .catch or .finally', () => {
const code = `
const fetchData = async (...args) => {
return await __provider.apis['getData']
.apply(null, args)
.then((res) => {
return res.data;
});
}
`;
const extractThenCallback = (code: string): string | null => {
const thenIndex = code.indexOf('.then(');
if (thenIndex === -1) return null;
let depth = 1;
let pos = thenIndex + 6;
while (pos < code.length && depth > 0) {
const char = code[pos];
if (char === '(') depth++;
else if (char === ')') depth--;
pos++;
}
return code.substring(thenIndex + 6, pos - 1).trim();
};
const transform = extractThenCallback(code);
expect(transform).toBe('(res) => {\n return res.data;\n }');
});
});
const setupSource = `
import { ref, reactive, computed, watch, onMounted, inject, provide, defineProps, defineEmits, defineExpose } from 'vue';
import { ElButton } from 'element-plus';
const props = defineProps({
title: { type: String, required: true }
});
const emit = defineEmits(['change', 'update']);
const count = ref(0);
const message = ref('hello');
const __state = reactive({
loading: false,
list: []
});
const form = reactive({
name: '',
age: 0
});
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
emit('change', count.value);
}
const theme = inject('theme', 'light');
provide('key', 'value');
watch(
() => count.value,
(val) => {
console.log('changed:', val);
},
{ immediate: true }
);
onMounted(() => {
console.log('mounted');
});
defineExpose({ count, increment });
`;
describe('parseScriptSetup', () => {
const result = parseScriptSetup(setupSource, project);
test('should parse imports', () => {
expect(result.imports).toBeDefined();
expect(result.imports!.length).toBeGreaterThan(0);
const vueImport = result.imports!.find((i) => i.from === 'vue');
expect(vueImport).toBeDefined();
});
test('should parse refs', () => {
expect(result.refs).toBeDefined();
expect(result.refs!['count']).toBeDefined();
expect(result.refs!['message']).toBeDefined();
});
test('should parse state from reactive', () => {
expect(result.state).toBeDefined();
expect(result.state!['loading']).toBeDefined();
expect(result.state!['list']).toBeDefined();
});
test('should parse other reactives', () => {
expect(result.reactives).toBeDefined();
expect(result.reactives!['form']).toBeDefined();
});
test('should parse computed', () => {
expect(result.computed).toBeDefined();
expect(result.computed!['doubleCount']).toBeDefined();
});
test('should parse methods', () => {
expect(result.methods).toBeDefined();
expect(result.methods!['increment']).toBeDefined();
});
test('should parse props via defineProps', () => {
expect(result.props).toBeDefined();
expect(result.props!.length).toBeGreaterThan(0);
});
test('should parse emits via defineEmits', () => {
expect(result.emits).toBeDefined();
expect(result.emits!.length).toBe(2);
});
test('should parse watch', () => {
expect(result.watch).toBeDefined();
expect(result.watch!.length).toBeGreaterThan(0);
});
test('should parse lifeCycles', () => {
expect(result.lifeCycles).toBeDefined();
expect(result.lifeCycles!['onMounted']).toBeDefined();
});
test('should parse inject', () => {
expect(result.inject).toBeDefined();
expect(result.inject!.length).toBeGreaterThan(0);
expect(result.inject![0].name).toBe('theme');
});
test('should parse provide', () => {
expect(result.provide).toBeDefined();
expect(result.provide!['key']).toBeDefined();
});
test('should parse expose', () => {
expect(result.expose).toBeDefined();
expect(result.expose).toContain('count');
expect(result.expose).toContain('increment');
});
});
describe('parseScriptSetup with ClassDeclaration', () => {
test('should throw error for top-level class declaration', () => {
const classSource = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: '1kzck23e', version: '1781335863126' })
class TClass {
}
`;
expect(() => parseScriptSetup(classSource, project)).toThrow(
/无法处理顶层类声明/
);
});
});
describe('parseScriptSetup orphan variable fallback → reactive', () => {
test('should convert const with {} to reactive', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const data1 = {};
`;
const result = parseScriptSetup(source, project);
expect(result.reactives).toBeDefined();
expect(result.reactives['data1']).toBeDefined();
expect(result.reactives['data1']).toHaveProperty('value', '({})');
// setup 中不应包含该游离声明
if (result.setup) {
expect(result.setup.value).not.toContain('data1');
}
});
test('should convert const with [] to reactive', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const data2 = [];
`;
const result = parseScriptSetup(source, project);
expect(result.reactives).toBeDefined();
expect(result.reactives['data2']).toBeDefined();
expect(result.reactives['data2']).toHaveProperty('value', '[]');
});
test('should convert multiple orphan {} and [] in same SFC', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const data1 = {};
const data2 = [];
`;
const result = parseScriptSetup(source, project);
expect(result.reactives).toBeDefined();
expect(result.reactives['data1']).toBeDefined();
expect(result.reactives['data1']).toHaveProperty('value', '({})');
expect(result.reactives['data2']).toBeDefined();
expect(result.reactives['data2']).toHaveProperty('value', '[]');
});
test('should convert multiple declarators in one const with object/array literals', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const a = {}, b = [];
`;
const result = parseScriptSetup(source, project);
expect(result.reactives).toBeDefined();
expect(result.reactives['a']).toBeDefined();
expect(result.reactives['a']).toHaveProperty('value', '({})');
expect(result.reactives['b']).toBeDefined();
expect(result.reactives['b']).toHaveProperty('value', '[]');
});
test('should throw error for const with number literal', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const count = 42;
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层变量声明/
);
});
test('should throw error for const with string literal', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const title = 'hello';
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层变量声明/
);
});
test('should throw error for const with Identifier', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const other = __provider;
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层变量声明/
);
});
test('should throw error for local useXxx() call', () => {
const source = `
import { ref } from 'vue';
function useMyHelper() {
const x = ref(0);
return x;
}
const val = useMyHelper();
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层变量声明/
);
});
test('should throw error for const with unrecognized function call', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
const useMouse = function () {
return { value: 'abc' };
};
const data4 = useMouse();
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层变量声明/
);
});
test('should throw error for top-level class declaration', () => {
const source = `
import { useProvider } from '@vtj/renderer';
const __provider = useProvider({ id: 'test', version: '1' });
class KClass {}
`;
expect(() => parseScriptSetup(source, project)).toThrow(
/无法处理顶层类声明/
);
});
});