Files
lgcosta 1866463b3d
ci / check (push) Successful in 57s
test(frontend): add jsdom tests for the file manager
Sobe web/index.html + web/js/app.js de verdade em jsdom, intercepta a API
e cobre arrastar-e-soltar entre pastas, mover/renomear/excluir, filtros,
busca e preview. CI passa a rodar npm ci && npm test.
2026-11-07 19:17:50 -03:00

197 lines
7.1 KiB
JavaScript

// Comportamento do gerenciador de arquivos: ações, filtros, busca e preview.
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const { createApp } = require('./harness');
function actionButton(row, label) {
return [...row.querySelectorAll('.fitem-acts button')].find((b) => b.textContent.trim() === label);
}
test('a lista mostra os arquivos da pasta selecionada', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
assert.deepStrictEqual(app.listRows().map((r) => r.querySelector('.fitem-name').textContent), ['raiz.txt']);
app.treeRowByName('Documentos').click();
await app.settle();
assert.deepStrictEqual(app.listRows().map((r) => r.querySelector('.fitem-name').textContent), ['nota.txt']);
});
test('renomear envia o novo nome e mantém o id', async (t) => {
const app = createApp({ promptAnswer: 'renomeado.txt' });
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('raiz.txt'), '✎').click();
await app.settle();
const req = app.lastRequest('PUT', '/api/files/file2/move');
assert.ok(req, 'deve chamar PUT /api/files/{id}/move');
assert.strictEqual(req.url, '/api/files/file2/move');
assert.deepStrictEqual(req.body, { name: 'renomeado.txt' });
});
test('mover pelo botão de pasta escolhe o destino pelo índice informado', async (t) => {
const app = createApp({ promptAnswer: '1' }); // 0 = Raiz, 1 = Documentos
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('raiz.txt'), '📂').click();
await app.settle();
const req = app.lastRequest('PUT', '/api/files/file2/move');
assert.ok(req, 'deve chamar PUT /api/files/{id}/move');
assert.deepStrictEqual(req.body, { folder: 'folder1' });
});
test('associar o arquivo à nota atual envia o id da nota', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
// abre a nota para que exista uma aba com id (clicar no item da lista,
// nunca no botão de excluir que fica dentro dele)
const noteItem = [...app.document.querySelectorAll('#notes-list li')]
.find((li) => li.querySelector('.n-title'));
assert.ok(noteItem, 'a nota deve aparecer na lista lateral');
noteItem.click();
await app.settle();
const row = app.rowByName('raiz.txt');
const pin = actionButton(row, '📌');
assert.ok(pin, 'o botão de associar deve existir');
pin.click();
await app.settle();
const req = app.lastRequest('PUT', '/api/files/file2/move');
assert.ok(req, 'deve chamar PUT /api/files/{id}/move');
assert.deepStrictEqual(req.body, { note: 'note1' });
});
test('excluir pede confirmação e só apaga depois de confirmado', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('raiz.txt'), '✕').click();
await app.settle();
const modal = app.document.querySelector('#modal-overlay');
assert.ok(!modal.classList.contains('hidden'), 'o modal de confirmação deve abrir');
assert.strictEqual(app.requests('DELETE', '/api/files/file2').length, 0, 'não exclui antes de confirmar');
app.document.querySelector('#modal-ok').click();
await app.settle();
assert.strictEqual(app.requests('DELETE', '/api/files/file2').length, 1, 'exclui depois de confirmar');
});
test('cancelar a confirmação de exclusão não apaga nada', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('raiz.txt'), '✕').click();
await app.settle();
app.document.querySelector('#modal-cancel').click();
await app.settle();
assert.strictEqual(app.requests('DELETE', '/api/files/file2').length, 0, 'não deve excluir');
});
test('os filtros separam anexos de arquivos avulsos', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
const chip = (label) => [...app.document.querySelectorAll('#files-filters button')]
.find((b) => b.textContent.trim() === label);
// raiz.txt está vinculado a note1 → some no filtro "Avulsos"
chip('Avulsos').click();
await app.settle();
assert.deepStrictEqual(app.listRows(), [], 'nenhum avulso na raiz');
chip('Anexos').click();
await app.settle();
assert.deepStrictEqual(app.listRows().map((r) => r.querySelector('.fitem-name').textContent), ['raiz.txt']);
});
test('a busca filtra os arquivos por nome', async (t) => {
const app = createApp();
t.after(() => app.close());
await app.openManager();
const search = app.document.querySelector('#files-search');
search.value = 'raiz';
search.dispatchEvent(new app.window.Event('input', { bubbles: true }));
await app.settle(400);
assert.deepStrictEqual(app.listRows().map((r) => r.querySelector('.fitem-name').textContent), ['raiz.txt']);
search.value = 'nada-encontrado';
search.dispatchEvent(new app.window.Event('input', { bubbles: true }));
await app.settle(400);
assert.deepStrictEqual(app.listRows(), [], 'busca sem resultado limpa a lista');
});
test('criar pasta envia nome e pai', async (t) => {
const app = createApp({ promptAnswer: 'Novos' });
t.after(() => app.close());
await app.openManager();
app.document.querySelector('#files-newfolder').click();
await app.settle();
const req = app.lastRequest('POST', '/api/folders');
assert.ok(req, 'deve chamar POST /api/folders');
assert.deepStrictEqual(req.body, { name: 'Novos', parent: '' });
});
test('o preview de imagem abre o lightbox com a url do arquivo', async (t) => {
const app = createApp({
fixtures: {
rootFiles: [{ id: 'img1', name: 'foto.png', folder: '', note: '', size: 10, mime: 'image/png', sha256: 'cc', created_at: '2026-01-02T00:00:00Z', url: '/api/files/static/img1' }],
},
});
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('foto.png'), '👁').click();
await app.settle();
const lightbox = app.document.querySelector('#lightbox');
assert.ok(!lightbox.classList.contains('hidden'), 'o lightbox deve abrir');
const img = lightbox.querySelector('img');
assert.ok(img, 'a imagem deve ser exibida');
assert.strictEqual(img.getAttribute('src'), '/api/files/static/img1');
});
test('o preview de compactado lista o conteúdo do arquivo', async (t) => {
const app = createApp({
fixtures: {
rootFiles: [{ id: 'zip1', name: 'backup.zip', folder: '', note: '', size: 10, mime: 'application/zip', sha256: 'dd', created_at: '2026-01-02T00:00:00Z', url: '/api/files/static/zip1' }],
},
routes: {
'/api/files/archive/zip1': ({ json }) => json({
entries: [{ name: 'docs/nota.txt', size: 16, dir: false }, { name: 'vazia/', size: 0, dir: true }],
total: 2,
truncated: false,
}),
},
});
t.after(() => app.close());
await app.openManager();
actionButton(app.rowByName('backup.zip'), '👁').click();
await app.settle();
const items = [...app.document.querySelectorAll('.archive-list li')];
assert.strictEqual(items.length, 2, 'o compactado lista os itens');
assert.match(items[0].textContent, /docs\/nota\.txt/);
assert.match(items[1].textContent, /vazia\//);
});