Skip to content

Commit 42c67dc

Browse files
committed
test(cover): add coverage for cover/formatters and cover/type
cover/formatters.test.mts: 17 tests covering every emoji threshold, default/json/simple format paths, NaN handling, type-coverage in overall average. cover/type.test.mts: 10 tests covering successful parse, unparseable output, spawn-failure null/throw paths, cwd validation, default process.cwd(). spawn is mocked at the package specifier.
1 parent 76dd39c commit 42c67dc

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* @fileoverview Unit tests for coverage output formatters.
3+
*
4+
* Covers `formatCoverage` (default/simple/json formats) and
5+
* `getCoverageEmoji` (every threshold band).
6+
*/
7+
8+
import { describe, expect, it } from 'vitest'
9+
10+
import {
11+
formatCoverage,
12+
getCoverageEmoji,
13+
} from '@socketsecurity/lib/cover/formatters'
14+
15+
const sampleCode = {
16+
statements: { covered: 85, total: 100, percent: '85.00' },
17+
branches: { covered: 80, total: 100, percent: '80.00' },
18+
functions: { covered: 90, total: 100, percent: '90.00' },
19+
lines: { covered: 88, total: 100, percent: '88.00' },
20+
}
21+
22+
describe('cover/formatters', () => {
23+
describe('getCoverageEmoji', () => {
24+
it('returns rocket for >= 99%', () => {
25+
expect(getCoverageEmoji(99)).toBe(' 🚀')
26+
expect(getCoverageEmoji(100)).toBe(' 🚀')
27+
})
28+
29+
it('returns target for >= 95%', () => {
30+
expect(getCoverageEmoji(95)).toBe(' 🎯')
31+
expect(getCoverageEmoji(98.99)).toBe(' 🎯')
32+
})
33+
34+
it('returns sparkles for >= 90%', () => {
35+
expect(getCoverageEmoji(90)).toBe(' ✨')
36+
expect(getCoverageEmoji(94.99)).toBe(' ✨')
37+
})
38+
39+
it('returns heart for >= 85%', () => {
40+
expect(getCoverageEmoji(85)).toBe(' 💚')
41+
expect(getCoverageEmoji(89.99)).toBe(' 💚')
42+
})
43+
44+
it('returns check for >= 80%', () => {
45+
expect(getCoverageEmoji(80)).toBe(' ✅')
46+
})
47+
48+
it('returns green-circle for >= 70%', () => {
49+
expect(getCoverageEmoji(70)).toBe(' 🟢')
50+
})
51+
52+
it('returns yellow-circle for >= 60%', () => {
53+
expect(getCoverageEmoji(60)).toBe(' 🟡')
54+
})
55+
56+
it('returns hammer for >= 50%', () => {
57+
expect(getCoverageEmoji(50)).toBe(' 🔨')
58+
})
59+
60+
it('returns warning for < 50%', () => {
61+
expect(getCoverageEmoji(49)).toBe(' ⚠️')
62+
expect(getCoverageEmoji(0)).toBe(' ⚠️')
63+
})
64+
65+
it('handles negative percentages by returning empty string', () => {
66+
// No threshold matches; .find returns undefined → '' fallback.
67+
expect(getCoverageEmoji(-1)).toBe('')
68+
})
69+
})
70+
71+
describe('formatCoverage', () => {
72+
it('produces a default human-readable report', () => {
73+
const out = formatCoverage({ code: sampleCode })
74+
expect(out).toContain('Code Coverage:')
75+
expect(out).toContain('Statements: 85.00%')
76+
expect(out).toContain('Branches: 80.00%')
77+
expect(out).toContain('Functions: 90.00%')
78+
expect(out).toContain('Lines: 88.00%')
79+
expect(out).toContain('Overall: 85.75%')
80+
})
81+
82+
it('returns JSON when format=json', () => {
83+
const out = formatCoverage({ code: sampleCode, format: 'json' })
84+
const parsed = JSON.parse(out)
85+
expect(parsed.code).toEqual(sampleCode)
86+
})
87+
88+
it('returns just the overall percent when format=simple', () => {
89+
const out = formatCoverage({ code: sampleCode, format: 'simple' })
90+
expect(out).toBe('85.75')
91+
})
92+
93+
it('includes type coverage section when present', () => {
94+
const out = formatCoverage({
95+
code: sampleCode,
96+
type: { covered: 1500, total: 2000, percent: '75.00' },
97+
})
98+
expect(out).toContain('Type Coverage:')
99+
expect(out).toContain('75.00% (1500/2000)')
100+
})
101+
102+
it('includes type in overall average when present', () => {
103+
const out = formatCoverage({
104+
code: sampleCode,
105+
type: { covered: 5, total: 10, percent: '50.00' },
106+
format: 'simple',
107+
})
108+
// average of (85, 80, 90, 88, 50) = 78.6
109+
expect(out).toBe('78.60')
110+
})
111+
112+
it('handles NaN percent values as 0', () => {
113+
const broken = {
114+
statements: { covered: 0, total: 0, percent: 'NaN' },
115+
branches: { covered: 0, total: 0, percent: 'NaN' },
116+
functions: { covered: 0, total: 0, percent: 'NaN' },
117+
lines: { covered: 0, total: 0, percent: 'NaN' },
118+
}
119+
const out = formatCoverage({ code: broken, format: 'simple' })
120+
expect(out).toBe('0.00')
121+
})
122+
123+
it('appends an emoji to the overall line in default format', () => {
124+
const out = formatCoverage({ code: sampleCode })
125+
// 85.75% overall → 💚 band.
126+
expect(out).toContain('💚')
127+
})
128+
})
129+
})

