Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions tests/lib/dataIdHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Extracted ensureDataIds + generateUniqueId for testing.
// Ref: #137 — ensureDataIds idempotency tests.
// Source: JavaScript.html L881-900 (App.ensureDataIds, App.generateUniqueId).
//
// The original `ensureDataIds` uses `this.generateUniqueId()` for ID generation.
// This extraction accepts an optional `idGenerator` parameter (DI) so tests
// can inject a deterministic generator for reproducible assertions.
//
// Key behavioral contracts preserved:
// - Traverses scheduleData → classroom → day → classItem
// - Only adds ID when classItem exists AND has no `.id`
// - Returns scheduleData (mutates in place)
// - Null/falsy scheduleData → returns {}
// - Null/falsy classroom/daySchedule → skipped gracefully

/**
* Generate a unique ID (mirrors App.generateUniqueId).
* Original: JavaScript.html L898-900.
*
* @returns {string} A time-based + random ID string.
*/
export function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
}

/**
* Ensure every class item in scheduleData has an `id` property.
*
* Original: App.ensureDataIds (JavaScript.html L881-896).
* Mutates in place — items without `.id` get one assigned.
* Items already having `.id` are left unchanged (idempotent).
*
* @param {object|null|undefined} scheduleData - The schedule data object.
* Shape: { [classroom: string]: { [day: string]: Array<{ id?: string, ... }> } }
* @param {function} [idGenerator=generateUniqueId] - DI for ID generation.
* @returns {object} The (possibly mutated) scheduleData, or {} if falsy input.
*/
export function ensureDataIds(scheduleData, idGenerator = generateUniqueId) {
if (!scheduleData) return {};
Object.values(scheduleData).forEach(classroom => {
if (!classroom) return;
Object.values(classroom).forEach(daySchedule => {
if (Array.isArray(daySchedule)) {
daySchedule.forEach(classItem => {
if (classItem && !classItem.id) {
classItem.id = idGenerator();
}
});
}
});
});
return scheduleData;
}
107 changes: 107 additions & 0 deletions tests/lib/undoRedoHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Extracted Undo/Redo stack logic for testing.
// Ref: #135 — Undo/Redo state integrity tests.
// Source: History.js.html L2-62 (createHistoryModule: saveState, undo, redo, resetHistory).
//
// The original is a closure-based module factory (createHistoryModule) with
// private `history` array and `historyIndex`. This extraction exposes the
// stack management as a pure-ish class with explicit state, making it
// testable without DOM or app object dependencies.
//
// Key behavioral contracts preserved:
// - saveState: dedup identical consecutive states, truncate redo branch,
// cap at 50 entries (shift oldest)
// - undo: decrement index if > 0, load state via structuredClone
// - redo: increment index if < length-1, load state via structuredClone
// - resetHistory: replace stack with single current state, index = 0

/**
* Create a testable history module with injected dependencies.
*
* Original: createHistoryModule(app) in History.js.html L2-120.
*
* @param {object} opts
* @param {function} opts.getCurrentState - Returns { classrooms, scheduleData, tags } snapshot.
* @param {function} [opts.onLoadState] - Called with (state) when undo/redo loads a state.
* @param {function} [opts.onUpdateButtons] - Called after stack changes.
* @param {function} [opts.onCheckDirty] - Called after stack changes.
* @param {function} [opts.onUpdateCleanSnapshot] - Called on resetHistory.
* @returns {object} History module with saveState, undo, redo, resetHistory, getStack, getIndex.
*/
export function createTestableHistoryModule(opts) {
const {
getCurrentState,
onLoadState = () => {},
onUpdateButtons = () => {},
onCheckDirty = () => {},
onUpdateCleanSnapshot = () => {},
} = opts;

let history = [];
let historyIndex = -1;

const module = {
saveState() {
const currentState = getCurrentState();
// Dedup: skip if identical to last entry
if (history.length > 0 && JSON.stringify(currentState) === JSON.stringify(history[historyIndex])) {
return;
}

// Truncate redo branch
history = history.slice(0, historyIndex + 1);
history.push(currentState);
historyIndex = history.length - 1;

// Cap at 50 entries
if (history.length > 50) {
history.shift();
historyIndex--;
}

onUpdateButtons();
onCheckDirty();
},

resetHistory() {
const initialState = getCurrentState();
history = [initialState];
historyIndex = 0;
onUpdateButtons();
onUpdateCleanSnapshot();
onCheckDirty();
},

undo() {
if (historyIndex > 0) {
historyIndex--;
onLoadState(history[historyIndex]);
}
},

redo() {
if (historyIndex < history.length - 1) {
historyIndex++;
onLoadState(history[historyIndex]);
}
},

// Test-only accessors
getStack() {
return history;
},

getIndex() {
return historyIndex;
},

canUndo() {
return historyIndex > 0;
},

canRedo() {
return historyIndex < history.length - 1;
},
};

return module;
}
Loading
Loading