// Tests for makeElementId — ID generation for design elements. // // What this guards // ──────────────── // makeElementId is called from inside hot paths: every addElement, // every duplicateElement, every replaceElements iteration during // session restoration, every template-loading entry. A regression // that produced colliding ids would cause React key warnings, lost // drag/selection state when keys collide on re-render, and confusion // in undo/redo (history snapshots would have two entries with the // same id). // // We test: // • The id format is grep-able (starts with "element-") so existing // log filters and the diagnostic-history `summarizeElements` // helper continue to recognise our ids. // • Suffix-less ids are unique enough for non-loop callers. // • Suffix-mixed ids are unique even in a tight loop where // Date.now() can return the same millisecond for every iteration. // The suffix-uniqueness test is the regression test for the bug the // suffix parameter was added to fix — see the docblock in makeId.js. import { makeElementId } from './makeId'; describe('makeElementId', () => { describe('format', () => { it('produces an id starting with "element-"', () => { // Required so the `summarizeElements` helper in useDesignEditor's // diagnostic logging produces useful output, and so call sites // that filter logs by `[element-` find what they expect. expect(makeElementId()).toMatch(/^element-/); }); it('embeds the timestamp', () => { // Format: element--. Pinning the timestamp // position helps debugging — if an id appears in two different // snapshots, the timestamps tell you which mount produced it. const before = Date.now(); const id = makeElementId(); const after = Date.now(); // Extract the timestamp segment. const match = id.match(/^element-(\d+)-/); expect(match).not.toBeNull(); const ts = Number(match[1]); expect(ts).toBeGreaterThanOrEqual(before); expect(ts).toBeLessThanOrEqual(after); }); it('embeds the suffix when provided', () => { // Suffix lands between the timestamp and the random segment, so // a caller passing a loop index produces something like // `element-1779491484151-3-ip8fyoztd`. The position is documented // in the makeId.js docblock and visible in the history-log // snapshots from the May 22 crop+undo investigation. const id = makeElementId(3); expect(id).toMatch(/^element-\d+-3-[a-z0-9]+$/); }); it('handles string suffixes (not just numeric)', () => { // The signature accepts `number|string`. We don't currently have // a caller using a string suffix, but the public type allows it // so we test that it doesn't break. const id = makeElementId('slot'); expect(id).toMatch(/^element-\d+-slot-[a-z0-9]+$/); }); }); describe('uniqueness', () => { it('two suffix-less calls produce different ids', () => { // Date.now() may return the same value for two close calls on // fast machines, but the 9-char random suffix makes a collision // astronomically unlikely (2^46 entropy per id). const a = makeElementId(); const b = makeElementId(); expect(a).not.toBe(b); }); it('a tight loop produces all-unique ids when suffix is used (regression test)', () => { // The bug that motivated the suffix parameter: a `for` loop // creating elements in one tick produces multiple ids that // share the same Date.now() value. Pre-suffix, the random // segment alone made collisions rare-but-possible; the suffix // makes them impossible (each iteration has a different // suffix), even if Math.random somehow returned the same // sequence twice in a row. const ids = new Set(); for (let i = 0; i < 1000; i++) { ids.add(makeElementId(i)); } expect(ids.size).toBe(1000); }); it('suffix-less ids in a tight loop are very-likely-unique (statistical, not guaranteed)', () => { // 1000 ids without suffix. With ~2^46 random entropy, the // birthday-paradox collision probability at N=1000 is // approximately N^2 / (2 * 2^46) = 1.4e-8. Astronomically // unlikely, but pin the statistical expectation here so a // future regression that, say, dropped Math.random precision // or hardcoded a constant random suffix gets caught. const ids = new Set(); for (let i = 0; i < 1000; i++) { ids.add(makeElementId()); } expect(ids.size).toBe(1000); }); }); });