Skip to content

Commit f39cfb2

Browse files
authored
Credits page anchor links (#705)
1 parent 2777fe3 commit f39cfb2

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { cachified } from '@epic-web/cachified'
2+
import { expect, test, vi } from 'vitest'
3+
4+
const staleCachedPeople = [
5+
{
6+
name: 'Jane Doe',
7+
id: undefined,
8+
},
9+
]
10+
11+
vi.mock('@epic-web/cachified', () => ({
12+
cachified: vi.fn(async () => staleCachedPeople),
13+
verboseReporter: vi.fn(() => undefined),
14+
}))
15+
16+
vi.mock('../cache.server.ts', () => ({
17+
cache: {
18+
name: 'test-cache',
19+
get: vi.fn(),
20+
set: vi.fn(),
21+
delete: vi.fn(),
22+
},
23+
shouldForceFresh: vi.fn(async () => false),
24+
}))
25+
26+
vi.mock('../github.server.ts', () => ({
27+
downloadFile: vi.fn(async () => ''),
28+
}))
29+
30+
import { getPeople } from '../credits.server.ts'
31+
import { downloadFile } from '../github.server.ts'
32+
33+
test('getPeople normalizes stale cached people with missing id values', async () => {
34+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
35+
try {
36+
vi.mocked(cachified).mockImplementationOnce(async () => staleCachedPeople)
37+
38+
const people = await getPeople({})
39+
expect(people).toHaveLength(1)
40+
expect(people[0]).toMatchObject({
41+
id: 'jane-doe',
42+
name: 'Jane Doe',
43+
})
44+
} finally {
45+
warnSpy.mockRestore()
46+
}
47+
})
48+
49+
test('getPeople checkValue rejects missing ids and revalidates via getFreshValue', async () => {
50+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
51+
try {
52+
vi.mocked(downloadFile).mockResolvedValueOnce('- name: Fresh Person')
53+
vi.mocked(cachified).mockImplementationOnce(async (...args) => {
54+
const options = args[0] as {
55+
checkValue?: (
56+
value: unknown,
57+
migrate: (value: unknown, updateCache?: boolean) => unknown,
58+
) => unknown
59+
getFreshValue: (context: {
60+
metadata: { createdTime: number }
61+
background: boolean
62+
}) => Promise<unknown>
63+
}
64+
expect(options.checkValue?.(staleCachedPeople, () => undefined)).toBe(false)
65+
return options.getFreshValue({
66+
metadata: { createdTime: Date.now() },
67+
background: false,
68+
})
69+
})
70+
71+
const people = await getPeople({})
72+
expect(people).toHaveLength(1)
73+
expect(people[0]).toMatchObject({
74+
id: 'fresh-person',
75+
name: 'Fresh Person',
76+
})
77+
expect(downloadFile).toHaveBeenCalledWith('content/data/credits.yml')
78+
} finally {
79+
warnSpy.mockRestore()
80+
}
81+
})

app/utils/credits.server.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export type Person = {
1616
}
1717

1818
type UnknownObj = Record<string, unknown>
19+
const isUnknownObj = (v: unknown): v is UnknownObj =>
20+
typeof v === 'object' && v !== null
21+
1922
function getValueWithFallback<PropertyType>(
2023
obj: UnknownObj,
2124
key: string,
@@ -43,6 +46,12 @@ function getValueWithFallback<PropertyType>(
4346
}
4447

4548
const isString = (v: unknown): v is string => typeof v === 'string'
49+
const hasStringId = (v: unknown): v is UnknownObj & { id: string } =>
50+
isUnknownObj(v) && isString(v.id)
51+
52+
function normalizePeople(people: ReadonlyArray<unknown>) {
53+
return people.filter(isUnknownObj).map(mapPerson).filter(typedBoolean)
54+
}
4655

4756
function mapPerson(rawPerson: UnknownObj) {
4857
try {
@@ -145,13 +154,17 @@ async function getPeople({
145154
throw new Error('Credits is not an array.')
146155
}
147156

148-
return rawCredits.map(mapPerson).filter(typedBoolean)
157+
return normalizePeople(rawCredits)
149158
},
150-
checkValue: (value: unknown) => Array.isArray(value),
159+
checkValue: (value: unknown) =>
160+
Array.isArray(value) && value.every(hasStringId),
151161
},
152162
verboseReporter(),
153163
)
154-
return allPeople
164+
// We normalize after `cachified` too because `checkValue` can reject stale data,
165+
// `getFreshValue` can fail (for example if GitHub is unavailable), and
166+
// `cachified` may still return fallbackToCache data under forceFresh semantics.
167+
return normalizePeople(allPeople)
155168
}
156169

157170
export { getPeople }

0 commit comments

Comments
 (0)