cc-haha/desktop/icon-assets.test.ts
程序员阿江(Relakkes) 49f1974007 fix(brand): close three icon gaps the first pass missed
Two reviews of the icon swap, run independently from opposite directions,
turned up three places the new mark never reached.

The og:image was the worst. `site/index.html:20` points at
`/images/banner.png`, and that file does not come from `docs/images/banner.png`
— the one this rebrand replaced. `site/scripts/prepare-static-output.mjs:129`
copies `docs/public/` wholesale into the site root before the two selective
image passes run, and neither of those matches an absolute https:// URL, so
`docs/public/images/banner.png` is what shipped. It was a 1200x630 screenshot
of the old site: blue panels, old circular CC badge. Every link shared to
WeChat or Slack would have previewed the pre-rebrand brand while the site
itself rendered the new one. Replaced with a card built from the new lockup;
`site/dist/images/banner.png` now hashes to it.

`desktop/src-tauri/app-icon.png` is the canonical 1024 RGBA source the platform
icon set gets regenerated from. That rule lived only in the body of 3f2ce2a6c,
and nothing references the file in code, so the rebrand skipped it — breaking
an invariant that had held since the file was introduced, where it and
`desktop/public/app-icon.png` were the same git blob. Left as it was, whoever
regenerated icons next would have restored the entire old set.

Linux had no icon above 310px. electron-builder points `linux.icon` at the
whole icons directory and keeps only files named NxN, so `icon.png` (512, no
dimensions in the name) and `128x128@2x.png` (256, collides with
`128x128.png`) were both dropped, leaving a Windows Store asset as the largest
entry. Adding 256x256.png and 512x512.png takes the resolved set from 12
entries topping out at 310 to 14 topping out at 512, confirmed by running
app-builder's icon resolver against the directory.

index.html also had no favicon, and that document is what the H5 remote client
loads in a phone browser.

CI could not have caught any of this — `scripts/quality-gate/package-smoke/`
asserts nothing about icons. `desktop/icon-assets.test.ts` now pins the source
invariant, the Linux sizes, the three packaged icons and the favicon; each
assertion was checked to fail when its subject is reverted.
2026-07-27 04:27:41 +08:00

58 lines
2.6 KiB
TypeScript

import { createHash } from 'node:crypto'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* Guards on the icon assets, because nothing else does.
*
* `scripts/quality-gate/package-smoke/` makes no icon assertions, so a build
* that ships a stale or half-replaced icon set goes green on every pipeline.
* The rules below were each learned by breaking them.
*/
const desktopRoot = __dirname
const icons = path.join(desktopRoot, 'src-tauri', 'icons')
const sha = (file: string) => createHash('sha256').update(readFileSync(file)).digest('hex')
describe('icon assets', () => {
it('keeps src-tauri/app-icon.png byte-identical to public/app-icon.png', () => {
// src-tauri/app-icon.png is the canonical 1024 RGBA source the platform
// icon set gets regenerated from — a directive that lived only in a commit
// body, which is exactly why a later rebrand replaced everything except
// this file and left a stale source primed to overwrite the new set.
const source = path.join(desktopRoot, 'src-tauri', 'app-icon.png')
const runtime = path.join(desktopRoot, 'public', 'app-icon.png')
expect(sha(source)).toBe(sha(runtime))
})
it('ships the sizes Linux actually looks for', () => {
// electron-builder points `linux.icon` at the whole icons/ directory and
// keeps only files named NxN. `icon.png` (512) has no dimensions in its
// name and `128x128@2x.png` (256) collides with `128x128.png`, so both are
// dropped — which once left the largest Linux icon at 310px, from a
// Windows Store asset, and blurry on HiDPI.
const present = new Set(readdirSync(icons))
for (const size of [32, 64, 128, 256, 512]) {
expect(present.has(`${size}x${size}.png`)).toBe(true)
}
})
it('carries a packaged icon for every desktop platform', () => {
// mac.icon / win.icon in package.json name these two directly; a missing
// file makes electron-builder silently fall back to the Electron logo.
for (const file of ['icon.icns', 'icon.ico', 'icon.png']) {
const full = path.join(icons, file)
expect(existsSync(full)).toBe(true)
expect(readFileSync(full).byteLength).toBeGreaterThan(1024)
}
})
it('serves a favicon to the H5 client', () => {
// The same index.html the Electron window loads is what a phone browser
// gets over H5 remote access, where a missing icon means a blank tab.
const html = readFileSync(path.join(desktopRoot, 'index.html'), 'utf8')
expect(html).toMatch(/<link\s+rel="icon"/)
})
})