diff --git a/tests/now-svg.test.ts b/tests/now-svg.test.ts
index 0d63955..3d9c351 100644
--- a/tests/now-svg.test.ts
+++ b/tests/now-svg.test.ts
@@ -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', () => {
@@ -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 })
@@ -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({
@@ -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">正在')
- expect(svg).toContain(' Zed')
- 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(/]*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(' {
+ const media = 'Helplessness Blues – Fleet Foxes '.repeat(20)
+ const svg = renderNowSvg({ kind: 'active', app: 'Vivaldi', media }, NOW_THEMES.light)
+ const drawn = [...svg.matchAll(/]*>([^<]*)<\/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