Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions examples/src/components/button/__tests__/Button2.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import { Button } from '../index';
import Button from '../index';

describe('Button.tsx', () => {

it('renders emits click', async () => {
const fn = vi.fn()
const wrapper = mount({
Expand Down
4 changes: 2 additions & 2 deletions examples/src/components/button/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UiButton from './Button'

export default UiButton
// export default UiButton

export { UiButton as Button }
export { UiButton as default }
1 change: 1 addition & 0 deletions examples/src/components/input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineComponent({

props: {
...props,
label: String
},

slots: Object as SlotsType<{
Expand Down
5 changes: 3 additions & 2 deletions examples/src/components/input/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import warpInput from './warpInput'
export default warpInput
import { withInstall } from './warpInput'
import Input from './Input'
export default withInstall(Input)
6 changes: 6 additions & 0 deletions examples/src/components/input/input.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,10 @@ describe('Input.tsx', () => {
(wrapper.vm as any).select();
(wrapper.vm as any).clear();
});

it('renders with custom class', () => {
render(<Input label="custom-class" />)
// expect(screen.getByRole('textbox')).toHaveClass('custom-class')
expect(1).toBe(1)
})
});
16 changes: 13 additions & 3 deletions examples/src/components/input/warpInput.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import Input from './Input'
Input.aaa = 1
export default Input
export type WithInstall<T> = T & {
install: (app: any) => void;
};

export function withInstall<T>(options: T) {
(options as Record<string, unknown>).install = (app: any) => {
const { name } = options as unknown as { name: string };
app.component(name, options);
};

return options as WithInstall<T>;
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vc-api-coverage",
"version": "0.6.1",
"version": "0.6.2",
"description": "Vue Component API Coverage Reporter",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/ApiReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default class VcCoverageReporter implements Reporter {
info.slots.total += comp.slots.length
info.exposes.total += comp.exposes.length
info.props.details = comp.props.map(p => ({ name: p, covered: unit.props.includes(p) }))
info.slots.details = unit.slots.map(s => ({ name: s, covered: true }))
info.slots.details = comp.slots.length > 0 ? comp.slots.map(s => ({ name: s, covered: unit.slots.includes(s) })) : unit.slots.map(s => ({ name: s, covered: true }))
info.exposes.details = comp.exposes.map(e => ({ name: e, covered: unit.exposes.includes(e) }))
info.props.covered = info.props.details.filter(d => d.covered).length
info.slots.covered = info.slots.details.filter(d => d.covered).length
Expand Down
11 changes: 3 additions & 8 deletions src/analyzer/ComponentAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SourceFile, Node, Type, Expression, ObjectLiteralExpression, SyntaxKind } from "ts-morph";
import { isComponentType } from "../common/utils";

class ComponentAnalyzer {
private sourceFile: SourceFile;
Expand Down Expand Up @@ -170,12 +171,6 @@ class ComponentAnalyzer {
return item.getText().replace(/[\'\"\`]/g, '');
}

isComponentFile(type: Type) {
const constructSignatures = type.getConstructSignatures();
if (constructSignatures.length === 0) return false;
return true
}

getExportedExpression() {
let exportedExpression = this.getExportedExpressionFromDefault();
if (exportedExpression) return exportedExpression;
Expand All @@ -202,7 +197,7 @@ class ComponentAnalyzer {
}
}

if (expression && this.isComponentFile(expression.getType())) {
if (expression && isComponentType(expression.getType())) {
return expression;
}
return null;
Expand All @@ -212,7 +207,7 @@ class ComponentAnalyzer {
const namedExportSymbol = this.sourceFile.getExportSymbols().find(symbol => {
const valueDeclaration = symbol.getValueDeclaration();
if (!valueDeclaration) return false;
return this.isComponentFile(valueDeclaration.getType());
return isComponentType(valueDeclaration.getType());
});

if (!namedExportSymbol) return null;
Expand Down
39 changes: 39 additions & 0 deletions src/analyzer/UnitTestAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Project, SyntaxKind, Node, SourceFile, CallExpression, ObjectLiteralExpression, JsxSelfClosingElement, JsxElement, Identifier, Symbol } from 'ts-morph';
import { isComponentFile, isComponentType } from '../common/utils';

interface TestUnit {
props?: string[];
Expand Down Expand Up @@ -89,12 +90,33 @@ class TestUnitAnalyzer {
if (!declarationNode) return null;
const declarationSourceFile = declarationNode.getSourceFile();
const originalPath = declarationSourceFile.getFilePath();
if (!isComponentFile(originalPath)) {
return this.resolveTsPath(declarationNode);
}
return originalPath;
} catch (error) {
return null;
}
}

// 解析ts路径
resolveTsPath(declarationNode: Node) {
if (!Node.isExportAssignment(declarationNode)) return null;
const exportedExpression = declarationNode.getExpression();
if (Node.isCallExpression(exportedExpression)) {
const args = exportedExpression.getArguments();
for (const arg of args) {
const argType = arg.getType();
if (isComponentType(argType)) {
// 获取文件路径
const res = this.resolveComponentPath(arg as Identifier) as string;
return res;
}
}
}
return null;
}


// 分析传统挂载mount/shallowMount方法调用
private analyzeTraditionalMountCalls(testCall: CallExpression) {
Expand Down Expand Up @@ -545,6 +567,23 @@ class TestUnitAnalyzer {
if (!component.emits.includes(propName)) {
component.emits.push(propName);
}
} else if (propName === 'v-slots') {
// Handle v-slots directive
const initializer = attr.getInitializer();
if (initializer && Node.isJsxExpression(initializer)) {
const expression = initializer.getExpression();
if (expression && Node.isObjectLiteralExpression(expression)) {
component.slots = component.slots || [];
expression.getProperties().forEach(prop => {
if (Node.isPropertyAssignment(prop) || Node.isShorthandPropertyAssignment(prop)) {
const slotName = prop.getName();
if (slotName && !component.slots!.includes(slotName)) {
component.slots!.push(slotName);
}
}
});
}
}
} else {
let isVModel = false;
// Handle v-model transformation for props
Expand Down
7 changes: 7 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from 'chalk';
import { VcCoverageData } from '../types';
import path from 'path';
import fs from 'fs';
import { Type } from 'ts-morph';

export function logDebug(moduleName: string, message: string, ...args: any[]) {
if (process.env.DEBUG) {
Expand Down Expand Up @@ -109,4 +110,10 @@ export function getThrowableMessage(e: Error, split = '\n') {
}
}
return '';
}

export function isComponentType(type: Type) {
const constructSignatures = type.getConstructSignatures();
if (constructSignatures.length === 0) return false;
return true
}
3 changes: 1 addition & 2 deletions src/reporter/CliReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function generateCliReport(allCoverageData: VcCoverageData[]): string {
formatComponentName('All', totalPercentage),
colorizePercentage(propsCoverage),
colorizePercentage(slotsCoverage),
colorizePercentage(exposesCoverage)
colorizePercentage(exposesCoverage),
];

// 如果有未覆盖的API或空组件,添加空列
Expand Down Expand Up @@ -141,7 +141,6 @@ export function generateCliReport(allCoverageData: VcCoverageData[]): string {
formatComponentName(data.name, totalPercentage),
chalk.dim('N/A'),
chalk.dim('N/A'),
chalk.dim('N/A'),
chalk.dim('N/A')
];

Expand Down
21 changes: 21 additions & 0 deletions test/analyzer/test-units-analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ describe('test-units-analyzer', () => {
expect(res[`./ButtonSlot.tsx`].slots!.sort()).toEqual(['footer', 'header', 'trigger'].sort())
})


it('should analyze `v-slots` in test units without default slot', () => {
const fakeTestFilePath = './slots-analyzer.test.tsx'
const project = new Project()
const sourceFile = project.createSourceFile(fakeTestFilePath, `
import Button from './ButtonSlot.tsx';
import { describe, it, expect, test } from 'vitest';
import { shallowMount } from '@vue/test-utils'
import { render } from '@testing-library/vue'

describe('components', () => {
it('should render correctly 1', () => {
render(() => <Button v-slots={{ header: () => 'Hello World' }}></Button>, {})
expect(1).toBe(1)
})
})
`)
const res = new TestUnitAnalyzer(sourceFile, project).analyze()
expect(res[`./ButtonSlot.tsx`].slots!.sort()).toEqual(['header'].sort())
})

it('should not analyze props in `mount` test units without expect', () => {
const fakeTestFilePath = './prop-analyzer.test.tsx'
const project = new Project()
Expand Down