test/unit/cover/type.test.mts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @fileoverview Unit tests for TypeScript type-coverage helper.
3+
*
4+
* Mocks `spawn` from `../../src/spawn` so tests don't need the
5+
* `type-coverage` binary installed. Covers:
6+
* - Successful parse of "covered / total percent%" output
7+
* - null on unparseable output (no match, partial match)
8+
* - null on spawn failure when generateIfMissing=false (default)
9+
* - Throws when generateIfMissing=true and spawn fails
10+
* - Throws when cwd is empty string (validation guard)
11+
*/
12+
13+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
14+
15+
// Mock spawn at the package specifier — src resolution is enabled in
16+
// vitest.config.mts via the 'source' export condition.
17+
vi.mock('@socketsecurity/lib/spawn')
18+
19+
import { spawn } from '@socketsecurity/lib/spawn'
20+
import { getTypeCoverage } from '@socketsecurity/lib/cover/type'
21+
22+
describe('cover/type', () => {
23+
beforeEach(() => {
24+
vi.mocked(spawn).mockReset()
25+
})
26+
27+
afterEach(() => {
28+
vi.restoreAllMocks()
29+
})
30+
31+
describe('getTypeCoverage', () => {
32+
it('parses successful spawn output', async () => {
33+
vi.mocked(spawn).mockResolvedValueOnce({
34+
code: 0,
35+
signal: null,
36+
stdout: Buffer.from('1234 / 5678 21.74%'),
37+
stderr: Buffer.from(''),
38+
} as any)
39+
const result = await getTypeCoverage({ cwd: '/some/path' })
40+
expect(result).toEqual({ covered: 1234, total: 5678, percent: '21.74' })
41+
})
42+
43+
it('parses output with leading whitespace and other text', async () => {
44+
vi.mocked(spawn).mockResolvedValueOnce({
45+
code: 0,
46+
signal: null,
47+
stdout: Buffer.from('some files\n 500 / 1000 50.00%\nsome more text'),
48+
stderr: Buffer.from(''),
49+
} as any)
50+
const result = await getTypeCoverage({ cwd: '/some/path' })
51+
expect(result).toEqual({ covered: 500, total: 1000, percent: '50.00' })
52+
})
53+
54+
it('returns null when output is unparseable', async () => {
55+
vi.mocked(spawn).mockResolvedValueOnce({
56+
code: 0,
57+
signal: null,
58+
stdout: Buffer.from('no metrics here'),
59+
stderr: Buffer.from(''),
60+
} as any)
61+
const result = await getTypeCoverage({ cwd: '/some/path' })
62+
expect(result).toBeNull()
63+
})
64+
65+
it('returns null when spawn output is empty', async () => {
66+
vi.mocked(spawn).mockResolvedValueOnce({
67+
code: 0,
68+
signal: null,
69+
stdout: undefined,
70+
stderr: Buffer.from(''),
71+
} as any)
72+
const result = await getTypeCoverage({ cwd: '/some/path' })
73+
expect(result).toBeNull()
74+
})
75+
76+
it('returns null when spawn rejects and generateIfMissing=false', async () => {
77+
vi.mocked(spawn).mockRejectedValueOnce(new Error('command not found'))
78+
const result = await getTypeCoverage({
79+
cwd: '/some/path',
80+
generateIfMissing: false,
81+
})
82+
expect(result).toBeNull()
83+
})
84+
85+
it('returns null when spawn rejects and generateIfMissing is omitted (default false)', async () => {
86+
vi.mocked(spawn).mockRejectedValueOnce(new Error('not installed'))
87+
const result = await getTypeCoverage({ cwd: '/some/path' })
88+
expect(result).toBeNull()
89+
})
90+
91+
it('throws when spawn rejects and generateIfMissing=true', async () => {
92+
vi.mocked(spawn).mockRejectedValueOnce(new Error('not installed'))
93+
await expect(
94+
getTypeCoverage({ cwd: '/some/path', generateIfMissing: true }),
95+
).rejects.toThrow(/Unable to generate type coverage/)
96+
})
97+
98+
it('throws when cwd is empty string', async () => {
99+
await expect(getTypeCoverage({ cwd: '' })).rejects.toThrow(
100+
/Working directory is required/,
101+
)
102+
})
103+
104+
it('uses process.cwd() when no cwd provided', async () => {
105+
vi.mocked(spawn).mockResolvedValueOnce({
106+
code: 0,
107+
signal: null,
108+
stdout: Buffer.from('1 / 1 100.00%'),
109+
stderr: Buffer.from(''),
110+
} as any)
111+
// Should resolve cleanly without throwing the cwd-required guard.
112+
const result = await getTypeCoverage()
113+
expect(result).toEqual({ covered: 1, total: 1, percent: '100.00' })
114+
})
115+
116+
it('passes --detail to type-coverage', async () => {
117+
vi.mocked(spawn).mockResolvedValueOnce({
118+
code: 0,
119+
signal: null,
120+
stdout: Buffer.from('100 / 200 50.00%'),
121+
stderr: Buffer.from(''),
122+
} as any)
123+
await getTypeCoverage({ cwd: '/x' })
124+
expect(vi.mocked(spawn).mock.calls[0]?.[0]).toBe('type-coverage')
125+
expect(vi.mocked(spawn).mock.calls[0]?.[1]).toEqual(['--detail'])
126+
})
127+
})
128+
})

0 commit comments

Comments
 (0)