test(frontend): add jsdom tests for the file manager
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.
This commit is contained in:
lgcosta committed 2026-11-07 19:17:50 -03:00
1 parent 84f48caaa1
commit 1866463b3d
8 files changed
+936 -1

No files matched your search

+4 -1
View File
@@ -16,7 +16,8 @@ jobs:
cache: true
- uses: actions/setup-node@v4.1.0
with:
node-version: 20
node-version: 22
cache: npm
- name: gofmt
run: test -z "$(gofmt -l internal cmd)"
- name: go vet
@@ -25,3 +26,5 @@ jobs:
run: go test -race ./...
- name: frontend syntax
run: node --check web/js/app.js
- name: frontend tests
run: npm ci && npm test
+1
View File
@@ -3,3 +3,4 @@
.env
*.tmp
/CLAUDE-SECURITY-*/
node_modules/
+9
View File
@@ -405,4 +405,13 @@ go test ./... # store (revisões/GC/tags/árvore de categorias/migração),
# diffx (diff unificado), api (fluxo HTTP completo + CRUD de categorias)
go vet ./...
node --check web/js/app.js
npm ci # dependência de teste do frontend (jsdom)
npm test # testes do frontend em jsdom: sobe web/index.html + web/js/app.js
# de verdade, intercepta a API e exercita o gerenciador de
# arquivos (arrastar-e-soltar, mover/renomear/excluir, filtros,
# busca e preview de imagem/compactado)
```
Os testes de front-end rodam com o `node --test` nativo (sem runner extra) e
vivem em `frontend/tests/` — fora de `web/`, que é embutido no binário.
+161
View File
@@ -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');
});
+196
View File
@@ -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\//);
});
+184
View File
@@ -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 };
+369
View File
@@ -0,0 +1,369 @@
{
"name": "notas-online-frontend-tests",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "notas-online-frontend-tests",
"version": "0.0.0",
"devDependencies": {
"jsdom": "^31.0.0"
}
},
"node_modules/@exodus/bytes": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz",
"integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@noble/hashes": "^1.8.0 || ^2.0.0"
},
"peerDependenciesMeta": {
"@noble/hashes": {
"optional": true
}
}
},
"node_modules/bcp-47": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.2.tgz",
"integrity": "sha512-wsv86UuNFuPnVieKLyECTT+CP1WSAHeoIg5P3xu2kA9Z+7fVPoKt7dxzSeEhtJel0AI+NW3fIW3KtTKQKUCngg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-alphanumerical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/bcp-47-match": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
"integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/data-urls": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz",
"integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^17.1.2"
},
"engines": {
"node": "^22.14.0 || >=24.0.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true,
"license": "MIT"
},
"node_modules/entities": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz",
"integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/html-encoding-sniffer": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz",
"integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.15.1"
},
"engines": {
"node": "^22.13.0 || >=24.0.0"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-decimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsdom": {
"version": "31.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-31.0.0.tgz",
"integrity": "sha512-xAkhD9z1eyFC5+QUi9K24CaZJgdCQ2GOGLW1GdVDoQvkp/AxEH9TQ0pAplPOlIiV92hfcgnFOADZypbjgbBT6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.16.0",
"bcp-47": "^2.1.2",
"bcp-47-match": "^2.0.3",
"data-urls": "^8.0.0",
"decimal.js": "^10.6.0",
"html-encoding-sniffer": "^7.0.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.5.3",
"parse5": "^8.1.0",
"parse5-parser-stream": "^8.1.0",
"saxes": "^6.0.0",
"tough-cookie": "^6.0.2",
"undici": "^8.12.3",
"w3c-xmlserializer": "^6.0.0",
"webidl-conversions": "^8.0.1",
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^17.3.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
},
"peerDependencies": {
"@napi-rs/canvas": "^1.0.13"
},
"peerDependenciesMeta": {
"@napi-rs/canvas": {
"optional": true
}
}
},
"node_modules/lru-cache": {
"version": "11.5.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/parse5": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.1.tgz",
"integrity": "sha512-EcR/6WDcU7b2N3vQSxKALCk9lXGtUMQxXio008OEMBE0kavu/7JLfBEeFPzi43nMqVDSbEn3QCK+BMCc9zO//w==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^9.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-8.1.1.tgz",
"integrity": "sha512-hMcVbbUJcQmP8rURWUdYrT1flRiBiBVjq/RYGguJpDUgIXSqdYKlE0CxthmdMWoSlBAuL5bUBa73g0pttSy7Hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"parse5": "^8.1.1"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/tldts": {
"version": "7.4.19",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz",
"integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^7.4.19"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.4.20",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.20.tgz",
"integrity": "sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==",
"dev": true,
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.1.0.tgz",
"integrity": "sha512-FTxc8u8Kl2tz0Lat1u8ksvcy8bwHRyEMawwTWybo51JqaC6O7SvfuhAKg+Ld6VqyIe2QRJBzY9HA7y2WyFRT2w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz",
"integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": "^22.14.0 || >=24.0.0"
}
},
"node_modules/undici": {
"version": "8.12.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.4.tgz",
"integrity": "sha512-8Mx+uNKKER2bA4AdtedkqRbwUCIJFr2nJckXRe5yrDqzePqwLP7VNcctN8/XdmDSbF7mS+jJKIszlL+vbam1VA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/w3c-xmlserializer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz",
"integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-url": {
"version": "17.3.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz",
"integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.16.0",
"tr46": "^7.0.0",
"webidl-conversions": "^8.0.1"
},
"engines": {
"node": "^22.14.0 || >=24.0.0"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "notas-online-frontend-tests",
"private": true,
"version": "0.0.0",
"description": "Testes do frontend (jsdom) do notas-online",
"scripts": {
"test": "node --test frontend/tests/*.test.js"
},
"devDependencies": {
"jsdom": "^31.0.0"
}
}