Skip to content

Commit ec87bdb

Browse files
committed
test: raise coverage to 70.67% (target: 95)
Adds 6 test files covering BlogPage, NotFoundPage, PrivacyPage, TermsPage, UseCasesPage, and the SSE-over-fetch streamer (src/lib/sseStream.ts). Coverage: 67.54% → 70.67% (src-only: ~76%). 33 new tests, all passing. Test count: 736 → 769. Remaining gap: src/pages/{SettingsPage,DocsPage,LoginPage,CheckoutPage, ContractsPage,AdminCustomersPage}.tsx are large (200-635 lines each) and authenticated; full coverage requires more API mock plumbing than the budget allowed. src/App.tsx + entry-server.tsx + scripts/prerender.mjs are bootstrap/SSG paths exercised only by the e2e Playwright suite — not addressable via vitest unit tests. Root-level files (playwright.config.ts, e2e/fixtures.ts) count as 0% in the coverage table but are runtime-irrelevant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9a30c8 commit ec87bdb

6 files changed

Lines changed: 481 additions & 0 deletions

File tree

src/lib/sseStream.test.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/* sseStream.test.ts — coverage for the SSE-over-fetch consumer.
2+
*
3+
* Tests the parser by stubbing global.fetch with a ReadableStream of
4+
* encoded UTF-8 chunks, then asserting each onLine + onError + onClose
5+
* branch in turn.
6+
*/
7+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
8+
import { streamSSE, SSEStreamError } from './sseStream'
9+
import { setToken, clearToken } from '../api'
10+
11+
function makeStream(chunks: string[]): ReadableStream<Uint8Array> {
12+
const enc = new TextEncoder()
13+
let i = 0
14+
return new ReadableStream<Uint8Array>({
15+
pull(controller) {
16+
if (i >= chunks.length) {
17+
controller.close()
18+
return
19+
}
20+
controller.enqueue(enc.encode(chunks[i++]))
21+
},
22+
})
23+
}
24+
25+
function flush(): Promise<void> {
26+
return new Promise((r) => setTimeout(r, 0))
27+
}
28+
29+
describe('SSEStreamError', () => {
30+
it('carries the HTTP status', () => {
31+
const e = new SSEStreamError(404)
32+
expect(e.status).toBe(404)
33+
expect(e.name).toBe('SSEStreamError')
34+
expect(e.message).toBe('HTTP 404')
35+
})
36+
})
37+
38+
describe('streamSSE', () => {
39+
beforeEach(() => {
40+
clearToken()
41+
})
42+
afterEach(() => {
43+
vi.restoreAllMocks()
44+
})
45+
46+
it('emits onLine for each `data: <payload>` SSE line and calls onClose at end', async () => {
47+
const body = makeStream(['data: hello\n', 'data: world\n'])
48+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body })
49+
;(globalThis as any).fetch = fetchMock
50+
51+
const lines: string[] = []
52+
const closed = new Promise<void>((resolve) => {
53+
streamSSE('/test', {
54+
onLine: (l) => lines.push(l),
55+
onClose: resolve,
56+
})
57+
})
58+
await closed
59+
expect(lines).toEqual(['hello', 'world'])
60+
expect(fetchMock).toHaveBeenCalledTimes(1)
61+
})
62+
63+
it('strips only ONE space after `data:` (spec-compliant)', async () => {
64+
const body = makeStream(['data:no-space\n', 'data: two-spaces\n'])
65+
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, body })
66+
67+
const lines: string[] = []
68+
await new Promise<void>((resolve) => {
69+
streamSSE('/x', { onLine: (l) => lines.push(l), onClose: resolve })
70+
})
71+
expect(lines).toEqual(['no-space', ' two-spaces'])
72+
})
73+
74+
it('ignores non-data lines (event:, id:, blank)', async () => {
75+
const body = makeStream([
76+
'event: ping\n',
77+
': comment\n',
78+
'\n',
79+
'data: keeper\n',
80+
])
81+
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, body })
82+
83+
const lines: string[] = []
84+
await new Promise<void>((resolve) => {
85+
streamSSE('/x', { onLine: (l) => lines.push(l), onClose: resolve })
86+
})
87+
expect(lines).toEqual(['keeper'])
88+
})
89+
90+
it('on non-OK response, calls onError with SSEStreamError + onClose', async () => {
91+
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, body: null })
92+
93+
let err: unknown = null
94+
let closed = false
95+
await new Promise<void>((resolve) => {
96+
streamSSE('/bad', {
97+
onLine: () => {},
98+
onError: (e) => { err = e },
99+
onClose: () => { closed = true; resolve() },
100+
})
101+
})
102+
expect(err).toBeInstanceOf(SSEStreamError)
103+
expect((err as SSEStreamError).status).toBe(503)
104+
expect(closed).toBe(true)
105+
})
106+
107+
it('on 401 mid-open, still surfaces SSEStreamError(401)', async () => {
108+
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: false, status: 401, body: null })
109+
110+
let err: unknown = null
111+
await new Promise<void>((resolve) => {
112+
streamSSE('/auth', {
113+
onLine: () => {},
114+
onError: (e) => { err = e },
115+
onClose: resolve,
116+
})
117+
})
118+
expect((err as SSEStreamError).status).toBe(401)
119+
})
120+
121+
it('on thrown fetch (e.g. network error), calls onError + onClose', async () => {
122+
;(globalThis as any).fetch = vi.fn().mockRejectedValue(new Error('network down'))
123+
124+
let err: unknown = null
125+
let closed = false
126+
await new Promise<void>((resolve) => {
127+
streamSSE('/x', {
128+
onLine: () => {},
129+
onError: (e) => { err = e },
130+
onClose: () => { closed = true; resolve() },
131+
})
132+
})
133+
expect((err as Error).message).toBe('network down')
134+
expect(closed).toBe(true)
135+
})
136+
137+
it('suppresses AbortError so the caller does not see spurious errors', async () => {
138+
const ab = new Error('abort')
139+
ab.name = 'AbortError'
140+
;(globalThis as any).fetch = vi.fn().mockRejectedValue(ab)
141+
142+
const errors: unknown[] = []
143+
let closed = false
144+
await new Promise<void>((resolve) => {
145+
streamSSE('/x', {
146+
onLine: () => {},
147+
onError: (e) => errors.push(e),
148+
onClose: () => { closed = true; resolve() },
149+
})
150+
})
151+
expect(errors).toEqual([])
152+
expect(closed).toBe(true)
153+
})
154+
155+
it('attaches Authorization header when a token is set', async () => {
156+
setToken('test-bearer-123')
157+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body: makeStream([]) })
158+
;(globalThis as any).fetch = fetchMock
159+
160+
await new Promise<void>((resolve) => {
161+
streamSSE('/x', { onLine: () => {}, onClose: resolve })
162+
})
163+
const opts = fetchMock.mock.calls[0][1]
164+
expect(opts.headers.Authorization).toBe('Bearer test-bearer-123')
165+
expect(opts.headers.Accept).toBe('text/event-stream')
166+
clearToken()
167+
})
168+
169+
it('does NOT attach Authorization header when no token is set', async () => {
170+
clearToken()
171+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body: makeStream([]) })
172+
;(globalThis as any).fetch = fetchMock
173+
174+
await new Promise<void>((resolve) => {
175+
streamSSE('/x', { onLine: () => {}, onClose: resolve })
176+
})
177+
const opts = fetchMock.mock.calls[0][1]
178+
expect(opts.headers.Authorization).toBeUndefined()
179+
})
180+
181+
it('returns a cleanup function that aborts the AbortController', async () => {
182+
// No need to await — just verify the callable contract.
183+
;(globalThis as any).fetch = vi.fn(() => new Promise(() => {}))
184+
const cleanup = streamSSE('/x', { onLine: () => {} })
185+
expect(typeof cleanup).toBe('function')
186+
cleanup() // does not throw
187+
await flush()
188+
})
189+
190+
it('honors an external AbortSignal — aborting it aborts the compound', async () => {
191+
;(globalThis as any).fetch = vi.fn(() => new Promise(() => {}))
192+
const ctl = new AbortController()
193+
const cleanup = streamSSE('/x', { onLine: () => {} }, ctl.signal)
194+
expect(typeof cleanup).toBe('function')
195+
ctl.abort()
196+
// Just verify no throw + cleanup still callable.
197+
cleanup()
198+
await flush()
199+
})
200+
})

