diff --git a/app.js b/app.js index 8d0b744..5ed9a3a 100644 --- a/app.js +++ b/app.js @@ -796,6 +796,25 @@ const I18N = { const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; +function createUuid() { + if (typeof window.crypto?.randomUUID === "function") { + return window.crypto.randomUUID(); + } + + const bytes = new Uint8Array(16); + if (typeof window.crypto?.getRandomValues === "function") { + window.crypto.getRandomValues(bytes); + } else { + for (let index = 0; index < bytes.length; index += 1) { + bytes[index] = Math.floor(Math.random() * 256); + } + } + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + function mapApiOwner(record) { return { id: record.id, @@ -1654,7 +1673,7 @@ function calculateForm() { function resetStayForm(ownerId = owners[0]?.id || "") { $("#stayForm").reset(); - stayIdempotencyKey = IS_API_MODE ? window.crypto.randomUUID() : null; + stayIdempotencyKey = IS_API_MODE ? createUuid() : null; $("#ownerSelect").value = String(ownerId); $("#confirmationInput").value = ""; $("#checkinInput").value = "2026-08-01"; diff --git a/index.html b/index.html index b5af7cb..f826502 100644 --- a/index.html +++ b/index.html @@ -337,6 +337,6 @@ - + diff --git a/tests/browser-uuid.test.mjs b/tests/browser-uuid.test.mjs new file mode 100644 index 0000000..3ef2056 --- /dev/null +++ b/tests/browser-uuid.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import vm from "node:vm"; + +const appSource = await readFile(new URL("../app.js", import.meta.url), "utf8"); +const helperStart = appSource.indexOf("function createUuid"); +const helperEnd = appSource.indexOf("\n\nfunction mapApiOwner", helperStart); +assert.notEqual(helperStart, -1, "createUuid helper should exist"); +assert.notEqual(helperEnd, -1, "createUuid helper should end before API mappers"); + +function loadCreateUuid(windowObject) { + const context = { window: windowObject }; + vm.runInNewContext(`${appSource.slice(helperStart, helperEnd)}\nglobalThis.createUuid = createUuid;`, context); + return context.createUuid; +} + +test("UUID generation falls back when crypto.randomUUID is unavailable", () => { + let next = 0; + const createUuid = loadCreateUuid({ + crypto: { + getRandomValues(bytes) { + for (let index = 0; index < bytes.length; index += 1) { + bytes[index] = next; + next = (next + 17) % 256; + } + return bytes; + } + } + }); + + assert.match( + createUuid(), + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + ); +}); + +test("UUID generation uses native randomUUID when it is available", () => { + const createUuid = loadCreateUuid({ + crypto: { + randomUUID() { + return "00000000-0000-4000-8000-000000000123"; + } + } + }); + + assert.equal(createUuid(), "00000000-0000-4000-8000-000000000123"); +});