test: add jsdom render + persistence integration tests (#13)
- render.test.mjs drives the real UI under jsdom: employees tab renders and add works, planning tab renders the calendar grid (makeDayCell), generate fills slots, a manual slot change updates history, and a beforeunload flush persists state to localStorage; data tab renders the history view. - persistence.test.mjs seeds malformed persisted state (employee without constraints, out-of-range moderatorsPerDay) and asserts the startup load normalizes it without crashing. - jsdom added as the single devDependency; node_modules gitignored; both suites wired into npm test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
836d0eeda5
commit
189bcb6f38
6 files changed
+678
-2
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
const HTML = `<!DOCTYPE html><html><body>
|
||||
<button class="tab-btn active" data-tab="employees">Mitarbeiter</button>
|
||||
<button class="tab-btn" data-tab="planning">Monatsplanung</button>
|
||||
<button class="tab-btn" data-tab="data">Daten</button>
|
||||
<section id="tab-employees" class="tab-content"></section>
|
||||
<section id="tab-planning" class="tab-content hidden"></section>
|
||||
<section id="tab-data" class="tab-content hidden"></section>
|
||||
</body></html>`;
|
||||
|
||||
const dom = new JSDOM(HTML, { url: 'http://localhost/' });
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.localStorage = dom.window.localStorage;
|
||||
dom.window.alert = () => {};
|
||||
global.alert = () => {};
|
||||
|
||||
// Seed malformed persisted state BEFORE app.js loads:
|
||||
// - an employee with no constraints (would crash plan generation if not normalized)
|
||||
// - an out-of-range moderatorsPerDay
|
||||
localStorage.setItem('morning-planner-state', JSON.stringify({
|
||||
employees: [{ id: 'x', name: 'NoConstraints' }],
|
||||
calendar: { holidays: [], companyClosures: [] },
|
||||
history: [],
|
||||
settings: { moderatorsPerDay: 99 }
|
||||
}));
|
||||
|
||||
const { state } = await import('../js/app.js');
|
||||
|
||||
// Employee is loaded but normalized (constraints filled) so nothing crashes later
|
||||
assert.equal(state.employees.length, 1, 'employee loaded from storage');
|
||||
assert.ok(Array.isArray(state.employees[0].constraints.neverDays), 'missing constraints normalized');
|
||||
assert.equal(state.employees[0].constraints.minGapDays, 14, 'minGapDays defaulted');
|
||||
|
||||
// Invalid moderatorsPerDay (99) falls back to the valid default
|
||||
assert.equal(state.settings.moderatorsPerDay, 1, 'out-of-range setting falls back to 1');
|
||||
|
||||
// Rendering the employee tab with the normalized data does not throw
|
||||
assert.ok(document.getElementById('emp-name-input'), 'app rendered after recovering from malformed state');
|
||||
|
||||
console.log('All persistence tests passed.');
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
// Minimal DOM matching index.html's tab contract
|
||||
const HTML = `<!DOCTYPE html><html><body>
|
||||
<button class="tab-btn active" data-tab="employees">Mitarbeiter</button>
|
||||
<button class="tab-btn" data-tab="planning">Monatsplanung</button>
|
||||
<button class="tab-btn" data-tab="data">Daten</button>
|
||||
<section id="tab-employees" class="tab-content"></section>
|
||||
<section id="tab-planning" class="tab-content hidden"></section>
|
||||
<section id="tab-data" class="tab-content hidden"></section>
|
||||
</body></html>`;
|
||||
|
||||
const dom = new JSDOM(HTML, { url: 'http://localhost/' });
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.localStorage = dom.window.localStorage;
|
||||
dom.window.alert = () => {};
|
||||
global.alert = () => {};
|
||||
|
||||
const $ = sel => document.querySelector(sel);
|
||||
const tab = name => [...document.querySelectorAll('.tab-btn')].find(b => b.dataset.tab === name);
|
||||
const btnByText = (root, text) => [...root.querySelectorAll('button')].find(b => b.textContent === text);
|
||||
const fire = (el, type) => el.dispatchEvent(new dom.window.Event(type, { bubbles: true }));
|
||||
|
||||
// Importing app.js runs the startup load + renderEmployees()
|
||||
const { state } = await import('../js/app.js');
|
||||
|
||||
// --- Employees tab renders its input ---
|
||||
assert.ok($('#emp-name-input'), 'employee name input rendered');
|
||||
|
||||
// --- Adding an employee updates state and the list ---
|
||||
const input = $('#emp-name-input');
|
||||
input.value = 'Alice';
|
||||
btnByText(document.getElementById('tab-employees'), 'Hinzufügen').click();
|
||||
input.value = 'Bob';
|
||||
btnByText(document.getElementById('tab-employees'), 'Hinzufügen').click();
|
||||
assert.equal(state.employees.length, 2, 'two employees added to state');
|
||||
assert.ok(document.getElementById('employee-list').textContent.includes('Alice'), 'Alice listed');
|
||||
|
||||
// --- Planning tab renders a calendar grid (exercises makeDayCell) ---
|
||||
tab('planning').click();
|
||||
const dayCells = document.querySelectorAll('#calendar-wrapper .cal-day');
|
||||
assert.ok(dayCells.length > 0, 'calendar grid rendered day cells');
|
||||
|
||||
// --- Generating a plan fills slots ---
|
||||
btnByText(document.getElementById('tab-planning'), 'Plan generieren').click();
|
||||
const selects = [...document.querySelectorAll('#calendar-wrapper select')];
|
||||
const filled = selects.filter(s => s.value !== '');
|
||||
assert.ok(filled.length > 0, 'generate assigned moderators to slots');
|
||||
assert.ok(state.history.length > 0, 'generated assignments stored in history');
|
||||
|
||||
// --- Manual slot change persists as a manual entry ---
|
||||
const histBefore = state.history.length;
|
||||
const firstFilled = filled[0];
|
||||
const iso = state.history[0].date;
|
||||
// change first filled slot to "— Leer —"
|
||||
firstFilled.value = '';
|
||||
fire(firstFilled, 'change');
|
||||
assert.ok(state.history.length <= histBefore, 'clearing a slot removed its entry');
|
||||
|
||||
// --- Debounced save flushes on beforeunload and persists to localStorage ---
|
||||
fire(window, 'beforeunload');
|
||||
const saved = JSON.parse(localStorage.getItem('morning-planner-state'));
|
||||
assert.equal(saved.employees.length, 2, 'employees persisted to localStorage');
|
||||
assert.ok(saved.history.length > 0, 'history persisted to localStorage');
|
||||
|
||||
// --- Data tab renders the history table ---
|
||||
tab('data').click();
|
||||
assert.ok(document.getElementById('tab-data').textContent.includes('Verlauf'), 'data tab rendered');
|
||||
|
||||
console.log('All render tests passed.');
|
||||
Reference in new issue
Block a user