const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const root = path.resolve(__dirname,'..'); global.flipnote=vm.compileFunction(fs.readFileSync(path.join(root,'assets/vendor/flipnote.js'),'utf8')+'\nreturn flipnote;',['module'])(module); const importer = require('../assets/ppm-import.js'); const fixture = fs.readFileSync(path.join(__dirname,'fixtures/local-import.ppm')); const buffer = bytes => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); const bit = (encoded,index) => (Buffer.from(encoded,'base64')[index >> 3] >> (index & 7)) & 1; test('local import keeps both layers, per-page colours, paper and visibility',()=>{ const result=importer.decode(buffer(fixture),'Local.ppm'); assert.equal(result.project.frames.length,2); assert.equal(result.project.title,'Local'); assert.deepEqual(result.project.layer_visibility,[true,false]); assert.deepEqual(result.project.frames.map(frame=>frame.colours),[['black','red'],['blue','black']]); assert.deepEqual(result.project.frames.map(frame=>frame.paper),['white','black']); assert.equal(bit(result.project.frames[0].layer1,0),1); assert.equal(bit(result.project.frames[0].layer2,191*256+255),1); assert.equal(bit(result.project.frames[1].layer1,7*256+7),1); assert.equal(bit(result.project.frames[1].layer1,0),0); assert.equal(bit(result.project.frames[1].layer2,10*256+10),1); assert.equal(result.project.loop,true); }); test('audio and page sound triggers stay with an imported animation',()=>{ const {project}=importer.decode(buffer(fixture)); assert.equal(project.speed_level,6); assert.deepEqual(project.frames[0].sounds,{sound1:true,sound2:false,sound3:false}); assert.deepEqual(Object.keys(project.audio),['music','sound1']); for(const track of Object.values(project.audio)){ const wav=Buffer.from(track.data,'base64'); assert.equal(wav.toString('ascii',0,4),'RIFF'); assert.equal(wav.readUInt32LE(24),32768); assert.equal(track.reference_fps,12); assert.equal(track.duration,(wav.length-44)/2/32768); } }); test('music pitch follows the stored Flipnote and music speeds exactly once',()=>{ const normal=importer.decode(buffer(fixture)); const fast=Buffer.from(fixture); const animationEnd=0x6A0+fast.readUInt32LE(4); const soundStart=Math.ceil((animationEnd+2)/4)*4; fast[soundStart+16]=1; const faster=importer.decode(buffer(fast)); assert.equal(faster.project.speed_level,7); assert.equal(faster.project.audio.music.reference_fps,20); assert.ok(faster.project.audio.music.duration{ for(const mutate of [ bytes=>bytes.fill(0,0,4), bytes=>bytes.writeUInt32LE(0xFFFFFFFF,4), bytes=>bytes.writeUInt16LE(1000,12), bytes=>bytes.writeUInt16LE(0,0x6A0), bytes=>bytes.writeUInt32LE(0xFFFFFFFF,0x6A8), bytes=>bytes.writeUInt32LE(0xFFFFFFFF,8), ]){ const bad=Buffer.from(fixture);mutate(bad); assert.throws(()=>importer.decode(buffer(bad))); } assert.throws(()=>importer.decode(buffer(fixture.subarray(0,100)))); assert.throws(()=>importer.decode(new ArrayBuffer(1024*1024+1))); }); test('opening a device file uses its bytes without making a network request',async()=>{ const oldFetch=global.fetch; global.fetch=()=>{throw new Error('Unexpected network request');}; try{ const result=await importer.read({name:'Device.ppm',size:fixture.length,arrayBuffer:async()=>buffer(fixture)}); assert.equal(result.project.title,'Device'); await assert.rejects(importer.read({name:'not-a-note.txt',size:fixture.length})); await assert.rejects(importer.read({name:'large.ppm',size:1024*1024+1})); }finally{global.fetch=oldFetch;} });