import { access, readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { inflateSync } from 'node:zlib' import { describe, expect, it } from 'vitest' const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const publicRoot = path.join(projectRoot, 'public') const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) async function readProjectFile(relativePath) { return readFile(path.join(projectRoot, relativePath)) } function pngDimensions(buffer) { expect(buffer.subarray(0, pngSignature.length)).toEqual(pngSignature) expect(buffer.subarray(12, 16).toString('ascii')).toBe('IHDR') return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) } } function paethPredictor(left, above, upperLeft) { const estimate = left + above - upperLeft const leftDistance = Math.abs(estimate - left) const aboveDistance = Math.abs(estimate - above) const upperLeftDistance = Math.abs(estimate - upperLeft) if (leftDistance <= aboveDistance && leftDistance <= upperLeftDistance) return left return aboveDistance <= upperLeftDistance ? above : upperLeft } function decodeRgbaPng(buffer) { const { width, height } = pngDimensions(buffer) expect(buffer[24]).toBe(8) expect(buffer[25]).toBe(6) const idatChunks = [] for (let offset = 8; offset < buffer.length;) { const length = buffer.readUInt32BE(offset) const type = buffer.subarray(offset + 4, offset + 8).toString('ascii') if (type === 'IDAT') idatChunks.push(buffer.subarray(offset + 8, offset + 8 + length)) offset += 12 + length } const encoded = inflateSync(Buffer.concat(idatChunks)) const rowLength = width * 4 const pixels = Buffer.alloc(rowLength * height) let encodedOffset = 0 for (let row = 0; row < height; row += 1) { const filter = encoded[encodedOffset] expect(filter).toBeLessThanOrEqual(4) encodedOffset += 1 const rowOffset = row * rowLength const previousRowOffset = rowOffset - rowLength for (let column = 0; column < rowLength; column += 1) { const left = column >= 4 ? pixels[rowOffset + column - 4] : 0 const above = row > 0 ? pixels[previousRowOffset + column] : 0 const upperLeft = row > 0 && column >= 4 ? pixels[previousRowOffset + column - 4] : 0 const predictor = filter === 1 ? left : filter === 2 ? above : filter === 3 ? Math.floor((left + above) / 2) : filter === 4 ? paethPredictor(left, above, upperLeft) : 0 pixels[rowOffset + column] = (encoded[encodedOffset] + predictor) & 0xff encodedOffset += 1 } } expect(encodedOffset).toBe(encoded.length) return { width, height, pixels } } describe('application branding assets', () => { it('keeps the vector favicon self-contained', async () => { const svg = await readFile(path.join(publicRoot, 'favicon.svg'), 'utf8') expect(svg).toContain('viewBox="0 0 64 64"') expect(svg).toContain('#1d2428') expect(svg).toContain('stroke="#ffffff"') expect(svg).not.toMatch(/ { const { width, height, pixels } = decodeRgbaPng(await readFile(path.join(publicRoot, filename))) let minimumAlpha = 255 let maximumAlpha = 0 let hasLightArtwork = false for (let offset = 0; offset < pixels.length; offset += 4) { minimumAlpha = Math.min(minimumAlpha, pixels[offset + 3]) maximumAlpha = Math.max(maximumAlpha, pixels[offset + 3]) if (pixels[offset] > 240 && pixels[offset + 1] > 240 && pixels[offset + 2] > 240) hasLightArtwork = true } expect({ width, height }).toEqual({ width: size, height: size }) expect([minimumAlpha, maximumAlpha]).toEqual(opaque ? [255, 255] : [0, 255]) expect(hasLightArtwork).toBe(true) }) it('packs 16, 32 and 48 pixel PNG frames into the ICO fallback', async () => { const ico = await readFile(path.join(publicRoot, 'favicon.ico')) expect(ico.readUInt16LE(0)).toBe(0) expect(ico.readUInt16LE(2)).toBe(1) expect(ico.readUInt16LE(4)).toBe(3) const sizes = [] for (let index = 0; index < 3; index += 1) { const entryOffset = 6 + index * 16 const width = ico[entryOffset] || 256 const height = ico[entryOffset + 1] || 256 const length = ico.readUInt32LE(entryOffset + 8) const imageOffset = ico.readUInt32LE(entryOffset + 12) const frame = ico.subarray(imageOffset, imageOffset + length) expect(height).toBe(width) expect(imageOffset + length).toBeLessThanOrEqual(ico.length) expect(pngDimensions(frame)).toEqual({ width, height }) sizes.push(width) } expect(sizes).toEqual([16, 32, 48]) }) it('references complete install metadata and existing icon files', async () => { const manifest = JSON.parse(await readFile(path.join(publicRoot, 'site.webmanifest'), 'utf8')) expect(manifest).toMatchObject({ name: 'Web Tools', short_name: 'Web Tools', description: '一个匿名访问、自托管的浏览器本地工具箱', start_url: '/', scope: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#f5f6f7', }) expect(manifest.icons).toEqual([ { src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, { src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { src: '/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, ]) await Promise.all(manifest.icons.map((icon) => access(path.join(publicRoot, icon.src.slice(1))))) }) it('declares modern, legacy and Apple browser icon entry points', async () => { const html = (await readProjectFile('index.html')).toString('utf8') expect(html).toContain('') expect(html).toContain('') expect(html).toContain('') expect(html).toContain('') }) it('declares one product description for standard and Open Graph consumers without a custom image', async () => { const html = (await readProjectFile('index.html')).toString('utf8') const socialMetadata = [...html.matchAll(/ match[1].toLowerCase()) expect(html).toContain('') expect(html).toContain('') expect(html.match(/一个匿名访问、自托管的浏览器本地工具箱/g)).toHaveLength(2) expect(socialMetadata).toEqual(['og:description']) expect(html).not.toContain('share-preview.png') await expect(access(path.join(publicRoot, 'share-preview.png'))).rejects.toMatchObject({ code: 'ENOENT' }) }) it('includes public assets in container builds', async () => { expect((await readProjectFile('Dockerfile')).toString('utf8')).toContain('COPY public ./public') }) })