Skip to content

Commit 1d94720

Browse files
committed
test(links): cover src/links/create.ts, which dist-based tests left at zero
Running coverage exposed a hole my own test created. create.test.mts imports from `dist` deliberately, because the bug this feature kept hitting lived in the bundle — but dist is a different file, so `src/links/create.ts` scored 0%. The suite was green and the source was uninstrumented. Fixed by mocking `src/external/external-pack`, the choke point: create.ts imports BOTH terminal-link and yoctocolors-cjs, and each re-exports from the pack, so mocking only terminal-link still pulled the raw ESM packages and threw "hasFlag is not a function". That is also why no source test for this file existed before — the pack made it unimportable, rather than anyone overlooking it. The two files now divide the work instead of overlapping: this one pins hyperlink's own logic, and the dist one proves the bundle loads and emits. The logic worth pinning is the fallback translation. `fallback: false` has to become a FUNCTION returning its input, not the boolean — terminal-link reads a falsy boolean as "use my default", so passing `false` through would still append the URL and silently break a gate's copy-pasteable lane A. Also covers link() and links() while the mock makes them reachable. They predate hyperlink and had no source test either. One correction landed here: there is no 'default' theme, so the by-name lookup is pinned against 'socket'. src/links/create.ts: 0% -> 93.33% lines, 100% functions. Line 107 is an unreached else in a colour branch. 33 tests across the links suite.
1 parent ecd5301 commit 1d94720

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @file Source-level unit tests for `hyperlink`, covering `src/links/create.ts`
3+
* itself.
4+
* Its sibling `create.test.mts` imports from `dist` on purpose, because the
5+
* bug this feature kept hitting lived in the BUNDLE. That has a cost: dist is
6+
* a different file, so `src/links/create.ts` scored zero coverage from it.
7+
* This file closes that hole by mocking the bundled external, which is also
8+
* what makes importing src possible at all — unmocked,
9+
* `src/external/terminal-link` reaches the raw ESM packages and throws
10+
* "hasFlag is not a function", since the stub swap only happens at bundle
11+
* time.
12+
* So the two files divide the work rather than duplicating it: this one pins
13+
* `hyperlink`'s own logic, specifically how it translates the `fallback`
14+
* option into terminal-link's callback protocol, and the dist file proves the
15+
* bundle actually loads and emits.
16+
*/
17+
18+
import { beforeEach, describe, expect, it, vi } from 'vitest'
19+
20+
const terminalLinkMock = vi.fn(
21+
(text: string, _url: string, _options?: unknown | undefined) => text,
22+
)
23+
24+
// external-pack is the choke point: create.ts imports BOTH terminal-link and
25+
// yoctocolors-cjs, and each re-exports from it, so mocking only terminal-link
26+
// still pulled the raw ESM packages and threw. Mocking the pack covers both.
27+
vi.mock(import('../../../src/external/external-pack'), () => ({
28+
terminalLink: terminalLinkMock,
29+
yoctocolorsCjs: new Proxy({}, { get: () => (text: string) => text }),
30+
}))
31+
32+
vi.mock(import('../../../src/external/terminal-link'), () => ({
33+
default: terminalLinkMock,
34+
}))
35+
36+
vi.mock(import('../../../src/external/yoctocolors-cjs'), () => ({
37+
default: new Proxy({}, { get: () => (text: string) => text }),
38+
}))
39+
40+
const URL = 'https://socket.dev'
41+
42+
beforeEach(() => {
43+
terminalLinkMock.mockClear()
44+
})
45+
46+
describe('links/create — hyperlink (source)', () => {
47+
it('delegates to terminal-link with the text and url unchanged', async () => {
48+
const { hyperlink } = await import('../../../src/links/create')
49+
hyperlink('Docs', URL)
50+
expect(terminalLinkMock).toHaveBeenCalledTimes(1)
51+
const [text, url] = terminalLinkMock.mock.calls[0]!
52+
expect(text).toBe('Docs')
53+
expect(url).toBe(URL)
54+
})
55+
56+
it('leaves fallback undefined by default so terminal-link appends the url', async () => {
57+
// terminal-link's own default renders `text (url)`. Passing undefined keeps
58+
// it, which is what keeps a destination reachable on a plain terminal.
59+
const { hyperlink } = await import('../../../src/links/create')
60+
hyperlink('Docs', URL)
61+
const options = terminalLinkMock.mock.calls[0]![2] as {
62+
fallback?: unknown | undefined
63+
}
64+
expect(options.fallback).toBeUndefined()
65+
})
66+
67+
it('passes an identity callback when fallback is disabled', async () => {
68+
// `fallback: false` has to become a FUNCTION returning its input, not the
69+
// boolean — terminal-link treats a falsy boolean as "use my default", so
70+
// passing `false` straight through would still append the url and silently
71+
// break a gate's copy-pasteable lane A.
72+
const { hyperlink } = await import('../../../src/links/create')
73+
hyperlink('Allow push to main', URL, { fallback: false })
74+
const options = terminalLinkMock.mock.calls[0]![2] as {
75+
fallback?: ((text: string, url: string) => string) | undefined
76+
}
77+
expect(typeof options.fallback).toBe('function')
78+
expect(options.fallback!('Allow push to main', URL)).toBe(
79+
'Allow push to main',
80+
)
81+
})
82+
83+
it('treats an explicit fallback: true the same as the default', async () => {
84+
const { hyperlink } = await import('../../../src/links/create')
85+
hyperlink('Docs', URL, { fallback: true })
86+
const options = terminalLinkMock.mock.calls[0]![2] as {
87+
fallback?: unknown | undefined
88+
}
89+
expect(options.fallback).toBeUndefined()
90+
})
91+
92+
it('returns whatever terminal-link produced, untouched', async () => {
93+
terminalLinkMock.mockReturnValueOnce('WRAPPED')
94+
const { hyperlink } = await import('../../../src/links/create')
95+
expect(hyperlink('Docs', URL)).toBe('WRAPPED')
96+
})
97+
})
98+
99+
describe('links/create — link and links (source)', () => {
100+
// These predate hyperlink and had no source test. They are reachable now
101+
// that external-pack is mockable, so cover them rather than leave the file
102+
// half-instrumented.
103+
104+
it('colors the text and returns it', async () => {
105+
const { link } = await import('../../../src/links/create')
106+
expect(link('Docs', URL)).toBe('Docs')
107+
})
108+
109+
it('appends the url when fallback is requested', async () => {
110+
const { link } = await import('../../../src/links/create')
111+
expect(link('Docs', URL, { fallback: true })).toBe(`Docs (${URL})`)
112+
})
113+
114+
it('accepts a theme by name', async () => {
115+
// The string branch indexes THEMES directly, so an unknown name yields
116+
// undefined and throws on theme!.colors. This pins the lookup against a
117+
// real theme rather than assuming a "default" key exists — there is none.
118+
const { link } = await import('../../../src/links/create')
119+
expect(link('Docs', URL, { theme: 'socket' })).toBe('Docs')
120+
})
121+
122+
it('falls back to cyan when the theme link color is an RGB array', async () => {
123+
// The ArrayIsArray branch: RGB is not implemented yet and routes to cyan.
124+
// Mocked colors are identity, so the assertion is that it returns rather
125+
// than throwing on a non-string color.
126+
const { link } = await import('../../../src/links/create')
127+
expect(
128+
link('Docs', URL, {
129+
theme: { colors: { link: [255, 0, 0] } },
130+
} as unknown as Parameters<typeof link>[2]),
131+
).toBe('Docs')
132+
})
133+
134+
it('maps an array of specs through link', async () => {
135+
const { links } = await import('../../../src/links/create')
136+
expect(
137+
links([
138+
['Docs', URL],
139+
['API', 'https://api.socket.dev'],
140+
]),
141+
).toEqual(['Docs', 'API'])
142+
})
143+
144+
it('threads options through to every spec', async () => {
145+
const { links } = await import('../../../src/links/create')
146+
expect(links([['Docs', URL]], { fallback: true })).toEqual([
147+
`Docs (${URL})`,
148+
])
149+
})
150+
})

0 commit comments

Comments
 (0)