'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { escapeHtmlAttribute, getFallbackLabel, getPageOriginFaviconUrl, getGoogleFaviconUrl, getGroupIcon, getPrimaryDomain, getIconSources, getFaviconUrl, } = require('./icon-utils.js'); test('getIconSources prefers real favicon before chrome, origin, and Google fallbacks', () => { const savedChrome = globalThis.chrome; globalThis.chrome = { runtime: { getURL: (path) => `chrome-extension://abc/${path}`, }, }; try { const iconData = getIconSources({ favIconUrl: 'https://example.com/favicon.ico', url: 'https://www.example.com/page', }, 32); assert.equal(iconData.hostname, 'www.example.com'); assert.equal(iconData.primaryDomain, 'example.com'); assert.deepEqual(iconData.sources, [ 'https://example.com/favicon.ico', 'chrome-extension://abc/_favicon/?pageUrl=https%3A%2F%2Fwww.example.com%2Fpage&size=32', 'https://www.example.com/favicon.ico', 'https://www.google.com/s2/favicons?domain=example.com&sz=32', 'https://www.google.com/s2/favicons?domain=www.example.com&sz=32', ]); } finally { if (savedChrome === undefined) { delete globalThis.chrome; } else { globalThis.chrome = savedChrome; } } }); test('getGroupIcon falls back through origin and Google sources when tab has no real favicon', () => { const iconData = getGroupIcon({ tabs: [{ url: 'https://chatgpt.com/c/test' }], }, 'ChatGPT', 32); assert.equal(iconData.src, 'https://chatgpt.com/favicon.ico'); assert.equal(iconData.fallbackSrc, 'https://www.google.com/s2/favicons?domain=chatgpt.com&sz=32'); assert.deepEqual(iconData.fallbackSources, [ 'https://www.google.com/s2/favicons?domain=chatgpt.com&sz=32', ]); assert.equal(iconData.fallbackLabel, 'C'); }); test('getFallbackLabel derives stable initials from labels and hosts', () => { assert.equal(getFallbackLabel('GitHub Issues', 'github.com'), 'GI'); assert.equal(getFallbackLabel('', 'www.wikipedia.org'), 'WI'); assert.equal(getGoogleFaviconUrl('github.com', 16), 'https://www.google.com/s2/favicons?domain=github.com&sz=16'); assert.equal(getPageOriginFaviconUrl('https://www.ict.ac.cn/yjsjy/zsxx/sszs/202605/t20260511_8199682.html'), 'https://www.ict.ac.cn/favicon.ico'); }); test('getIconSources uses chrome, origin, and Google fallback chain when no real favicon is available', () => { const savedChrome = globalThis.chrome; globalThis.chrome = { runtime: { getURL: (path) => `chrome-extension://abc/${path}`, }, }; try { const iconData = getIconSources({ url: 'https://www.example.com/page', }, 32); assert.deepEqual(iconData.sources, [ 'chrome-extension://abc/_favicon/?pageUrl=https%3A%2F%2Fwww.example.com%2Fpage&size=32', 'https://www.example.com/favicon.ico', 'https://www.google.com/s2/favicons?domain=example.com&sz=32', 'https://www.google.com/s2/favicons?domain=www.example.com&sz=32', ]); } finally { if (savedChrome === undefined) { delete globalThis.chrome; } else { globalThis.chrome = savedChrome; } } }); test('getPrimaryDomain collapses regular subdomains but preserves common hosted tenants', () => { assert.equal(getPrimaryDomain('foo.example.com'), 'example.com'); assert.equal(getPrimaryDomain('foo.bar.example.co.uk'), 'example.co.uk'); assert.equal(getPrimaryDomain('ict.ac.cn'), 'ict.ac.cn'); assert.equal(getPrimaryDomain('myproject.github.io'), 'myproject.github.io'); assert.equal(getPrimaryDomain('writer.substack.com'), 'writer.substack.com'); }); test('getIconSources skips unstable browser internal favicon urls but keeps Google fallback URLs', () => { const iconData = getIconSources({ favIconUrl: 'chrome://favicon2/?pageUrl=https%3A%2F%2Fwww.ict.ac.cn', url: 'https://www.ict.ac.cn/yjsjy/zsxx/sszs/202605/t20260511_8199682.html', }, 32); assert.deepEqual(iconData.sources, [ 'https://www.ict.ac.cn/favicon.ico', 'https://www.google.com/s2/favicons?domain=ict.ac.cn&sz=32', 'https://www.google.com/s2/favicons?domain=www.ict.ac.cn&sz=32', ]); }); test('getFaviconUrl builds fallback URL from URL string input (no chrome runtime)', () => { const result = getFaviconUrl('https://www.example.com/page'); assert.equal(result.source, ''); assert.equal(result.url, ''); assert.equal(result.fallback, 'https://www.example.com/favicon.ico'); }); test('getFaviconUrl returns empty result for empty input', () => { const result = getFaviconUrl({}); assert.equal(result.url, ''); assert.equal(result.source, ''); assert.equal(result.fallback, ''); }); test('getFaviconUrl returns empty result for non-HTTP URL', () => { const result = getFaviconUrl('chrome://extensions/'); assert.equal(result.url, ''); assert.equal(result.source, ''); assert.equal(result.fallback, ''); }); test('getFaviconUrl builds fallback URL from options object', () => { const result = getFaviconUrl({ domain: 'github.com', size: 64 }); assert.equal(result.fallback, 'https://github.com/favicon.ico'); }); test('getFaviconUrl preserves http protocol and localhost port in fallback URL', () => { const result = getFaviconUrl('http://localhost:3000/app'); assert.equal(result.fallback, 'http://localhost:3000/favicon.ico'); }); test('getFaviconUrl preserves custom https port in fallback URL', () => { const result = getFaviconUrl('https://example.com:8443/path'); assert.equal(result.fallback, 'https://example.com:8443/favicon.ico'); }); test('getFaviconUrl defaults size to 128 for string input', () => { const result = getFaviconUrl('https://example.com'); assert.ok(result.url === '' || result.url.includes('size=128'), 'Chrome URL should use size=128 when available'); }); test('getFaviconUrl respects custom size in options', () => { const result = getFaviconUrl({ domain: 'example.com', size: 32 }); assert.ok(result.url === '' || result.url.includes('size=32'), 'Chrome URL should use custom size when available'); }); test('getFaviconUrl generates chrome _favicon URL when chrome runtime is available', () => { const savedGetURL = globalThis.chrome?.runtime?.getURL; globalThis.chrome = { runtime: { getURL: (path) => `chrome-extension://abc/${path}`, }, }; try { const result = getFaviconUrl({ domain: 'https://example.com/path', size: 32 }); assert.ok(result.url.startsWith('chrome-extension://abc/_favicon/'), 'Chrome URL should use _favicon path'); assert.ok(result.url.includes('pageUrl='), 'Chrome URL should include pageUrl param'); assert.ok(result.url.includes('size=32'), 'Chrome URL should include size param'); assert.equal(result.fallback, 'https://example.com/favicon.ico'); } finally { if (savedGetURL) { globalThis.chrome = { runtime: { getURL: savedGetURL } }; } else { delete globalThis.chrome; } } }); test('escapeHtmlAttribute protects custom tooltip text', () => { assert.equal( escapeHtmlAttribute('ChatGPT "Projects" & Notes'), 'ChatGPT "Projects" & Notes' ); });