ci / check (push) Successful in 57s
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.
197 lines
7.1 KiB
JavaScript
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\//);
|
|
});
|