Skip to content

Commit 2799607

Browse files
committed
refactor: move sorting lambda to utils and add unit tests
1 parent 69dcd00 commit 2799607

3 files changed

Lines changed: 140 additions & 15 deletions

File tree

app/pages/package/[[org]]/[name]/versions.vue

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { compare, validRange } from 'semver'
55
import {
66
buildVersionToTagsMap,
77
buildTaggedVersionRows,
8+
compareTagRows,
9+
compareVersionGroupKeys,
810
filterVersions,
911
getVersionGroupKey,
1012
getVersionGroupLabel,
11-
getTagPriority,
1213
} from '~/utils/versions'
1314
import { fetchAllPackageVersions } from '~/utils/npm/api'
1415
@@ -71,14 +72,7 @@ const latestTagRow = computed(() => tagRows.value.find(r => r.tags.includes('lat
7172
const otherTagRows = computed(() =>
7273
tagRows.value
7374
.filter(r => !r.tags.includes('latest'))
74-
.sort((rowA, rowB) => {
75-
const priorityA = Math.min(...rowA.tags.map(getTagPriority))
76-
const priorityB = Math.min(...rowB.tags.map(getTagPriority))
77-
if (priorityA !== priorityB) return priorityA - priorityB
78-
const timeA = versionTimes.value[rowA.version] ?? ''
79-
const timeB = versionTimes.value[rowB.version] ?? ''
80-
return timeB.localeCompare(timeA)
81-
}),
75+
.sort((rowA, rowB) => compareTagRows(rowA, rowB, versionTimes.value)),
8276
)
8377
8478
function getVersionTime(version: string): string | undefined {
@@ -99,12 +93,7 @@ const versionGroups = computed(() => {
9993
}
10094
10195
return Array.from(byKey.keys())
102-
.sort((a, b) => {
103-
const [aMajor, aMinor] = a.split('.').map(Number)
104-
const [bMajor, bMinor] = b.split('.').map(Number)
105-
if (aMajor !== bMajor) return (bMajor ?? 0) - (aMajor ?? 0)
106-
return (bMinor ?? -1) - (aMinor ?? -1)
107-
})
96+
.sort(compareVersionGroupKeys)
10897
.map(groupKey => ({
10998
groupKey,
11099
label: getVersionGroupLabel(groupKey),

app/utils/versions.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,41 @@ export function getTagPriority(tag: string | undefined): number {
8383
return Infinity
8484
}
8585

86+
/**
87+
* Compare two tagged version rows for display ordering.
88+
* Sorts by minimum tag priority first; falls back to publish date descending.
89+
* @param rowA - First row
90+
* @param rowB - Second row
91+
* @param versionTimes - Map of version string to ISO publish time
92+
* @returns Negative/zero/positive comparator value
93+
*/
94+
export function compareTagRows(
95+
rowA: TaggedVersionRow,
96+
rowB: TaggedVersionRow,
97+
versionTimes: Record<string, string>,
98+
): number {
99+
const priorityA = Math.min(...rowA.tags.map(getTagPriority))
100+
const priorityB = Math.min(...rowB.tags.map(getTagPriority))
101+
if (priorityA !== priorityB) return priorityA - priorityB
102+
const timeA = versionTimes[rowA.version] ?? ''
103+
const timeB = versionTimes[rowB.version] ?? ''
104+
return timeB.localeCompare(timeA)
105+
}
106+
107+
/**
108+
* Compare two version group keys for display ordering.
109+
* Sorts by major descending, then by minor descending for 0.x groups.
110+
* @param a - Group key (e.g. "1", "0.9")
111+
* @param b - Group key (e.g. "2", "0.10")
112+
* @returns Negative/zero/positive comparator value
113+
*/
114+
export function compareVersionGroupKeys(a: string, b: string): number {
115+
const [majorA, minorA] = a.split('.').map(Number)
116+
const [majorB, minorB] = b.split('.').map(Number)
117+
if (majorA !== majorB) return (majorB ?? 0) - (majorA ?? 0)
118+
return (minorB ?? -1) - (minorA ?? -1)
119+
}
120+
86121
/**
87122
* Sort tags with 'latest' first, then alphabetically
88123
* @param tags - Array of tag names

test/unit/app/utils/versions.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'
22
import {
33
buildTaggedVersionRows,
44
buildVersionToTagsMap,
5+
compareTagRows,
6+
compareVersionGroupKeys,
57
filterExcludedTags,
68
filterVersions,
79
getPrereleaseChannel,
@@ -423,6 +425,105 @@ describe('isSameVersionGroup', () => {
423425
})
424426
})
425427

428+
describe('compareTagRows', () => {
429+
function row(version: string, tags: string[]) {
430+
return { id: `version:${version}`, primaryTag: tags[0]!, tags, version }
431+
}
432+
433+
it('sorts by tag priority ascending (rc before beta)', () => {
434+
const rc = row('2.0.0-rc.1', ['rc'])
435+
const beta = row('2.0.0-beta.1', ['beta'])
436+
expect(compareTagRows(rc, beta, {})).toBeLessThan(0)
437+
expect(compareTagRows(beta, rc, {})).toBeGreaterThan(0)
438+
})
439+
440+
it('sorts by tag priority ascending (beta before alpha)', () => {
441+
const beta = row('2.0.0-beta.1', ['beta'])
442+
const alpha = row('2.0.0-alpha.1', ['alpha'])
443+
expect(compareTagRows(beta, alpha, {})).toBeLessThan(0)
444+
})
445+
446+
it('falls back to publish date descending when priorities are equal', () => {
447+
const newer = row('1.1.0', ['legacy'])
448+
const older = row('1.0.0', ['legacy'])
449+
const times = { '1.1.0': '2024-06-01T00:00:00.000Z', '1.0.0': '2024-01-01T00:00:00.000Z' }
450+
expect(compareTagRows(newer, older, times)).toBeLessThan(0)
451+
expect(compareTagRows(older, newer, times)).toBeGreaterThan(0)
452+
})
453+
454+
it('returns 0 for equal priority and equal publish time', () => {
455+
const a = row('1.0.0', ['legacy'])
456+
const b = row('1.0.1', ['legacy'])
457+
const times = { '1.0.0': '2024-01-01T00:00:00.000Z', '1.0.1': '2024-01-01T00:00:00.000Z' }
458+
expect(compareTagRows(a, b, times)).toBe(0)
459+
})
460+
461+
it('uses minimum tag priority for multi-tag rows', () => {
462+
// Row with ['rc', 'next'] has min priority of rc (2)
463+
// Row with ['beta'] has priority 3 — so rc-row should sort first
464+
const rcAndNext = row('3.0.0-rc.1', ['rc', 'next'])
465+
const beta = row('3.0.0-beta.1', ['beta'])
466+
expect(compareTagRows(rcAndNext, beta, {})).toBeLessThan(0)
467+
})
468+
469+
it('sorts unknown tags after known priority tags', () => {
470+
const known = row('2.0.0-alpha.1', ['alpha'])
471+
const unknown = row('2.0.0-custom.1', ['custom-tag'])
472+
expect(compareTagRows(known, unknown, {})).toBeLessThan(0)
473+
})
474+
475+
it('sorts unknown tags by publish date descending', () => {
476+
const newer = row('2.0.0', ['v2-custom'])
477+
const older = row('1.0.0', ['v1-custom'])
478+
const times = { '2.0.0': '2025-01-01T00:00:00.000Z', '1.0.0': '2024-01-01T00:00:00.000Z' }
479+
expect(compareTagRows(newer, older, times)).toBeLessThan(0)
480+
})
481+
482+
it('treats missing publish time as empty string (sorts last among same-priority rows)', () => {
483+
const withTime = row('1.1.0', ['legacy'])
484+
const withoutTime = row('1.0.0', ['legacy'])
485+
const times = { '1.1.0': '2024-06-01T00:00:00.000Z' }
486+
expect(compareTagRows(withTime, withoutTime, times)).toBeLessThan(0)
487+
})
488+
})
489+
490+
describe('compareVersionGroupKeys', () => {
491+
it('sorts higher major before lower major', () => {
492+
expect(compareVersionGroupKeys('2', '1')).toBeLessThan(0)
493+
expect(compareVersionGroupKeys('1', '2')).toBeGreaterThan(0)
494+
})
495+
496+
it('returns 0 for equal keys', () => {
497+
expect(compareVersionGroupKeys('3', '3')).toBe(0)
498+
expect(compareVersionGroupKeys('0.9', '0.9')).toBe(0)
499+
})
500+
501+
it('sorts higher minor before lower minor for 0.x groups', () => {
502+
expect(compareVersionGroupKeys('0.10', '0.9')).toBeLessThan(0)
503+
expect(compareVersionGroupKeys('0.9', '0.10')).toBeGreaterThan(0)
504+
})
505+
506+
it('sorts non-0.x keys (no minor) before 0.x keys with same major', () => {
507+
// major-only key "0" has no minor (undefined → -1), so "0.1" sorts before "0"
508+
expect(compareVersionGroupKeys('0.1', '0')).toBeLessThan(0)
509+
})
510+
511+
it('sorts major-version groups in descending order when used with Array.sort', () => {
512+
const keys = ['1', '3', '2', '10']
513+
expect(keys.sort(compareVersionGroupKeys)).toEqual(['10', '3', '2', '1'])
514+
})
515+
516+
it('sorts 0.x groups in descending minor order when used with Array.sort', () => {
517+
const keys = ['0.1', '0.10', '0.9', '0.2']
518+
expect(keys.sort(compareVersionGroupKeys)).toEqual(['0.10', '0.9', '0.2', '0.1'])
519+
})
520+
521+
it('interleaves major and 0.x groups correctly', () => {
522+
const keys = ['0.9', '1', '0.10', '2']
523+
expect(keys.sort(compareVersionGroupKeys)).toEqual(['2', '1', '0.10', '0.9'])
524+
})
525+
})
526+
426527
describe('filterVersions', () => {
427528
const versions = ['1.0.0', '1.1.0', '1.5.3', '2.0.0', '2.1.0', '3.0.0-beta.1']
428529

0 commit comments

Comments
 (0)