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.
This commit is contained in:
1 parent
84f48caaa1
commit
1866463b3d
8 files changed
+936
-1
No files matched your search
@@ -0,0 +1,161 @@
|
||||
// Arrastar-e-soltar de arquivos entre pastas no gerenciador.
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { createApp, FakeDataTransfer } = require('./harness');
|
||||
|
||||
test('arrastar um arquivo para uma pasta move o arquivo', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
const source = app.rowByName('raiz.txt');
|
||||
const target = app.treeRowByName('Documentos');
|
||||
assert.ok(source, 'arquivo na raiz deve aparecer na lista');
|
||||
assert.ok(target, 'pasta deve aparecer na árvore');
|
||||
|
||||
app.dragAndDrop(source, target);
|
||||
|
||||
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('o arrasto leva o id do arquivo no payload interno', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
const dt = app.dragAndDrop(app.rowByName('raiz.txt'), app.treeRowByName('Documentos'));
|
||||
|
||||
assert.strictEqual(dt.getData('application/x-notas-file'), 'file2');
|
||||
assert.strictEqual(dt.getData('text/plain'), 'raiz.txt');
|
||||
});
|
||||
|
||||
test('soltar na pasta em que o arquivo já está não chama a API', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
app.treeRowByName('Documentos').click();
|
||||
await app.settle();
|
||||
const source = app.rowByName('nota.txt');
|
||||
assert.ok(source, 'nota.txt deve estar na lista da pasta');
|
||||
|
||||
app.dragAndDrop(source, app.treeRowByName('Documentos'));
|
||||
|
||||
assert.strictEqual(app.requests('PUT', '/move').length, 0, 'nenhuma chamada de movementação');
|
||||
});
|
||||
|
||||
test('soltar na raiz move o arquivo para a raiz', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
app.treeRowByName('Documentos').click();
|
||||
await app.settle();
|
||||
app.dragAndDrop(app.rowByName('nota.txt'), app.treeRowByName('Raiz'));
|
||||
|
||||
const req = app.lastRequest('PUT', '/api/files/file1/move');
|
||||
assert.ok(req, 'deve chamar PUT /api/files/{id}/move');
|
||||
assert.deepStrictEqual(req.body, { folder: '' });
|
||||
});
|
||||
|
||||
test('drop de arquivo do sistema é ignorado', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
const dt = new FakeDataTransfer(); // sem mime interno: é um arquivo do disco
|
||||
app.fireDrag(app.rowByName('raiz.txt'), 'dragover', dt);
|
||||
app.fireDrag(app.treeRowByName('Documentos'), 'drop', dt);
|
||||
|
||||
assert.strictEqual(app.requests('PUT', '/move').length, 0, 'não deve mover nada');
|
||||
});
|
||||
|
||||
test('a pasta alvo é destacada durante o arrasto', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
const source = app.rowByName('raiz.txt');
|
||||
const target = app.treeRowByName('Documentos');
|
||||
const dt = new FakeDataTransfer();
|
||||
|
||||
app.fireDrag(source, 'dragstart', dt);
|
||||
assert.ok(source.classList.contains('dragging'), 'origem fica marcada como arrastando');
|
||||
|
||||
app.fireDrag(target, 'dragover', dt);
|
||||
assert.ok(target.classList.contains('drop-hl'), 'destino fica destacado');
|
||||
|
||||
app.fireDrag(source, 'dragend', dt);
|
||||
assert.ok(!target.classList.contains('drop-hl'), 'destaque é limpo ao fim do arrasto');
|
||||
assert.ok(!source.classList.contains('dragging'), 'origem volta ao normal');
|
||||
});
|
||||
|
||||
test('o destino da movimentação é informado ao usuário', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
app.dragAndDrop(app.rowByName('raiz.txt'), app.treeRowByName('Documentos'));
|
||||
await app.settle();
|
||||
|
||||
assert.match(app.toastText(), /Documentos/);
|
||||
});
|
||||
|
||||
test('após mover, o gerenciador recarrega a árvore', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
const before = app.requests('GET', '/api/files/tree').length;
|
||||
app.dragAndDrop(app.rowByName('raiz.txt'), app.treeRowByName('Documentos'));
|
||||
await app.settle();
|
||||
|
||||
assert.ok(app.requests('GET', '/api/files/tree').length > before, 'a árvore deve ser recarregada');
|
||||
});
|
||||
|
||||
test('a falha da API não deixa a UI afirmar sucesso', async (t) => {
|
||||
const app = createApp({
|
||||
routes: {
|
||||
'/api/files/file2/move': ({ json }) => json({ error: 'pasta inexistente' }, 400),
|
||||
},
|
||||
});
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
app.dragAndDrop(app.rowByName('raiz.txt'), app.treeRowByName('Documentos'));
|
||||
await app.settle();
|
||||
|
||||
assert.match(app.toastText(), /pasta inexistente/, 'o erro da API aparece para o usuário');
|
||||
assert.doesNotMatch(app.toastText(), /Movido para/, 'não pode anunciar sucesso');
|
||||
});
|
||||
|
||||
test('no modo seletor as linhas não são arrastáveis', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
|
||||
// abre o gerenciador como seletor (botão 📎 Anexo)
|
||||
app.document.querySelector('#btn-attach').click();
|
||||
await app.waitFor(() => app.listRows().length > 0);
|
||||
|
||||
for (const row of app.listRows()) {
|
||||
assert.strictEqual(row.draggable, false, 'linha do seletor não deve ser arrastável');
|
||||
}
|
||||
});
|
||||
|
||||
test('o arquivo continua vinculado à nota após mudar de pasta', async (t) => {
|
||||
const app = createApp();
|
||||
t.after(() => app.close());
|
||||
await app.openManager();
|
||||
|
||||
app.dragAndDrop(app.rowByName('raiz.txt'), app.treeRowByName('Documentos'));
|
||||
await app.settle();
|
||||
|
||||
// a movimentação não mexe no vínculo com a nota: só o id e a pasta mudam
|
||||
const req = app.lastRequest('PUT', '/api/files/file2/move');
|
||||
assert.deepStrictEqual(Object.keys(req.body), ['folder']);
|
||||
assert.strictEqual(req.url, '/api/files/file2/move', 'o id do arquivo é o mesmo, então a referência da nota continua válida');
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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\//);
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
// Harness de teste do frontend: sobe o app real (web/index.html + web/js/app.js)
|
||||
// em jsdom, com as chamadas de API interceptadas.
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..', '..');
|
||||
const INDEX = path.join(ROOT, 'web', 'index.html');
|
||||
const APPJS = path.join(ROOT, 'web', 'js', 'app.js');
|
||||
|
||||
// FakeDataTransfer: jsdom não implementa DataTransfer nem DragEvent.
|
||||
class FakeDataTransfer {
|
||||
constructor() {
|
||||
this._data = new Map();
|
||||
this.files = [];
|
||||
this.dropEffect = 'none';
|
||||
this.effectAllowed = 'all';
|
||||
}
|
||||
setData(type, value) { this._data.set(String(type), String(value)); }
|
||||
getData(type) { return this._data.has(String(type)) ? this._data.get(String(type)) : ''; }
|
||||
get types() { return [...this._data.keys()]; }
|
||||
clearData(type) {
|
||||
if (type) this._data.delete(String(type));
|
||||
else this._data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Fixture padrão: uma pasta com um arquivo, um arquivo na raiz e um anexo.
|
||||
function defaultFixtures() {
|
||||
return {
|
||||
tree: [
|
||||
{
|
||||
id: 'folder1', name: 'Documentos', parent: '', created_at: '2026-01-01T00:00:00Z',
|
||||
children: [],
|
||||
files: [
|
||||
{ id: 'file1', name: 'nota.txt', folder: 'folder1', note: '', size: 3, mime: 'text/plain', sha256: 'aa', created_at: '2026-01-01T00:00:00Z', url: '/api/files/static/file1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
rootFiles: [
|
||||
{ id: 'file2', name: 'raiz.txt', folder: '', note: 'note1', size: 4, mime: 'text/plain', sha256: 'bb', created_at: '2026-01-02T00:00:00Z', url: '/api/files/static/file2' },
|
||||
],
|
||||
notes: [{
|
||||
id: 'note1', title: 'Nota', format: 'md', categories: [], tags: [], content: 'oi',
|
||||
author: '', message: '', hash: 'h1',
|
||||
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria o ambiente do app.
|
||||
* @param {object} opts fixtures (tree/rootFiles/notes) e respostas customizadas.
|
||||
*/
|
||||
function createApp(opts = {}) {
|
||||
const fixtures = { ...defaultFixtures(), ...(opts.fixtures || {}) };
|
||||
const routes = opts.routes || {};
|
||||
const html = fs.readFileSync(INDEX, 'utf8');
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost:8080/',
|
||||
pretendToBeVisual: true,
|
||||
runScripts: 'outside-only',
|
||||
});
|
||||
const { window } = dom;
|
||||
|
||||
const calls = [];
|
||||
window.fetch = async (url, init = {}) => {
|
||||
const u = String(url);
|
||||
const method = init.method || 'GET';
|
||||
let body = init.body;
|
||||
if (typeof body === 'string') {
|
||||
try { body = JSON.parse(body); } catch { /* mantém texto */ }
|
||||
}
|
||||
calls.push({ url: u, method, body });
|
||||
const json = (data, status = 200) => ({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => data,
|
||||
text: async () => JSON.stringify(data),
|
||||
headers: { get: () => null },
|
||||
blob: async () => ({}),
|
||||
});
|
||||
for (const [pattern, responder] of Object.entries(routes)) {
|
||||
if (u.includes(pattern)) return responder({ url: u, method, body, json });
|
||||
}
|
||||
if (u.includes('/api/me')) return json({ id: 'u1', name: 'tester' });
|
||||
if (u.includes('/api/files/tree')) return json({ tree: fixtures.tree, rootFiles: fixtures.rootFiles });
|
||||
const noteMatch = u.match(/\/api\/notes\/([^/?]+)/);
|
||||
if (noteMatch) {
|
||||
const found = fixtures.notes.find((n) => n.id === noteMatch[1]);
|
||||
return json(found || {});
|
||||
}
|
||||
if (u.includes('/api/notes') && !u.includes('/api/notes?')) return json(fixtures.notes);
|
||||
if (u.includes('/api/tags')) return json([]);
|
||||
if (u.includes('/api/categories')) return json([]);
|
||||
return json({});
|
||||
};
|
||||
window.EventSource = class { constructor(u) { this.url = u; } close() {} addEventListener() {} removeEventListener() {} };
|
||||
if (!window.matchMedia) {
|
||||
window.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} });
|
||||
}
|
||||
window.prompt = () => (opts.promptAnswer === undefined ? null : opts.promptAnswer);
|
||||
window.confirm = () => true;
|
||||
window.scrollTo = () => {};
|
||||
|
||||
window.eval(fs.readFileSync(APPJS, 'utf8'));
|
||||
|
||||
const doc = window.document;
|
||||
const api = {
|
||||
window,
|
||||
document: doc,
|
||||
calls,
|
||||
fixtures,
|
||||
/** Espera a UI assentar (promessas resolvidas + render). */
|
||||
async settle(ms = 30) {
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
},
|
||||
/** Espera até a condição ser verdadeira (ou estoura). */
|
||||
async waitFor(fn, timeout = 2000) {
|
||||
const deadline = Date.now() + timeout;
|
||||
for (;;) {
|
||||
if (fn()) return;
|
||||
if (Date.now() > deadline) throw new Error('waitFor: condição não satisfeita');
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
},
|
||||
/** Abre o gerenciador de arquivos (botão da toolbar). */
|
||||
async openManager() {
|
||||
doc.querySelector('#btn-files').click();
|
||||
await api.waitFor(() => doc.querySelector('#files-overlay') && !doc.querySelector('#files-overlay').classList.contains('hidden'));
|
||||
await api.settle();
|
||||
return api;
|
||||
},
|
||||
/** Linhas de arquivo visíveis na lista. */
|
||||
listRows() {
|
||||
return [...doc.querySelectorAll('#files-list .fitem')];
|
||||
},
|
||||
/** Linha de arquivo pelo nome. */
|
||||
rowByName(name) {
|
||||
return api.listRows().find((r) => r.querySelector('.fitem-name').textContent.includes(name));
|
||||
},
|
||||
/** Linha da árvore pelo nome (pastas e Raiz). */
|
||||
treeRowByName(name) {
|
||||
return [...doc.querySelectorAll('#files-tree .tree-row')].find((r) => r.textContent.includes(name));
|
||||
},
|
||||
/** Dispara um evento de drag com DataTransfer simulado. */
|
||||
fireDrag(el, type, dt) {
|
||||
const ev = new window.Event(type, { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(ev, 'dataTransfer', { value: dt });
|
||||
el.dispatchEvent(ev);
|
||||
return ev;
|
||||
},
|
||||
/** Arrasta um elemento da origem até o destino (dragstart → dragover → drop → dragend). */
|
||||
dragAndDrop(source, target, dt = new FakeDataTransfer()) {
|
||||
api.fireDrag(source, 'dragstart', dt);
|
||||
api.fireDrag(target, 'dragover', dt);
|
||||
api.fireDrag(target, 'drop', dt);
|
||||
api.fireDrag(source, 'dragend', dt);
|
||||
return dt;
|
||||
},
|
||||
/** Requisições registradas com o método e filtro informados. */
|
||||
requests(method, needle = '') {
|
||||
return calls.filter((c) => c.method === method && c.url.includes(needle));
|
||||
},
|
||||
/** Última requisição que casa com método/filtro. */
|
||||
lastRequest(method, needle = '') {
|
||||
const list = api.requests(method, needle);
|
||||
return list.length ? list[list.length - 1] : null;
|
||||
},
|
||||
/** Texto do toast. */
|
||||
toastText() {
|
||||
const t = doc.querySelector('#toast');
|
||||
return t ? t.textContent.trim() : '';
|
||||
},
|
||||
close() {
|
||||
window.close();
|
||||
},
|
||||
};
|
||||
return api;
|
||||
}
|
||||
|
||||
module.exports = { createApp, FakeDataTransfer, defaultFixtures };
|
||||
Reference in new issue
Block a user