/**
* A Vue Options API component's functions are symbols: `methods`, `computed`,
* `watch` entries and the lifecycle hooks, each owning the calls written in it.
* (`src/extraction/vue-options-api.ts`, wired in `vue-extractor.ts`.)
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { vueOptionsMembers } from '../src/extraction/vue-options-api';
describe('vueOptionsMembers', () => {
it('names every function an options object declares', () => {
const script = `
import { mapActions } from 'vuex'
export default {
name: 'Login',
props: { redirect: String },
data() {
return { loading: false }
},
computed: {
title() { return 'Sign in' },
fullName: {
get() { return this.first },
set(v) { this.first = v }
},
...mapGetters(['user'])
},
watch: {
$route: { handler(route) { this.redirect = route.query.redirect }, immediate: true },
'form.email'(value) { this.check(value) },
loading: 'onLoading'
},
created() { this.init() },
mounted: function () { this.focus() },
methods: {
...mapActions(['login']),
async handleLogin() { await this.login() },
validate: (value) => value.length > 0,
reset: function () {},
[DYNAMIC]() {}
}
}
`;
expect(vueOptionsMembers(script).map((m) => m.name)).toEqual([
'data', 'title', 'fullName', '$route', 'form.email', 'created', 'mounted', 'handleLogin', 'validate', 'reset',
]);
});
it('reads defineComponent, Vue.extend and Vue.component, and nothing without an options object', () => {
expect(vueOptionsMembers(`export default defineComponent({ setup() { return {} } })`).map((m) => m.name)).toEqual(['setup']);
expect(vueOptionsMembers(`export default Vue.extend({ methods: { go() {} } })`).map((m) => m.name)).toEqual(['go']);
expect(vueOptionsMembers(`export default Vue.component('x-btn', { methods: { tap() {} } })`).map((m) => m.name)).toEqual(['tap']);
expect(vueOptionsMembers(`const x = { methods: { go() {} } }\nexport default x`)).toEqual([]);
});
});
describe('a Vue Options API component, indexed', () => {
let root = '';
afterAll(() => {
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('gives each method its calls, and the template handler its method', async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vue-options-'));
fs.mkdirSync(path.join(root, 'src/views/login'), { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'admin', dependencies: { vue: '^2.6.0' } }));
fs.writeFileSync(
path.join(root, 'src/views/login/index.vue'),
`
`
);
const cg = await CodeGraph.init(root, { index: true });
try {
const nodes = cg.getNodesInFile('src/views/login/index.vue');
const method = (name: string) => nodes.find((n) => n.kind === 'method' && n.name === name)!;
expect(nodes.filter((n) => n.kind === 'method').map((n) => n.name).sort()).toEqual(['data', 'handleLogin', 'validate']);
expect(method('handleLogin').qualifiedName).toBe('index::handleLogin');
// The call inside handleLogin is handleLogin's, not the file's.
const calls = cg.getOutgoingEdges(method('handleLogin').id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name);
expect(calls).toContain('validate');
// `@click="handleLogin"` in the template runs the method.
const component = nodes.find((n) => n.kind === 'component')!;
const handlers = cg.getOutgoingEdges(component.id).filter((e) => e.target === method('handleLogin').id);
expect(handlers.length).toBeGreaterThan(0);
} finally {
cg.close();
}
});
});