src/pages/BlogPage.test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* BlogPage.test.tsx — coverage for the public blog index. */
2+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3+
import { render, cleanup } from '@testing-library/react'
4+
import { MemoryRouter } from 'react-router-dom'
5+
import { BlogPage } from './BlogPage'
6+
import { POSTS } from '../content/posts'
7+
8+
let originalTitle = ''
9+
beforeEach(() => {
10+
originalTitle = document.title
11+
document.title = 'instanode · Real infrastructure for AI agents'
12+
})
13+
afterEach(() => {
14+
document.title = originalTitle
15+
cleanup()
16+
})
17+
18+
describe('BlogPage', () => {
19+
it('sets document.title to "Blog · instanode" on mount', () => {
20+
render(
21+
<MemoryRouter>
22+
<BlogPage />
23+
</MemoryRouter>,
24+
)
25+
expect(document.title).toBe('Blog · instanode')
26+
})
27+
28+
it('restores the previous document.title on unmount', () => {
29+
const { unmount } = render(
30+
<MemoryRouter>
31+
<BlogPage />
32+
</MemoryRouter>,
33+
)
34+
unmount()
35+
expect(document.title).toBe('instanode · Real infrastructure for AI agents')
36+
})
37+
38+
it('renders a card for every post, with link to /blog/<slug>', () => {
39+
if (POSTS.length === 0) return
40+
render(
41+
<MemoryRouter>
42+
<BlogPage />
43+
</MemoryRouter>,
44+
)
45+
for (const p of POSTS) {
46+
const link = document.querySelector(`a[href="/blog/${p.slug}"]`)
47+
expect(link, `missing link for ${p.slug}`).toBeTruthy()
48+
}
49+
})
50+
51+
it('renders posts in reverse-chronological order', () => {
52+
if (POSTS.length < 2) return
53+
render(
54+
<MemoryRouter>
55+
<BlogPage />
56+
</MemoryRouter>,
57+
)
58+
const cards = Array.from(document.querySelectorAll('a.blog-card-link'))
59+
const slugs = cards.map((c) => (c.getAttribute('href') || '').replace('/blog/', ''))
60+
const sorted = [...POSTS].sort((a, b) => b.date.localeCompare(a.date)).map((p) => p.slug)
61+
expect(slugs).toEqual(sorted)
62+
})
63+
})

