UNPKG

@baseplate-dev/core-generators

Version:

Core generators for Baseplate

167 lines 6.5 kB
import { expect } from 'vitest'; import { areFragmentsEqual, normalizeFragment } from './utils.js'; /** * Formats imports for display in error messages */ function formatImports(imports) { if (!imports || imports.length === 0) return '(none)'; return imports .map((imp) => { const parts = []; if (imp.isTypeOnly) parts.push('type'); if (imp.namespaceImport) { parts.push(`* as ${imp.namespaceImport}`); } if (imp.defaultImport) { parts.push(imp.defaultImport); } if (imp.namedImports && imp.namedImports.length > 0) { const namedStr = imp.namedImports .map((ni) => (ni.alias ? `${ni.name} as ${ni.alias}` : ni.name)) .join(', '); parts.push(`{ ${namedStr} }`); } return ` ${parts.join(' ')} from '${imp.moduleSpecifier}'`; }) .join('\n'); } /** * Formats hoisted fragments for display in error messages */ function formatHoistedFragments(fragments) { if (!fragments || fragments.length === 0) return '(none)'; return fragments .map((frag) => ` [${frag.key}]: ${frag.contents}`) .join('\n'); } /** * Checks if an import declaration contains a specific named import */ function hasNamedImport(imp, name, options) { if (!imp.namedImports) return false; return imp.namedImports.some((ni) => { if (ni.name !== name) return false; // If isTypeOnly is specified in options, check it matches if (options?.isTypeOnly !== undefined) { // Check both the named import level and the declaration level const isImportTypeOnly = ni.isTypeOnly ?? imp.isTypeOnly ?? false; return isImportTypeOnly === options.isTypeOnly; } return true; }); } /** * Custom Vitest matchers for TypeScript code fragments */ export const fragmentMatchers = { /** * Asserts that a TypeScript fragment matches the expected fragment * Compares contents, imports (order-independent), and optionally hoisted fragments * * @example * ```typescript * expect(actualFragment).toMatchTsFragment(expectedFragment); * expect(actualFragment).toMatchTsFragment(expectedFragment, { * ignoreHoistedFragments: true * }); * ``` */ toMatchTsFragment(received, expected, options) { const { isNot } = this; // Normalize both fragments for comparison const normalizedReceived = normalizeFragment(received, { compareHoistedFragments: !options?.ignoreHoistedFragments, }); const normalizedExpected = normalizeFragment(expected, { compareHoistedFragments: !options?.ignoreHoistedFragments, }); // Check equality const pass = areFragmentsEqual(received, expected, { compareHoistedFragments: !options?.ignoreHoistedFragments, }); return { pass, message: () => { if (pass && isNot) { return 'Expected fragments not to be equal'; } const messages = ['Expected fragments to be equal']; // Check contents if (normalizedReceived.contents !== normalizedExpected.contents) { messages.push('', 'Contents:', ` Expected: ${normalizedExpected.contents}`, ` Received: ${normalizedReceived.contents}`); } // Check imports const receivedImportsStr = formatImports(normalizedReceived.imports); const expectedImportsStr = formatImports(normalizedExpected.imports); if (receivedImportsStr !== expectedImportsStr) { messages.push('', 'Imports:', 'Expected:', expectedImportsStr, 'Received:', receivedImportsStr); } // Check hoisted fragments if not ignored if (!options?.ignoreHoistedFragments) { const receivedHoistedStr = formatHoistedFragments(normalizedReceived.hoistedFragments); const expectedHoistedStr = formatHoistedFragments(normalizedExpected.hoistedFragments); if (receivedHoistedStr !== expectedHoistedStr) { messages.push('', 'Hoisted Fragments:', 'Expected:', expectedHoistedStr, 'Received:', receivedHoistedStr); } } return messages.join('\n'); }, }; }, /** * Asserts that a fragment includes a specific import * * @example * ```typescript * expect(fragment).toIncludeImport('z', 'zod'); * expect(fragment).toIncludeImport('Prisma', '@prisma/client', { isTypeOnly: true }); * ``` */ toIncludeImport(received, name, from, options) { const { isNot } = this; const imports = received.imports ?? []; const matchingImport = imports.find((imp) => imp.moduleSpecifier === from); const pass = matchingImport !== undefined && hasNamedImport(matchingImport, name, options); return { pass, message: () => { if (pass && isNot) { const typeOnlyStr = options?.isTypeOnly ? ' (type-only)' : ''; return `Expected not to include import "${name}"${typeOnlyStr} from "${from}"`; } const typeOnlyStr = options?.isTypeOnly ? ' (type-only)' : ''; const foundFrom = matchingImport ? `, but found import from "${matchingImport.moduleSpecifier}"` : ''; return [ `Expected to include import "${name}"${typeOnlyStr} from "${from}"${foundFrom}`, '', 'Available imports:', formatImports(imports), ].join('\n'); }, }; }, }; /** * Extends Vitest's expect with custom matchers for TypeScript fragments * Call this function once in your test setup to enable the matchers * * @example * ```typescript * // In test setup file or at the top of test file * import { extendFragmentMatchers } from '@baseplate-dev/core-generators/test-helpers'; * * extendFragmentMatchers(); * ``` */ export function extendFragmentMatchers() { expect.extend(fragmentMatchers); } //# sourceMappingURL=matchers.js.map