diff --git a/src/commands/changelog.ts b/src/commands/changelog.ts index 6a0e06d..9e380cc 100644 --- a/src/commands/changelog.ts +++ b/src/commands/changelog.ts @@ -5,7 +5,6 @@ import { defaultArgs } from '@/args/default.ts' import { resolveConfig } from '@/config.ts' import { diffComponent } from '@/utils/api-diff.ts' import { logError } from '@/utils/error.ts' -import { loadVersionMetaData } from '@/utils/loader.ts' import { output } from '@/utils/output.ts' import { resolveVersion } from '@/utils/version.ts' @@ -144,11 +143,9 @@ export default defineCommand({ const config = resolveConfig(args) try { - const [fromSnapshot, toSnapshot] = await Promise.all([ - resolveVersion({ ...config, version: args.from }).then(loadVersionMetaData), - resolveVersion({ ...config, version: args.to }).then(loadVersionMetaData), - ]) - const result = diffComponent(fromSnapshot, toSnapshot, args.component) + const v1 = await resolveVersion({ ...config, version: args.from }) + const v2 = await resolveVersion({ ...config, version: args.to }) + const result = await diffComponent(v1, v2, args.component) output({ json: result, diff --git a/src/utils/api-diff.ts b/src/utils/api-diff.ts index 7e583ad..61d6026 100644 --- a/src/utils/api-diff.ts +++ b/src/utils/api-diff.ts @@ -1,5 +1,6 @@ import type { ChangedApiItem, ComponentDiff, ComponentDiffResult, FlatApiItem } from '#/changelog.ts' -import type { ChangelogFile, ComponentRecord } from '#/components.ts' +import type { ComponentRecord } from '#/components.ts' +import type { ResolvedVersion } from '@/types.ts' import { compareDiffItems, flattenComponent, @@ -7,6 +8,7 @@ import { toDiffApiItem, } from '@/utils/api-diff-normalize.ts' import { matchRenamedItems } from '@/utils/api-diff-rename.ts' +import { loadVersionMetaData } from '@/utils/loader.ts' function getItemKey(item: FlatApiItem): string { return `${item.scope}\0${item.category}\0${item.identity}` @@ -118,11 +120,10 @@ function findComponent(components: ComponentRecord[], name: string): ComponentRe return components.find(component => component.name.toLowerCase() === normalizedName) } -export function diffComponent( - fromSnapshot: ChangelogFile, - toSnapshot: ChangelogFile, - component?: string, -): ComponentDiffResult { +export async function diffComponent(v1: ResolvedVersion, v2: ResolvedVersion, component?: string): Promise { + const fromSnapshot = await loadVersionMetaData(v1) + const toSnapshot = await loadVersionMetaData(v2) + const componentNames = component ? [component] : [...new Set([ diff --git a/test/changelog-command.test.ts b/test/changelog-command.test.ts index 0345d6b..99597b2 100644 --- a/test/changelog-command.test.ts +++ b/test/changelog-command.test.ts @@ -40,22 +40,25 @@ afterEach(() => { }) describe('changelog command', () => { - it('starts loading both snapshots before either one resolves', async () => { - const pending = new Map void>() - mockedLoadVersionMetaData.mockImplementation((version) => { - return new Promise(resolve => pending.set(version.version, resolve)) - }) + it('resolves both versions and loads their snapshots', async () => { + mockedLoadVersionMetaData.mockImplementation(async version => emptySnapshot(version.version)) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const execution = runCommand(changelogCommand, { + + await runCommand(changelogCommand, { rawArgs: ['1.0.5', '1.5.2', '--format', 'json'], }) - await vi.waitFor(() => expect(mockedLoadVersionMetaData).toHaveBeenCalledTimes(2)) - expect([...pending.keys()]).toEqual(['1.0.5', '1.5.2']) - - pending.get('1.0.5')!(emptySnapshot('1.0.5')) - pending.get('1.5.2')!(emptySnapshot('1.5.2')) - await execution + expect(mockedResolveVersion).toHaveBeenCalledTimes(2) + expect(mockedResolveVersion).toHaveBeenNthCalledWith(1, expect.objectContaining({ version: '1.0.5' })) + expect(mockedResolveVersion).toHaveBeenNthCalledWith(2, expect.objectContaining({ version: '1.5.2' })) + expect(mockedLoadVersionMetaData).toHaveBeenNthCalledWith(1, { + version: '1.0.5', + majorVersion: 'v1', + }) + expect(mockedLoadVersionMetaData).toHaveBeenNthCalledWith(2, { + version: '1.5.2', + majorVersion: 'v1', + }) expect(consoleSpy).toHaveBeenCalledWith(JSON.stringify({ from: '1.0.5', diff --git a/test/changelog.test.ts b/test/changelog.test.ts index 241a519..5f18f64 100644 --- a/test/changelog.test.ts +++ b/test/changelog.test.ts @@ -1,10 +1,17 @@ +import type { ResolvedVersion } from '../src/types.ts' import type { ComponentDiff } from '../src/types/changelog' import type { ChangelogFile, ComponentApiItemRecord, ComponentApiRecord, ComponentRecord } from '../src/types/components.ts' import { join } from 'node:path' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { diffComponent } from '../src/utils/api-diff.ts' import { loadVersionMetaData } from '../src/utils/loader.ts' +vi.mock('../src/utils/loader.ts', () => ({ + loadVersionMetaData: vi.fn(), +})) + +const mockedLoadVersionMetaData = vi.mocked(loadVersionMetaData) + function apiItem( name: string, type = 'string', @@ -66,8 +73,31 @@ function snapshot(version: string, components: ComponentRecord[]): ChangelogFile } } +function resolvedVersion(version: string): ResolvedVersion { + return { + version, + majorVersion: `v${version.split('.')[0]}`, + } +} + +async function diffSnapshots( + from: ChangelogFile, + to: ChangelogFile, + component?: string, +) { + mockedLoadVersionMetaData + .mockResolvedValueOnce(from) + .mockResolvedValueOnce(to) + + return await diffComponent( + resolvedVersion(from.version), + resolvedVersion(to.version), + component, + ) +} + describe('diffComponent', () => { - it('returns added, removed, and changed API items in the requested shape', () => { + it('returns added, removed, and changed API items in the requested shape', async () => { const from = snapshot('1.0.5', [ component('Button', apiRecord({ properties: [ @@ -85,7 +115,7 @@ describe('diffComponent', () => { })), ]) - expect(diffComponent(from, to, 'button')).toEqual({ + await expect(diffSnapshots(from, to, 'button')).resolves.toEqual({ from: '1.0.5', to: '1.5.2', diffs: [{ @@ -113,10 +143,10 @@ describe('diffComponent', () => { }) }) - it('treats markdown strikethrough as a deprecation change', () => { + it('treats markdown strikethrough as a deprecation change', async () => { const fromItem = apiItem('rootStyle', 'CSSProperties', '-', 'Style on the root element') const toItem = apiItem('~~rootStyle~~', 'CSSProperties', '-', 'Deprecated. Use `styles.root` instead') - const result = diffComponent( + const result = await diffSnapshots( snapshot('1.0.5', [component('Tree', apiRecord({ properties: [fromItem] }))]), snapshot('1.5.2', [component('Tree', apiRecord({ properties: [toItem] }))]), 'Tree', @@ -135,10 +165,10 @@ describe('diffComponent', () => { }) }) - it('compares method signatures under the same method identity', () => { + it('compares method signatures under the same method identity', async () => { const fromItem = apiItem('scrollTo({ key: Key })', '') const toItem = apiItem('scrollTo({ key: Key, autoExpand?: boolean })', '') - const result = diffComponent( + const result = await diffSnapshots( snapshot('1.0.5', [component('Tree', apiRecord({ methods: [fromItem] }))]), snapshot('1.5.2', [component('Tree', apiRecord({ methods: [toItem] }))]), 'Tree', @@ -155,7 +185,7 @@ describe('diffComponent', () => { }]) }) - it('moves high-confidence replacements into changed as renamed', () => { + it('moves high-confidence replacements into changed as renamed', async () => { const fromItem = apiItem( 'dropdownRender', '(originNode: VueNode) => VueNode', @@ -163,7 +193,7 @@ describe('diffComponent', () => { 'Deprecated. Use `popupRender` instead', ) const toItem = apiItem('popupRender', '(originNode: VueNode) => VueNode') - const result = diffComponent( + const result = await diffSnapshots( snapshot('1.0.5', [component('Select', apiRecord({ properties: [fromItem] }))]), snapshot('1.5.2', [component('Select', apiRecord({ properties: [toItem] }))]), 'Select', @@ -183,10 +213,10 @@ describe('diffComponent', () => { }) }) - it('uses Levenshtein similarity for structurally compatible renames', () => { + it('uses Levenshtein similarity for structurally compatible renames', async () => { const fromItem = apiItem('popupRenderer', '() => VueNode') const toItem = apiItem('popupRender', '() => VueNode') - const result = diffComponent( + const result = await diffSnapshots( snapshot('1.0.5', [component('Select', apiRecord({ properties: [fromItem] }))]), snapshot('1.5.2', [component('Select', apiRecord({ properties: [toItem] }))]), 'Select', @@ -199,8 +229,8 @@ describe('diffComponent', () => { }]) }) - it('keeps ambiguous rename candidates as added and removed', () => { - const result = diffComponent( + it('keeps ambiguous rename candidates as added and removed', async () => { + const result = await diffSnapshots( snapshot('1.0.5', [component('Select', apiRecord({ properties: [apiItem('optionLabel', 'string')], }))]), @@ -223,8 +253,8 @@ describe('diffComponent', () => { }) }) - it('does not report description-only changes', () => { - const result = diffComponent( + it('does not report description-only changes', async () => { + const result = await diffSnapshots( snapshot('1.0.5', [component('Button', apiRecord({ properties: [apiItem('disabled', 'boolean', 'false', 'Old description')], }))]), @@ -237,9 +267,9 @@ describe('diffComponent', () => { expect(result.diffs).toEqual([]) }) - it('includes a component that only exists in one snapshot', () => { + it('includes a component that only exists in one snapshot', async () => { const added = apiItem('value', 'string') - const result = diffComponent( + const result = await diffSnapshots( snapshot('1.0.5', []), snapshot('1.5.2', [component('NewComponent', apiRecord({ properties: [added] }))]), 'NewComponent', @@ -253,7 +283,7 @@ describe('diffComponent', () => { }]) }) - it('compares every component when no component is provided', () => { + it('compares every component when no component is provided', async () => { const from = snapshot('1.0.5', [ component('Button', apiRecord({ properties: [apiItem('legacy', 'boolean')] })), component('Input', apiRecord({ properties: [apiItem('value')] })), @@ -264,7 +294,7 @@ describe('diffComponent', () => { component('Select', apiRecord({ properties: [apiItem('options', 'object[]')] })), ]) - const result = diffComponent(from, to) + const result = await diffSnapshots(from, to) expect(result.diffs.map((diff: ComponentDiff) => diff.component)).toEqual([ 'Button', @@ -280,8 +310,8 @@ describe('diffComponent', () => { }) }) - it('includes sub-component scope in API identity', () => { - const result = diffComponent( + it('includes sub-component scope in API identity', async () => { + const result = await diffSnapshots( snapshot('1.0.5', [component('Select', apiRecord(), { Option: apiRecord(), })]), @@ -298,16 +328,19 @@ describe('diffComponent', () => { }) }) - it('throws when a requested component exists in neither snapshot', () => { - expect(() => diffComponent( + it('throws when a requested component exists in neither snapshot', async () => { + await expect(diffSnapshots( snapshot('1.0.5', []), snapshot('1.5.2', []), 'Missing', - )).toThrow('Component Missing not found') + )).rejects.toThrow('Component Missing not found') }) it('throws a clear error when a version major does not exist', async () => { - await expect(loadVersionMetaData({ + const { loadVersionMetaData: actualLoadVersionMetaData } + = await vi.importActual('../src/utils/loader.ts') + + await expect(actualLoadVersionMetaData({ version: '9.0.0', majorVersion: 'v9', }, join(import.meta.dirname, '..', 'data'))).rejects.toThrow('v9.0.0 not found')