Support resumable game folder uploads and remove DLC declarations
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { selectGameFolder } from '../../src/lib/ui/game-folder.js';
|
||||
|
||||
function selectedFile(path: string, size = 10) {
|
||||
const read = vi.fn(() => { throw new Error('Folder selection must not read file bodies.'); });
|
||||
const file = {
|
||||
name: path.split('/').at(-1), webkitRelativePath: path, size, lastModified: 1_700_000_000_000,
|
||||
arrayBuffer: read, text: read, stream: read, slice: read
|
||||
} as unknown as File;
|
||||
return { file, read };
|
||||
}
|
||||
|
||||
describe('game folder selection', () => {
|
||||
it('removes the selected root exactly once while preserving nested game paths', () => {
|
||||
const first = selectedFile('T6/T6/zone/english/base.ff', 20);
|
||||
const second = selectedFile('T6/main/iw_00.iwd', 30);
|
||||
const result = selectGameFolder([first.file, second.file]);
|
||||
expect(result.name).toBe('T6');
|
||||
expect(result.size).toBe(50);
|
||||
expect(result.files).toEqual([
|
||||
{ path: 'T6/zone/english/base.ff', file: first.file },
|
||||
{ path: 'main/iw_00.iwd', file: second.file }
|
||||
]);
|
||||
expect(first.read).not.toHaveBeenCalled();
|
||||
expect(second.read).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prepares multi-gigabyte and empty files using metadata alone', () => {
|
||||
const large = selectedFile('Game/zone/base.ff', 7 * 1024 ** 3);
|
||||
const empty = selectedFile('Game/empty.txt', 0);
|
||||
const result = selectGameFolder([large.file, empty.file]);
|
||||
expect(result.size).toBe(7 * 1024 ** 3);
|
||||
expect(result.files.map(item => item.path)).toEqual(['empty.txt', 'zone/base.ff']);
|
||||
expect(result.files[1].file).toBe(large.file);
|
||||
expect(large.read).not.toHaveBeenCalled();
|
||||
expect(empty.read).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects case-insensitive duplicate file names', () => {
|
||||
expect(() => selectGameFolder([
|
||||
selectedFile('Game/Zone/base.ff').file,
|
||||
selectedFile('Game/zone/BASE.ff').file
|
||||
])).toThrow('duplicate game paths');
|
||||
});
|
||||
|
||||
it('rejects a path used as both a file and a directory regardless of case', () => {
|
||||
expect(() => selectGameFolder([
|
||||
selectedFile('Game/zone/english/base.ff').file,
|
||||
selectedFile('Game/ZONE').file
|
||||
])).toThrow('same game path for a file and a directory');
|
||||
});
|
||||
|
||||
it.each(['../base.ff', './base.ff', '/Game/base.ff', 'base.ff'])(
|
||||
'rejects an absent or parent root in %s', path => {
|
||||
expect(() => selectGameFolder([selectedFile(path).file])).toThrow('one game folder');
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['Game/../base.ff', 'Game/zone/./base.ff', 'Game/zone//base.ff', 'Game/zone\\base.ff'])(
|
||||
'rejects traversal and non-normalized content paths in %s', path => {
|
||||
expect(() => selectGameFolder([selectedFile(path).file])).toThrow('unsupported game path');
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects files from multiple selected roots', () => {
|
||||
expect(() => selectGameFolder([
|
||||
selectedFile('T5/main/base.ff').file,
|
||||
selectedFile('T6/main/base.ff').file
|
||||
])).toThrow('one game folder');
|
||||
});
|
||||
|
||||
it('rejects empty selections', () => {
|
||||
expect(() => selectGameFolder([])).toThrow('contains no files');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user