Files
hyperframes/packages/cli/src/capture/faviconRanker.test.ts
T
Miguel Ángel 7dc31bd2cc fix(cli): score apple-touch-icon-precomposed at the 180px default (#3607)
declaredSize() matched the rel token "apple-touch-icon" exactly, so the
legacy "apple-touch-icon-precomposed" spelling (one token, not two) fell
through to 0 instead of the 180px Apple default, even though the selector
already collects it. Same page, one spelling apart, opposite winner inside
tier 1 — it never drops a candidate and never beats the .ico tier.

Switch to a startsWith check on the token. Exact-token matching stays for
mask-icon, where a longer rel really would be a different asset.
2026-09-03 00:21:05 -04:00

156 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { parseSizes, rankIconCandidates, type IconCandidate } from "./faviconRanker.js";
/**
* Fixtures transcribe the `<link>` tags these sites declare in their document head.
* They are the shapes that made DOM-order downloading land the worst icon on disk.
*/
// linear.app: legacy .ico declared first, the SVG and the 180px apple-touch after it.
const LINEAR: IconCandidate[] = [
{ rel: "icon", href: "https://linear.app/favicon.ico", sizes: "any", type: null },
{ rel: "icon", href: "https://linear.app/favicon.svg", sizes: null, type: "image/svg+xml" },
{
rel: "apple-touch-icon",
href: "https://linear.app/apple-touch-icon.png",
sizes: "180x180",
type: null,
},
];
// notion.com: .ico first, then an apple-touch png that declares no `sizes`.
const NOTION: IconCandidate[] = [
{ rel: "icon", href: "https://www.notion.com/front-static/favicon.ico", sizes: null, type: null },
{
rel: "apple-touch-icon",
href: "https://www.notion.com/front-static/logo-ios.png",
sizes: null,
type: null,
},
];
// stripe.com: svg, a 96x96 png, a shortcut .ico, and a 180x180 apple-touch png.
const STRIPE: IconCandidate[] = [
{
rel: "icon",
href: "https://images.stripeassets.com/x/favicon.svg",
sizes: null,
type: "image/svg+xml",
},
{
rel: "icon",
href: "https://images.stripeassets.com/x/favicon.png?w=96&h=96",
sizes: "96x96",
type: "image/png",
},
{
rel: "shortcut icon",
href: "https://assets.stripeassets.com/x/favicon.ico",
sizes: null,
type: null,
},
{
rel: "apple-touch-icon",
href: "https://images.stripeassets.com/x/favicon.png?w=180&h=180",
sizes: "180x180",
type: null,
},
];
const hrefs = (cs: IconCandidate[]): string[] => rankIconCandidates(cs).map((c) => c.href);
describe("rankIconCandidates", () => {
it("puts the SVG first even though the page declares the .ico first", () => {
expect(hrefs(LINEAR)).toEqual([
"https://linear.app/favicon.svg",
"https://linear.app/apple-touch-icon.png",
"https://linear.app/favicon.ico",
]);
});
it("prefers an unsized apple-touch-icon over a .ico", () => {
expect(hrefs(NOTION)).toEqual([
"https://www.notion.com/front-static/logo-ios.png",
"https://www.notion.com/front-static/favicon.ico",
]);
});
it("orders svg, then largest declared size, then .ico", () => {
expect(hrefs(STRIPE)).toEqual([
"https://images.stripeassets.com/x/favicon.svg",
"https://images.stripeassets.com/x/favicon.png?w=180&h=180",
"https://images.stripeassets.com/x/favicon.png?w=96&h=96",
"https://assets.stripeassets.com/x/favicon.ico",
]);
});
it("does not let a Safari pinned-tab silhouette beat the real favicon", () => {
// A mask-icon is an SVG, so ranking on format alone would promote the silhouette.
const masked: IconCandidate[] = [
{ rel: "mask-icon", href: "https://x.test/pinned.svg", sizes: null, type: null },
{ rel: "icon", href: "https://x.test/favicon.png", sizes: "32x32", type: "image/png" },
{ rel: "icon", href: "https://x.test/favicon.svg", sizes: null, type: "image/svg+xml" },
];
expect(hrefs(masked)).toEqual([
"https://x.test/favicon.svg",
"https://x.test/favicon.png",
"https://x.test/pinned.svg",
]);
});
it("still returns a mask-icon when the page declares nothing else", () => {
// Ranked last, not dropped: a silhouette on disk beats no icon at all.
const only: IconCandidate[] = [{ rel: "mask-icon", href: "https://x.test/pinned.svg" }];
expect(hrefs(only)).toEqual(["https://x.test/pinned.svg"]);
});
it("drops candidates with no href", () => {
expect(hrefs([{ rel: "icon", href: "" }, ...NOTION])).toHaveLength(2);
});
it("scores apple-touch-icon-precomposed at the same 180px default as apple-touch-icon", () => {
// Precomposed is one token longer, not a different rel: it must win against a small
// sized png the same way a plain apple-touch-icon would.
const precomposed: IconCandidate[] = [
{
rel: "apple-touch-icon-precomposed",
href: "https://x.test/apple-touch-icon-precomposed.png",
sizes: null,
type: null,
},
{ rel: "icon", href: "https://x.test/favicon-32.png", sizes: "32x32", type: "image/png" },
];
expect(hrefs(precomposed)).toEqual([
"https://x.test/apple-touch-icon-precomposed.png",
"https://x.test/favicon-32.png",
]);
});
it("keeps DOM order between candidates of equal rank", () => {
const same: IconCandidate[] = [
{ rel: "icon", href: "https://x.test/a.png", sizes: "32x32" },
{ rel: "icon", href: "https://x.test/b.png", sizes: "32x32" },
];
expect(hrefs(same)).toEqual(["https://x.test/a.png", "https://x.test/b.png"]);
});
});
describe("parseSizes", () => {
it("reads a single declaration", () => {
expect(parseSizes("32x32")).toBe(32);
});
it("takes the largest of a multi-size declaration", () => {
expect(parseSizes("180x180 167x167")).toBe(180);
});
it("scores `any` as no declared pixel size", () => {
expect(parseSizes("any")).toBe(0);
});
it("scores a missing attribute as no declared pixel size", () => {
expect(parseSizes(null)).toBe(0);
expect(parseSizes(undefined)).toBe(0);
});
});