'use strict'; const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const test = require('node:test'); const { deriveResumeState, nextFocusIndex, normalizeFileSelection, validateWizardStep } = require('../../renderer/modules/import-wizard'); const { buildManifest, validateManifest } = require('../../src/services/portable-manifest-service'); const evidence = (value, source = 'test', confidence = 'high') => ({ value, source, confidence, observedAt: 1 }); function component(id, type, relativePath) { return { id, type: evidence(type), relativePath, size: 4, sha256: id[0].repeat(64), platform: 'ps4', titleId: evidence('CUSA12345'), contentId: evidence(`UP0001-CUSA12345_00-${id.slice(0, 16).toUpperCase()}`), version: evidence('1.00'), provenance: [{ source: 'staging-sha256', confidence: 'verified', observedAt: 1 }] }; } function classification() { const components = [component('a'.repeat(24), 'base', 'base.pkg'), component('b'.repeat(24), 'patch_bundle', 'patches/fix.zip'), component('c'.repeat(24), 'image', 'art/cover.png')]; return { status: 'classified', jobId: 'job', title: { id: evidence('CUSA12345'), platform: 'ps4' }, components, notes: [], recipeProposal: { installOrder: components.map((item, index) => ({ order: index + 1, componentId: item.id, componentType: item.type.value, action: 'proposal-only' })) } }; } test('folder and multi-file selections normalize relative names and reject empty files', () => { const selected = normalizeFileSelection([{ name: 'base.pkg', webkitRelativePath: 'Game/base.pkg', size: 4 }, { name: 'empty.txt', size: 0 }]); assert.deepEqual(selected.map(item => item.relativePath), ['Game/base.pkg']); }); test('resume progress uses durable server offsets', () => { assert.deepEqual(deriveResumeState({ state: 'uploading', files: [{ size: 8, offset: 4 }, { size: 2, bytesReceived: 2 }] }), { resumable: true, expected: 10, received: 6, percent: 60 }); assert.equal(deriveResumeState({ state: 'uploaded', files: [] }).resumable, false); }); test('keyboard focus wraps in both directions and validation errors are explicit', () => { assert.equal(nextFocusIndex(2, 3, false), 0); assert.equal(nextFocusIndex(0, 3, true), 2); assert.equal(validateWizardStep({ step: 0, selection: [], session: null }), 'importWizard.error.selection'); assert.equal(validateWizardStep({ step: 3, classification: { status: 'classified' }, destinationId: '' }), 'importWizard.error.destination'); assert.equal(validateWizardStep({ step: 4, classification: { status: 'classified' }, destinationId: 'library', confirmed: false }), 'importWizard.error.confirm'); }); test('editable order, non-executable notes, patch/APR and artwork selections persist in the manifest', () => { const source = classification(); const order = source.components.map(item => item.id).reverse(); const manifest = buildManifest(source, { titleName: 'Wizard', componentOrder: order, installNotes: 'Review first; never execute.', patchComponentIds: [source.components[1].id], artworkComponentIds: [source.components[2].id] }, 2); assert.deepEqual(manifest.recipe.installOrder.map(item => item.componentId), order); assert.equal(manifest.recipe.installNotes, 'Review first; never execute.'); assert.deepEqual(manifest.recipe.attachments.patches, [{ componentId: source.components[1].id, type: 'patch_bundle' }]); assert.deepEqual(manifest.recipe.attachments.artwork, [{ componentId: source.components[2].id, type: 'image' }]); assert.equal(manifest.recipe.notesExecutable, false); assert.deepEqual(validateManifest(manifest), { valid: true, errors: [] }); assert.throws(() => buildManifest(source, { componentOrder: [order[0], order[0], order[2]] }), error => error.code === 'MANIFEST_ORDER'); }); test('wizard is core-loaded with responsive styling and complete English/Dutch keys', () => { const root = path.resolve(__dirname, '../..'); const index = fs.readFileSync(path.join(root, 'renderer/index.html'), 'utf8'); const bootstrap = fs.readFileSync(path.join(root, 'renderer/bootstrap.js'), 'utf8'); const css = fs.readFileSync(path.join(root, 'renderer/styles/import-wizard.css'), 'utf8'); const source = fs.readFileSync(path.join(root, 'renderer/modules/import-wizard.js'), 'utf8'); const en = JSON.parse(fs.readFileSync(path.join(root, 'build/lang/en.json'), 'utf8')); const nl = JSON.parse(fs.readFileSync(path.join(root, 'build/lang/nl.json'), 'utf8')); assert.doesNotMatch(index, /styles\/import-wizard\.css/); assert.doesNotMatch(bootstrap, /modules\/import-wizard\.js/); assert.match(index, /type="module" src="modules\/import-wizard\.js"/); assert.match(source, /styles\/import-wizard\.css/); assert.match(css, /@media\(max-width:640px\)/); assert.match(source, /role=\"dialog\" aria-modal=\"true\"/); for (const key of ['importWizard.open', 'importWizard.resume', 'importWizard.reviewRequired', 'importWizard.finalizeAction']) { assert.equal(typeof en.strings[key], 'string'); assert.equal(typeof nl.strings[key], 'string'); } });