d94a11d466
RotatedImage: размер контейнера через clientWidth/Height (не getBoundingClientRect), чтобы cover при 90°/270° работал под zoom React Flow; убраны отладочные логи. Главное меню в dev: пункт «Вид» с DevTools (Ctrl+Shift+I без пустого application menu). Список проектов: project.list без лицензии; список подгружается при неактивной лицензии; ProjectPicker с подсказками; listProjects пропускает битые zip. Сохранение проектов: atomicReplace — замена zip без rm до commit; восстановление *.dnd.zip.tmp при старте; тесты. EditorApp: блокировка UI при открытых окнах презентации и пульта; стили оверлея. Made-with: Cursor
29 lines
1.2 KiB
TypeScript
29 lines
1.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { replaceFileAtomic } from './atomicReplace';
|
|
|
|
void test('replaceFileAtomic: replaces existing file without deleting before rename succeeds', async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-replace-'));
|
|
const finalPath = path.join(dir, 'out.bin');
|
|
const tmpPath = path.join(dir, 'out.bin.tmp');
|
|
await fs.writeFile(finalPath, 'previous', 'utf8');
|
|
await fs.writeFile(tmpPath, 'updated-content', 'utf8');
|
|
await replaceFileAtomic(tmpPath, finalPath);
|
|
assert.equal(await fs.readFile(finalPath, 'utf8'), 'updated-content');
|
|
await assert.rejects(() => fs.stat(tmpPath));
|
|
});
|
|
|
|
void test('replaceFileAtomic: rejects empty source', async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-replace-'));
|
|
const finalPath = path.join(dir, 'out.bin');
|
|
const tmpPath = path.join(dir, 'out.bin.tmp');
|
|
await fs.writeFile(finalPath, 'x', 'utf8');
|
|
await fs.writeFile(tmpPath, '', 'utf8');
|
|
await assert.rejects(() => replaceFileAtomic(tmpPath, finalPath));
|
|
assert.equal(await fs.readFile(finalPath, 'utf8'), 'x');
|
|
});
|