src/pages/NotFoundPage.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* NotFoundPage.test.tsx — coverage tests for the SPA 404 catch-all. */
2+
import { describe, it, expect, beforeEach } from 'vitest'
3+
import { render, screen } from '@testing-library/react'
4+
import { MemoryRouter } from 'react-router-dom'
5+
import { NotFoundPage } from './NotFoundPage'
6+
7+
describe('NotFoundPage', () => {
8+
beforeEach(() => {
9+
// jsdom defaults pathname to '/' — for one test we override.
10+
})
11+
12+
it('renders 404 eyebrow and headline', () => {
13+
render(
14+
<MemoryRouter>
15+
<NotFoundPage />
16+
</MemoryRouter>,
17+
)
18+
expect(screen.getByText(/404/i)).toBeTruthy()
19+
expect(screen.getByText(/that page is not provisioned/i)).toBeTruthy()
20+
})
21+
22+
it('shows the current URL path in a <code>', () => {
23+
render(
24+
<MemoryRouter>
25+
<NotFoundPage />
26+
</MemoryRouter>,
27+
)
28+
// jsdom default location.pathname is '/' so the rendered code is '/'.
29+
const code = document.querySelector('code.nf-url')
30+
expect(code).toBeTruthy()
31+
expect(code!.textContent).toBe(window.location.pathname || '/')
32+
})
33+
34+
it('renders both primary and secondary CTAs', () => {
35+
render(
36+
<MemoryRouter>
37+
<NotFoundPage />
38+
</MemoryRouter>,
39+
)
40+
const home = document.querySelector('a.nf-cta-primary')
41+
const docs = document.querySelector('a.nf-cta-secondary')
42+
expect(home?.getAttribute('href')).toBe('/')
43+
expect(docs?.getAttribute('href')).toBe('/docs')
44+
})
45+
46+
it('lists the standard help links (pricing, use-cases, blog, changelog)', () => {
47+
render(
48+
<MemoryRouter>
49+
<NotFoundPage />
50+
</MemoryRouter>,
51+
)
52+
for (const href of ['/pricing', '/use-cases', '/blog', '/changelog']) {
53+
expect(document.querySelector(`a[href="${href}"]`)).toBeTruthy()
54+
}
55+
})
56+
})

src/pages/PrivacyPage.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* PrivacyPage.test.tsx — coverage tests for the static legal stop-gap page. */
2+
import { describe, it, expect } from 'vitest'
3+
import { render, screen } from '@testing-library/react'
4+
import { MemoryRouter } from 'react-router-dom'
5+
import { PrivacyPage } from './PrivacyPage'
6+
7+
describe('PrivacyPage', () => {
8+
it('renders the privacy heading and testid wrapper', () => {
9+
render(
10+
<MemoryRouter>
11+
<PrivacyPage />
12+
</MemoryRouter>,
13+
)
14+
const section = screen.getByTestId('privacy-page')
15+
expect(section).toBeTruthy()
16+
expect(section.textContent).toContain('Privacy')
17+
})
18+
19+
it('exposes a mailto link for the legal contact', () => {
20+
render(
21+
<MemoryRouter>
22+
<PrivacyPage />
23+
</MemoryRouter>,
24+
)
25+
const links = document.querySelectorAll('a[href^="mailto:"]')
26+
expect(links.length).toBeGreaterThanOrEqual(1)
27+
expect(links[0].getAttribute('href')).toBe('mailto:legal@instanode.dev')
28+
})
29+
30+
it('mentions our sub-processors so reviewers can see them', () => {
31+
render(
32+
<MemoryRouter>
33+
<PrivacyPage />
34+
</MemoryRouter>,
35+
)
36+
expect(document.body.textContent ?? '').toContain('Razorpay')
37+
expect(document.body.textContent ?? '').toContain('Resend')
38+
})
39+
})

0 commit comments

Comments
 (0)