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