Files
SpineViewer/tests/apng-thumbnail.test.js
T
2026-09-19 10:36:32 +08:00

80 lines
4.0 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const { encode } = require('../js/apng-encoder');
function chunks(buffer) {
const result = [];
let offset = 8;
while (offset < buffer.length) {
const length = buffer.readUInt32BE(offset);
const type = buffer.toString('ascii', offset + 4, offset + 8);
result.push({ type, data: buffer.subarray(offset + 8, offset + 8 + length) });
offset += length + 12;
}
return result;
}
const red = new Uint8Array(16);
const blue = new Uint8Array(16);
for (let index = 0; index < 16; index += 4) {
red.set([255, 0, 0, 255], index);
blue.set([0, 0, 255, 255], index);
}
const png = encode({
width: 2,
height: 2,
frames: [
{ rgba: red, delayNumerator: 125, delayDenominator: 1000 },
{ rgba: blue, delayNumerator: 125, delayDenominator: 1000 },
],
});
assert.deepEqual([...png.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]);
const parsed = chunks(png);
assert.deepEqual(parsed.map((entry) => entry.type), ['IHDR', 'acTL', 'fcTL', 'IDAT', 'fcTL', 'fdAT', 'IEND']);
assert.equal(parsed.find((entry) => entry.type === 'acTL').data.readUInt32BE(0), 2);
assert.equal(parsed[2].data.readUInt32BE(0), 0);
assert.equal(parsed[4].data.readUInt32BE(0), 1);
assert.equal(parsed[5].data.readUInt32BE(0), 2);
for (const root of ['.', 'release/UNIVERSAL_SpineViewer']) {
const html = fs.readFileSync(root + '/viewer/spine.html', 'utf8');
const css = fs.readFileSync(root + '/viewer/spine.css', 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
new vm.Script(script);
assert.match(script, /addEventListener\('contextmenu'[\s\S]*generateAnimatedThumbnail\(\)/);
assert.match(script, /groupThumbBtn'[\s\S]*addEventListener\('contextmenu'[\s\S]*generateGroupThumbnails\(true\)/);
assert.match(script, /async function generateGroupThumbnails\(animated = false\)/);
assert.match(script, /if \(animated[\s\S]*renderAnimatedThumbnail\(animation, options\)[\s\S]*renderCurrentFrameThumbnail\(\)/);
assert.match(script, /const size = 400;/, 'animated output must remain fixed at 400x400');
assert.match(script, /async function encodeAnimatedThumbnailToLimit/);
assert.match(script, /const targetBytes = maxKilobytes \* 1024/);
assert.match(script, /if \(buffer\.length <= targetBytes\) return/);
assert.match(script, /count: 2, bits: 3, block: 12/);
assert.match(script, /count: 2, bits: 2, block: 1, size: 32/);
assert.match(script, /const outputSize = profile\.size \|\| size/);
assert.match(script, /return smallest;/);
assert.doesNotMatch(script, /throw new Error\(t\('viewer\.animatedCannotFit'/);
assert.match(script, /require\('\.\.\/js\/animated-webp-encoder\.js'\)/);
assert.match(script, /async function encodeAnimatedWebpToLimit/);
assert.match(script, /canvas\.toDataURL\('image\/webp', profile\.quality\)/);
assert.match(script, /extension: options\.format === 'apng' \? '\.png' : '\.webp'/);
assert.match(script, /for \(let alpha = 3; alpha < rgba\.length; alpha \+= 4\) rgba\[alpha\] = 255/);
assert.match(script, /const totalFrames = Math\.max\(1, frameOf\(animationLength\)\)/);
assert.match(script, /start: startFrame \/ FPS/);
assert.match(html, /id="animatedThumbStart" type="number" min="0" step="1"/);
assert.match(html, /id="animatedThumbEnd" type="number" min="0" step="1"/);
assert.match(html, /data-i18n="viewer\.framesUnit"/);
assert.match(html, /id="animatedThumbMaxSize" type="number" min="50" max="10240" step="50" value="100"/);
assert.doesNotMatch(html, /id="animatedThumbQuality"/);
assert.doesNotMatch(html, /viewer\.secondsUnit/);
assert.match(html, /id="animatedThumbDialog"/);
assert.match(html, /id="animatedThumbFormat"><option value="webp">AWebP \(\.webp\)<\/option><option value="apng">APNG \(\.png\)<\/option>/);
assert.match(css, /\.animated-thumb-form \{[^}]*grid-template-columns: 96px minmax\(0, 1fr\)/);
assert.match(css, /\.animated-thumb-form input\[type="number"\]/);
}
console.log('PASS: APNG structure, compact dialog, size fallback and single/group right-click wiring');