Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 230 additions & 9 deletions tests/now-svg.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { describe, expect, it } from 'vitest'
import {
ACTIVITY_STALE_MS,
ACTIVITY_URL,
ICON_MAX_BYTES,
NOW_THEMES,
fetchNowActivity,
fetchNowIcon,
fitNowRow,
handleNowSvg,
nowCopy,
nowIsSleeping,
nowLine,
nowSvgHeaders,
nowThemeFromPath,
parseNowActivity,
renderNowSvg,
truncateNowText,
xmlEscape,
} from '../workers/froq-api/src/nowSvg'

const FRESH = '2026-10-28T10:00:00.000Z'
const NOW = Date.parse(FRESH)
const PNG_1X1_B64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
const PNG_1X1 = Uint8Array.from(atob(PNG_1X1_B64), c => c.charCodeAt(0))
const PNG_DATA_URI = `data:image/png;base64,${PNG_1X1_B64}`

describe('nowThemeFromPath', () => {
it('maps light and dark paths', () => {
Expand Down Expand Up @@ -61,6 +71,28 @@ describe('parseNowActivity', () => {
})
})

it('keeps process.iconUrl only when the process has a name', () => {
expect(
parseNowActivity({
active: true,
timestamp: FRESH,
process: {
name: 'Zed',
iconUrl: 'https://example.com/zed.png',
},
}),
).toMatchObject({
processName: 'Zed',
iconUrl: 'https://example.com/zed.png',
})
expect(
parseNowActivity({
active: true,
process: { iconUrl: 'https://example.com/orphan.png' },
}),
).not.toHaveProperty('iconUrl')
})

it('treats junk payloads as inactive', () => {
expect(parseNowActivity(null)).toEqual({ active: false })
expect(parseNowActivity('nope')).toEqual({ active: false })
Expand Down Expand Up @@ -114,6 +146,18 @@ describe('nowLine', () => {
})
})

it('passes iconUrl through on both active and sleeping lines', () => {
const iconUrl = 'https://example.com/zed.png'
expect(
nowLine({ active: true, timestamp: FRESH, processName: 'Zed', iconUrl }, NOW),
).toMatchObject({ kind: 'active', app: 'Zed', iconUrl })
expect(nowLine({ active: false, processName: 'Zed', iconUrl }, NOW)).toMatchObject({
kind: 'sleeping',
app: 'Zed',
iconUrl,
})
})

it('sleeping copy uses the last app, or 眠了 when unknown', () => {
expect(nowLine(null, NOW)).toEqual({ kind: 'sleeping' })
expect(nowLine({ active: false, timestamp: FRESH, processName: 'Cursor' }, NOW)).toEqual({
Expand All @@ -123,24 +167,116 @@ describe('nowLine', () => {
})
})

describe('nowCopy', () => {
it('keeps the existing Chinese lines', () => {
expect(nowCopy({ kind: 'active', app: 'Zed' })).toEqual({
prefix: '正在 ',
app: 'Zed',
suffix: '',
})
expect(
nowCopy({ kind: 'active', app: 'Vivaldi', media: 'Helplessness Blues – Fleet Foxes' }),
).toEqual({
prefix: '正在 ',
app: 'Vivaldi',
suffix: ' · Helplessness Blues – Fleet Foxes',
})
expect(nowCopy({ kind: 'sleeping', app: 'Cursor' })).toEqual({
prefix: '在 ',
app: 'Cursor',
suffix: ' 中眠了',
})
expect(nowCopy({ kind: 'sleeping' })).toEqual({
prefix: '眠了',
app: '',
suffix: '',
})
})
})

describe('fitNowRow', () => {
it('truncates media before the app name', () => {
const media = ` · ${'Helplessness Blues – Fleet Foxes '.repeat(12)}`
const fitted = fitNowRow(
{ prefix: '正在 ', app: 'Vivaldi', suffix: media },
{ hasIcon: true, sleeping: false },
)
expect(fitted.app).toBe('Vivaldi')
expect(fitted.suffix).toContain('…')
expect(fitted.suffix.length).toBeLessThan(media.length)
})

it('truncates a long app when that is the only overflow', () => {
const app = 'W'.repeat(120)
const fitted = fitNowRow(
{ prefix: '正在 ', app, suffix: '' },
{ hasIcon: false, sleeping: false },
)
expect(fitted.app).toContain('…')
expect(fitted.app.length).toBeLessThan(app.length)
})
})

describe('truncateNowText', () => {
it('returns the original string when it already fits', () => {
expect(truncateNowText('Zed', 400)).toBe('Zed')
})
})

describe('renderNowSvg', () => {
it('accents 正在 on the light theme', () => {
it('draws a SiteDoing-like working row: ink dot, mono, muted 正在', () => {
const svg = renderNowSvg({ kind: 'active', app: 'Zed' }, NOW_THEMES.light)
expect(svg).toContain('fill="#a83429">正在</tspan>')
expect(svg).toContain('<tspan> Zed</tspan>')
expect(svg).toContain('font-family="Georgia,')
expect(svg).toContain('fill="#1d1d1a"')
expect(svg).toContain('fill="#5c5a54"')
expect(svg).toContain('正在 ')
expect(svg).toContain('Zed')
expect(svg).toContain('ui-monospace')
expect(svg).not.toContain('Georgia')
expect(svg).not.toContain('serif')
expect(svg).toContain('width="640"')
expect(svg).toContain('height="28"')
expect(svg).toContain('height="24"')
expect(svg).toMatch(/<circle[^>]*fill="#1d1d1a"/)
expect(svg).not.toContain('font-style="italic"')
})

it('sleeping line is italic muted, including 眠了', () => {
it('sleeping line is hollow muted dot + 眠了, not italic serif', () => {
const withApp = renderNowSvg({ kind: 'sleeping', app: 'Cursor' }, NOW_THEMES.dark)
expect(withApp).toContain('font-style="italic"')
expect(withApp).toContain('fill="#9c968c"')
expect(withApp).toContain('在 Cursor 中眠了')
expect(withApp).not.toContain('font-style="italic"')
expect(withApp).toContain('fill="none"')
expect(withApp).toContain('stroke="#9c968c"')
expect(withApp).toContain('在 ')
expect(withApp).toContain('Cursor')
expect(withApp).toContain(' 中眠了')
expect(withApp).toContain('id="now-sleep"')
expect(withApp).toContain('ui-monospace')
expect(renderNowSvg({ kind: 'sleeping' }, NOW_THEMES.dark)).toContain('眠了')
})

it('embeds a PNG data URI beside the app and rejects remote hrefs', () => {
const withIcon = renderNowSvg({ kind: 'active', app: 'Zed' }, NOW_THEMES.light, PNG_DATA_URI)
expect(withIcon).toContain(`href="${PNG_DATA_URI}"`)
expect(withIcon).toContain('width="16"')
expect(withIcon).toContain('height="16"')
const remote = renderNowSvg(
{ kind: 'active', app: 'Zed' },
NOW_THEMES.light,
'https://evil.example/zed.png',
)
expect(remote).not.toContain('evil.example')
expect(remote).not.toContain('<image')
})

it('ellipsizes long media so the row stays inside 640px', () => {
const media = 'Helplessness Blues – Fleet Foxes '.repeat(20)
const svg = renderNowSvg({ kind: 'active', app: 'Vivaldi', media }, NOW_THEMES.light)
const drawn = [...svg.matchAll(/<text\b[^>]*>([^<]*)<\/text>/g)].map(match => match[1]).join('')
expect(drawn).toContain('…')
expect(drawn).not.toContain(media)
expect(drawn).toContain('Vivaldi')
expect(svg).toContain(`aria-label="正在 Vivaldi · ${media}"`)
expect(svg).toContain('width="640"')
})

it('escapes XML in labels', () => {
const svg = renderNowSvg({ kind: 'active', app: 'A&B <script>' }, NOW_THEMES.light)
expect(svg).toContain('A&amp;B &lt;script&gt;')
Expand Down Expand Up @@ -169,6 +305,56 @@ describe('handleNowSvg', () => {
const body = await res.text()
expect(body).toContain('正在')
expect(body).toContain('Zed')
expect(body).toContain('ui-monospace')
})

it('inlines a fetched PNG icon as a data URI', async () => {
const iconUrl = 'https://icons.example/zed.png'
const payload = {
active: true,
timestamp: new Date().toISOString(),
process: { name: 'Zed', iconUrl },
}
const res = await handleNowSvg(
new Request('https://api.froq.me/now-light.svg'),
'light',
async input => {
if (input === ACTIVITY_URL) {
return new Response(JSON.stringify(payload), { status: 200 })
}
if (input === iconUrl) {
return new Response(PNG_1X1, { headers: { 'Content-Type': 'image/png' }, status: 200 })
}
throw new Error(`unexpected fetch ${input}`)
},
)
const body = await res.text()
expect(body).toContain(`href="${PNG_DATA_URI}"`)
expect(body).not.toContain(iconUrl)
})

it('stays text-only when the icon fetch fails', async () => {
const iconUrl = 'https://icons.example/zed.png'
const payload = {
active: true,
timestamp: new Date().toISOString(),
process: { name: 'Zed', iconUrl },
}
const res = await handleNowSvg(
new Request('https://api.froq.me/now-light.svg'),
'light',
async input => {
if (input === ACTIVITY_URL) {
return new Response(JSON.stringify(payload), { status: 200 })
}
return new Response('nope', { status: 500 })
},
)
const body = await res.text()
expect(body).toContain('Zed')
expect(body).toContain('正在')
expect(body).not.toContain('<image')
expect(body).not.toContain(iconUrl)
})

it('fetch failure renders 眠了, never a live app', async () => {
Expand Down Expand Up @@ -196,6 +382,41 @@ describe('handleNowSvg', () => {
})
})

describe('fetchNowIcon', () => {
it('returns a png data URI for a PNG body', async () => {
await expect(
fetchNowIcon('https://icons.example/a.png', async () => {
return new Response(PNG_1X1, { headers: { 'Content-Type': 'image/png' }, status: 200 })
}),
).resolves.toBe(PNG_DATA_URI)
})

it('rejects svg, oversize, and non-http URLs', async () => {
await expect(
fetchNowIcon('https://icons.example/a.svg', async () => {
return new Response('<svg xmlns="http://www.w3.org/2000/svg"></svg>', {
headers: { 'Content-Type': 'image/svg+xml' },
status: 200,
})
}),
).resolves.toBeUndefined()

const huge = new Uint8Array(ICON_MAX_BYTES + 1)
huge[0] = 0x89
huge[1] = 0x50
huge[2] = 0x4e
huge[3] = 0x47
await expect(
fetchNowIcon('https://icons.example/huge.png', async () => {
return new Response(huge, { headers: { 'Content-Type': 'image/png' }, status: 200 })
}),
).resolves.toBeUndefined()

await expect(fetchNowIcon('data:image/png;base64,aaaa')).resolves.toBeUndefined()
await expect(fetchNowIcon('/relative.png')).resolves.toBeUndefined()
})
})

describe('fetchNowActivity', () => {
it('returns null when the upstream is down', async () => {
expect(
Expand Down
9 changes: 9 additions & 0 deletions workers/froq-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ locked to froq.me.
Cache: `Cache-Control: public, max-age=60, s-maxage=60` plus matching `Expires`.
Also `Cross-Origin-Resource-Policy: cross-origin` for Camo.

The row matches froq.me `SiteDoing`: monospace type, 8px status dot (filled ink
when working, hollow muted when sleeping), and the 16×16 app icon from
`process.iconUrl`. Camo cannot load arbitrary image hosts, so the Worker fetches
the icon and inlines it as a `data:image/png;base64,…` URI. Icon fetch failure
is text-only — never an external `<image href>`.

Sleeping (same rule as froq.me `activityIsSleeping`, plus `active === false`):
missing/invalid timestamp, stamp older than 5 minutes, or `active` not true.
Fetch failure renders `眠了` — never a fake live app.

Copy stays the existing Chinese lines: `正在 {app} · media`, `在 {app} 中眠了`,
or `眠了`. Long app/media labels ellipsize inside the 640px README width.

```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.froq.me/now-dark.svg">
Expand Down
Loading
Loading