Files
portal/app/vite-plugins/dev-indices/__tests__/signature.test.ts
T

145 lines
5.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
computeSignature,
normalizeFilePath,
extractStaticText,
extractDistinctiveAttrs,
type SignatureNode,
type SignatureContext,
} from '../signature';
// Minimal node shape that matches what we extract from @vue/compiler-dom AST.
// In production we adapt the parser's ElementNode → SignatureNode.
const node = (overrides: Partial<SignatureNode> = {}): SignatureNode => ({
tag: 'div',
props: [],
children: [],
...overrides,
});
const ctx = (overrides: Partial<SignatureContext> = {}): SignatureContext => ({
filePath: 'app/resources/js/components/AppSidebar.vue',
appRoot: 'app/',
ancestorChain: ['AppSidebar'],
sameTagOrdinal: 0,
...overrides,
});
describe('normalizeFilePath', () => {
it('strips appRoot and .vue extension, uses forward slashes', () => {
expect(
normalizeFilePath('app/resources/js/components/A.vue', 'app/'),
).toBe('resources/js/components/A');
});
it('handles backslashes on Windows-style paths', () => {
expect(
normalizeFilePath('app\\resources\\js\\A.vue', 'app/'),
).toBe('resources/js/A');
});
});
describe('extractStaticText', () => {
it('returns first 24 chars of plain text child', () => {
const n = node({ children: [{ type: 'text', content: 'Создать проект сейчас же' }] });
expect(extractStaticText(n)).toBe('создать проект сейчас же');
});
it('truncates >24 chars', () => {
const n = node({ children: [{ type: 'text', content: 'A'.repeat(50) }] });
expect(extractStaticText(n)).toHaveLength(24);
});
it('ignores mustache interpolations', () => {
const n = node({
children: [
{ type: 'text', content: 'count is ' },
{ type: 'interpolation', content: '{{ count }}' },
],
});
expect(extractStaticText(n)).toBe('count is');
});
it('returns null when no static text', () => {
const n = node({ children: [{ type: 'interpolation', content: '{{ x }}' }] });
expect(extractStaticText(n)).toBeNull();
});
});
describe('extractDistinctiveAttrs', () => {
it('extracts allowlisted static attrs sorted by name', () => {
const n = node({
tag: 'v-btn',
props: [
{ type: 'attribute', name: 'icon', value: 'plus' },
{ type: 'attribute', name: 'role', value: 'button' },
{ type: 'attribute', name: 'class', value: 'mr-2' },
],
});
expect(extractDistinctiveAttrs(n)).toBe('icon=plus,role=button');
});
it('ignores v-bind / : attrs (dynamic)', () => {
const n = node({
props: [
{ type: 'directive', name: 'bind', arg: 'icon', exp: 'someVar' },
{ type: 'attribute', name: 'id', value: 'static-id' },
],
});
expect(extractDistinctiveAttrs(n)).toBe('id=static-id');
});
it('returns empty string if no allowlisted attrs', () => {
const n = node({ props: [{ type: 'attribute', name: 'class', value: 'foo' }] });
expect(extractDistinctiveAttrs(n)).toBe('');
});
it('escape-hatch: data-dev-name takes precedence', () => {
const n = node({
tag: 'v-btn',
props: [
{ type: 'attribute', name: 'icon', value: 'plus' },
{ type: 'attribute', name: 'data-dev-name', value: 'sidebar.create-btn' },
],
});
expect(extractDistinctiveAttrs(n)).toContain('data-dev-name=sidebar.create-btn');
});
});
describe('computeSignature', () => {
it('canonical signature for a plain element', () => {
const n = node({
tag: 'v-btn',
props: [{ type: 'attribute', name: 'icon', value: 'plus' }],
children: [{ type: 'text', content: 'Создать' }],
});
expect(computeSignature(n, ctx({ ancestorChain: ['AppSidebar', 'nav', 'v-list-item'] }))).toBe(
'resources/js/components/AppSidebar::AppSidebar>nav>v-list-item::v-btn[icon=plus]::создать::0',
);
});
it('identical templates produce identical signatures', () => {
const a = node({ tag: 'span', children: [{ type: 'text', content: 'hi' }] });
const b = node({ tag: 'span', children: [{ type: 'text', content: 'hi' }] });
expect(computeSignature(a, ctx())).toBe(computeSignature(b, ctx()));
});
it('different ordinalAmongSameTag yields different signatures', () => {
const a = node({ tag: 'v-btn' });
const s0 = computeSignature(a, ctx({ sameTagOrdinal: 0 }));
const s1 = computeSignature(a, ctx({ sameTagOrdinal: 1 }));
expect(s0).not.toBe(s1);
});
it('data-dev-name overrides structural pieces', () => {
const a = node({
tag: 'v-btn',
props: [{ type: 'attribute', name: 'data-dev-name', value: 'sidebar.create' }],
});
const sigA = computeSignature(a, ctx({ sameTagOrdinal: 0 }));
// Same name, different ordinal — signature MUST be identical (escape hatch)
const sigB = computeSignature(a, ctx({ sameTagOrdinal: 7 }));
expect(sigA).toBe(sigB);
});
});