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
476 changes: 476 additions & 0 deletions js/marker-detail-editing.js

Large diffs are not rendered by default.

500 changes: 42 additions & 458 deletions js/marker-detail-modal.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const APP_SHELL = [
'/js/category-customization.js',
'/js/commit-hash.js',
'/js/marker-detail-modal.js',
'/js/marker-detail-editing.js',
'/js/light-conditions-now.js',
'/js/light-page-view.js',
'/js/light-channel-view.js',
Expand Down
2 changes: 1 addition & 1 deletion tests/test-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ const _SAFE_HELPERS = new Set([
// is the markdown.js sanitized full renderer)
'escapeHTML', 'renderMarkdown',
]);
const _SWEEP_FILES = ['views.js', 'dashboard-page-view.js', 'category-page-view.js', 'category-view-renderers.js', 'category-customization.js', 'focus-card.js', 'marker-detail-modal.js', 'dashboard-widget-renderers.js', 'light-conditions-now.js', 'light-page-view.js', 'light-channel-view.js', 'light-sessions-view.js', 'light-device-setup-modal.js', 'sun-session-ui.js', 'compare-correlations.js', 'mobile-dashboard.js', 'context-card-editor-ui.js', 'context-card-medical-history-editor.js', 'chat.js', 'charts.js'];
const _SWEEP_FILES = ['views.js', 'dashboard-page-view.js', 'category-page-view.js', 'category-view-renderers.js', 'category-customization.js', 'focus-card.js', 'marker-detail-modal.js', 'marker-detail-editing.js', 'dashboard-widget-renderers.js', 'light-conditions-now.js', 'light-page-view.js', 'light-channel-view.js', 'light-sessions-view.js', 'light-device-setup-modal.js', 'sun-session-ui.js', 'compare-correlations.js', 'mobile-dashboard.js', 'context-card-editor-ui.js', 'context-card-medical-history-editor.js', 'chat.js', 'charts.js'];

function _sweepInnerHTML(filename, src) {
const lines = src.split('\n');
Expand Down
67 changes: 34 additions & 33 deletions tests/test-manual-entry-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ console.log('=== Manual Entry Flow Tests ===\n');

const viewsSrc = read('js/views.js');
const markerDetailSrc = read('js/marker-detail-modal.js');
const markerDetailEditingSrc = read('js/marker-detail-editing.js');

// ═══════════════════════════════════════
// 1. saveManualEntry is async (we await dialogs)
// ═══════════════════════════════════════
console.log('%c 1. Async save flow ', 'font-weight:bold;color:#f59e0b');

assert('saveManualEntry signature is async with opts arg',
/export async function saveManualEntry\(id, opts = \{\}\)/.test(markerDetailSrc));
/export async function saveManualEntry\(id, opts = \{\}\)/.test(markerDetailEditingSrc));
assert('saveAndAddAnotherManualEntry wraps saveManualEntry with keepOpen: true',
/saveAndAddAnotherManualEntry\(id\)[\s\S]{0,200}saveManualEntry\(id, \{ keepOpen: true \}\)/.test(markerDetailSrc));
/saveAndAddAnotherManualEntry\(id\)[\s\S]{0,200}saveManualEntry\(id, \{ keepOpen: true \}\)/.test(markerDetailEditingSrc));
assert('saveAndAddAnotherManualEntry bound to window',
viewsSrc.includes('saveAndAddAnotherManualEntry,'));

Expand All @@ -43,37 +44,37 @@ console.log('=== Manual Entry Flow Tests ===\n');
// feature (which introduces checkRefMax/checkRefMin to shadow with alt-unit
// ranges) doesn't break pattern pins. Intent is unchanged: a > 10x guard.
assert('Sanity check triggers when value > refMax * 10',
/value > \w*[Rr]ef[Mm]ax \* 10/.test(markerDetailSrc));
/value > \w*[Rr]ef[Mm]ax \* 10/.test(markerDetailEditingSrc));
// Greptile P2: without `> 0` guard, `refMax === 0` makes the multiplication
// zero and every positive value triggers the warning.
assert('Sanity check is guarded against refMax === 0 (no spurious warn)',
/(\w*[Rr]ef[Mm]ax) != null && \1 > 0 && value > \1 \* 10/.test(markerDetailSrc));
/(\w*[Rr]ef[Mm]ax) != null && \1 > 0 && value > \1 \* 10/.test(markerDetailEditingSrc));
assert('Sanity check triggers when value < refMin / 10 (and refMin > 0)',
/(\w*[Rr]ef[Mm]in) > 0 && value < \1 \/ 10/.test(markerDetailSrc));
/(\w*[Rr]ef[Mm]in) > 0 && value < \1 \/ 10/.test(markerDetailEditingSrc));
assert('Sanity check rejects negative values',
/value < 0\)\s*warn\s*=/.test(markerDetailSrc) || /if \(value < 0\)/.test(markerDetailSrc));
/value < 0\)\s*warn\s*=/.test(markerDetailEditingSrc) || /if \(value < 0\)/.test(markerDetailEditingSrc));
assert('Sanity-warn message mentions unit confusion',
/Did you enter the right unit\?/.test(markerDetailSrc));
/Did you enter the right unit\?/.test(markerDetailEditingSrc));
assert('Sanity check awaits showConfirmDialog and bails on cancel',
/if \(warn && !await showConfirmDialog\(`\$\{warn\}/.test(markerDetailSrc));
/if \(warn && !await showConfirmDialog\(`\$\{warn\}/.test(markerDetailEditingSrc));
assert('Sanity check uses marker.refMin/refMax (with optional alt-unit overlay)',
/marker\.refMin[\s\S]{0,200}marker\.refMax/.test(markerDetailSrc));
/marker\.refMin[\s\S]{0,200}marker\.refMax/.test(markerDetailEditingSrc));

// ═══════════════════════════════════════
// 3. Duplicate-date confirm
// ═══════════════════════════════════════
console.log('%c 3. Duplicate-date confirm ', 'font-weight:bold;color:#f59e0b');

assert('Duplicate check inspects existing entry for the chosen date',
/existingEntry\s*=\s*state\.importedData\.entries\?\.find\(e => e\.date === date\)/.test(markerDetailSrc));
/existingEntry\s*=\s*state\.importedData\.entries\?\.find\(e => e\.date === date\)/.test(markerDetailEditingSrc));
assert('Confirm dialog message includes existing value + unit + date',
/already exists for \$\{date\}\. Overwrite\?/.test(markerDetailSrc));
/already exists for \$\{date\}\. Overwrite\?/.test(markerDetailEditingSrc));
assert('Duplicate check uses display-unit value (marker.values[dateIdx]) not raw SI',
/const dateIdx = data\.dates\.indexOf\(date\)[\s\S]{0,300}marker\.values\[dateIdx\]/.test(markerDetailSrc));
/const dateIdx = data\.dates\.indexOf\(date\)[\s\S]{0,300}marker\.values\[dateIdx\]/.test(markerDetailEditingSrc));
assert('Manual overwrite remembers imported original for revert',
/function _rememberManualOriginal\(dotKey, date, entry\)/.test(markerDetailSrc) &&
/state\.importedData\.manualValues\[mvKey\] = hasImportedOriginal \? current : true/.test(markerDetailSrc) &&
/saveManualEntry[\s\S]{0,3500}_rememberManualOriginal\(dotKey, date, entry\)/.test(markerDetailSrc));
/function _rememberManualOriginal\(dotKey, date, entry\)/.test(markerDetailEditingSrc) &&
/state\.importedData\.manualValues\[mvKey\] = hasImportedOriginal \? current : true/.test(markerDetailEditingSrc) &&
/saveManualEntry[\s\S]{0,3500}_rememberManualOriginal\(dotKey, date, entry\)/.test(markerDetailEditingSrc));
assert('Clickable manual badge reverts to imported value when original exists',
/manual \\u00d7/.test(markerDetailSrc) &&
/Revert manual value to imported value/.test(markerDetailSrc) &&
Expand All @@ -85,9 +86,9 @@ console.log('=== Manual Entry Flow Tests ===\n');
console.log('%c 4. Save & Add Another ', 'font-weight:bold;color:#f59e0b');

assert('keepOpen branch re-opens the manual-entry form with same id + date',
/if \(keepOpen\)\s*\{[\s\S]{0,300}openManualEntryForm\(id, date\)/.test(markerDetailSrc));
/if \(keepOpen\)\s*\{[\s\S]{0,300}openManualEntryForm\(id, date\)/.test(markerDetailEditingSrc));
assert('keepOpen branch still navigate()s to refresh the underlying page',
/if \(keepOpen\)\s*\{[\s\S]{0,200}markerDetailDeps\.navigate\(navCat\)/.test(markerDetailSrc));
/if \(keepOpen\)\s*\{[\s\S]{0,200}markerDetailDeps\.navigate\(navCat\)/.test(markerDetailEditingSrc));
assert('Save & Add Another button rendered in form actions',
/Save\s*&amp;\s*Add Another|Save & Add Another/.test(markerDetailSrc));
assert('Save & Add Another button onclick calls saveAndAddAnotherManualEntry',
Expand All @@ -99,9 +100,9 @@ console.log('=== Manual Entry Flow Tests ===\n');
console.log('%c 5. Session last-date ', 'font-weight:bold;color:#f59e0b');

assert('saveManualEntry writes the chosen date to sessionStorage',
/sessionStorage\.setItem\('labcharts-last-manual-date', date\)/.test(markerDetailSrc));
/sessionStorage\.setItem\('labcharts-last-manual-date', date\)/.test(markerDetailEditingSrc));
assert('Write wrapped in try/catch (private-mode browsers)',
/try \{ sessionStorage\.setItem\('labcharts-last-manual-date'/.test(markerDetailSrc));
/try \{ sessionStorage\.setItem\('labcharts-last-manual-date'/.test(markerDetailEditingSrc));
assert('openManualEntryForm reads sessionLast and validates the format',
/sessionStorage\.getItem\('labcharts-last-manual-date'\)/.test(markerDetailSrc) &&
/\/\^\\d\{4\}-\\d\{2\}-\\d\{2\}\$\/\.test\(raw\)/.test(markerDetailSrc));
Expand All @@ -124,37 +125,37 @@ console.log('=== Manual Entry Flow Tests ===\n');
console.log('%c 6. Inline edit cancel + no-change ', 'font-weight:bold;color:#f59e0b');

assert('editMarkerValue declares a cancelled flag',
/let cancelled = false/.test(markerDetailSrc) ||
/editMarkerValue[\s\S]{0,1500}cancelled\s*=\s*false/.test(markerDetailSrc));
/let cancelled = false/.test(markerDetailEditingSrc) ||
/editMarkerValue[\s\S]{0,1500}cancelled\s*=\s*false/.test(markerDetailEditingSrc));
assert('save() short-circuits when cancelled is true',
/const save = async \(\) => \{[\s\S]{0,200}if \(cancelled\) return/.test(markerDetailSrc));
/const save = async \(\) => \{[\s\S]{0,200}if \(cancelled\) return/.test(markerDetailEditingSrc));
assert('Escape handler sets cancelled = true before re-rendering',
/else if \(e\.key === 'Escape'\) \{ cancelled = true; showDetailModal/.test(markerDetailSrc));
/else if \(e\.key === 'Escape'\) \{ cancelled = true; showDetailModal/.test(markerDetailEditingSrc));
assert("No-change save short-circuits (no manual flip on a same-value edit)",
/newValue === parseFloat\(currentValue\)\)/.test(markerDetailSrc));
/newValue === parseFloat\(currentValue\)\)/.test(markerDetailEditingSrc));
assert('Enter saves inline edits directly instead of relying on blur',
/if \(e\.key === 'Enter'\) \{ e\.preventDefault\(\); void save\(\); \}/.test(markerDetailSrc));
/if \(e\.key === 'Enter'\) \{ e\.preventDefault\(\); void save\(\); \}/.test(markerDetailEditingSrc));
assert('Inline edit guards against double saves from Enter + blur',
/let saveStarted = false/.test(markerDetailSrc) &&
/if \(saveStarted\) return;[\s\S]{0,80}saveStarted = true/.test(markerDetailSrc));
/let saveStarted = false/.test(markerDetailEditingSrc) &&
/if \(saveStarted\) return;[\s\S]{0,80}saveStarted = true/.test(markerDetailEditingSrc));
assert('Inline edit awaits persistence before refreshing the modal',
/await saveImportedData\(\);[\s\S]{0,160}markerDetailDeps\.navigate/.test(markerDetailSrc));
/await saveImportedData\(\);[\s\S]{0,160}markerDetailDeps\.navigate/.test(markerDetailEditingSrc));
assert('revertMarkerValue awaits persistence before refreshing the modal',
/export async function revertMarkerValue\(id, date\)[\s\S]{0,900}await saveImportedData\(\);[\s\S]{0,160}markerDetailDeps\.navigate/.test(markerDetailSrc));
/export async function revertMarkerValue\(id, date\)[\s\S]{0,900}await saveImportedData\(\);[\s\S]{0,160}markerDetailDeps\.navigate/.test(markerDetailEditingSrc));
assert('editMarkerValue calls injected navigate() to rebuild Table/Heatmap after save',
/editMarkerValue[\s\S]{0,2500}markerDetailDeps\.navigate\(state\.currentView \|\| 'dashboard'\)/.test(markerDetailSrc));
/editMarkerValue[\s\S]{0,2500}markerDetailDeps\.navigate\(state\.currentView \|\| 'dashboard'\)/.test(markerDetailEditingSrc));
assert('revertMarkerValue also calls injected navigate() to rebuild the underlying view',
/revertMarkerValue[\s\S]{0,1200}markerDetailDeps\.navigate\(state\.currentView \|\| 'dashboard'\)/.test(markerDetailSrc));
/revertMarkerValue[\s\S]{0,1200}markerDetailDeps\.navigate\(state\.currentView \|\| 'dashboard'\)/.test(markerDetailEditingSrc));

// ═══════════════════════════════════════
// 7. Input width fix
// ═══════════════════════════════════════
console.log('%c 7. Input width fix ', 'font-weight:bold;color:#f59e0b');

assert('Edit input uses width:100% with max-width:140px (replaces width:80px)',
/editMarkerValue[\s\S]{0,1500}width:100%;max-width:140px/.test(markerDetailSrc));
/editMarkerValue[\s\S]{0,1500}width:100%;max-width:140px/.test(markerDetailEditingSrc));
assert('Old width:80px input style removed from editMarkerValue',
!/editMarkerValue[\s\S]{0,1500}width:80px/.test(markerDetailSrc));
!/editMarkerValue[\s\S]{0,1500}width:80px/.test(markerDetailEditingSrc));

// ═══════════════════════════════════════
// 8. Add Value Manually placement (above Note) + rename
Expand Down
39 changes: 21 additions & 18 deletions tests/test-marker-value-notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ const state = (await import('../js/state.js')).state;
const viewsSrc = read('js/views.js');
const categoryViewRenderersSrc = read('js/category-view-renderers.js');
const markerDetailSrc = read('js/marker-detail-modal.js');
const markerDetailEditingSrc = read('js/marker-detail-editing.js');
assert('saveManualEntry reads me-note from the form',
/const\s+noteInput\s*=\s*document\.getElementById\('me-note'\)/.test(markerDetailSrc));
/const\s+noteInput\s*=\s*document\.getElementById\('me-note'\)/.test(markerDetailEditingSrc));
assert('saveManualEntry stores noteText in markerValueNotes when non-empty',
/if \(noteText\) state\.importedData\.markerValueNotes\[noteKey\] = noteText/.test(markerDetailSrc));
/if \(noteText\) state\.importedData\.markerValueNotes\[noteKey\] = noteText/.test(markerDetailEditingSrc));
assert('saveManualEntry clears the entry when noteText is empty (idempotent edit-to-blank)',
/else delete state\.importedData\.markerValueNotes\[noteKey\]/.test(markerDetailSrc));
/else delete state\.importedData\.markerValueNotes\[noteKey\]/.test(markerDetailEditingSrc));
assert('manual-entry form HTML includes the me-note textarea',
markerDetailSrc.includes('id="me-note"') && /placeholder=".*fasted/i.test(markerDetailSrc));

Expand All @@ -106,17 +107,17 @@ const state = (await import('../js/state.js')).state;
console.log('%c 5. Value-note CRUD handlers ', 'font-weight:bold;color:#f59e0b');

assert('editValueNote handler exported',
/export async function editValueNote\(id, date\)/.test(markerDetailSrc));
/export async function editValueNote\(id, date\)/.test(markerDetailEditingSrc));
assert('deleteValueNote handler exported',
/export async function deleteValueNote\(id, date\)/.test(markerDetailSrc));
/export async function deleteValueNote\(id, date\)/.test(markerDetailEditingSrc));
assert('editValueNote bound to window for inline onclicks',
/editValueNote,\s*$/m.test(viewsSrc) || viewsSrc.includes('editValueNote,'));
assert('deleteValueNote bound to window for inline onclicks',
viewsSrc.includes('deleteValueNote,'));
assert('editValueNote re-renders the detail modal on save',
/editValueNote[\s\S]{0,1500}showDetailModal\(id\)/.test(markerDetailSrc));
/editValueNote[\s\S]{0,1500}showDetailModal\(id\)/.test(markerDetailEditingSrc));
assert('deleteValueNote confirms before removing',
/deleteValueNote[\s\S]{0,400}showConfirmDialog\(/.test(markerDetailSrc));
/deleteValueNote[\s\S]{0,400}showConfirmDialog\(/.test(markerDetailEditingSrc));

// Direct state manipulation — verify the data model is what render code expects.
state.importedData = state.importedData || {};
Expand All @@ -133,7 +134,9 @@ const state = (await import('../js/state.js')).state;
console.log('%c 6. Orphan cleanup ', 'font-weight:bold;color:#f59e0b');

assert('deleteMarkerValue drops the per-value note for the same (date, marker)',
/deleteMarkerValue[\s\S]{0,2000}delete state\.importedData\.markerValueNotes\[dotKey \+ ':' \+ date\]/.test(markerDetailSrc));
/deleteMarkerValue[\s\S]{0,2000}delete state\.importedData\.markerValueNotes\[dotKey \+ ':' \+ date\]/.test(markerDetailEditingSrc));
assert('deleteMarkerValue drops mirrored insulin manualValues state',
/deleteMarkerValue[\s\S]{0,1200}delete state\.importedData\.manualValues\['diabetes\.insulin_d:' \+ date\]/.test(markerDetailEditingSrc));

// Insulin dual-mapping parity: the value mirrors hormones.insulin ↔
// diabetes.insulin_d, so the per-value note must mirror too. Bidirectional
Expand All @@ -142,27 +145,27 @@ const state = (await import('../js/state.js')).state;

// 500-char cap defends against runaway paste (matches the wearable note cap).
assert('saveManualEntry caps the note at 500 chars before storing',
/noteRaw\.length > 500 \? noteRaw\.slice\(0, 500\) : noteRaw/.test(markerDetailSrc));
/noteRaw\.length > 500 \? noteRaw\.slice\(0, 500\) : noteRaw/.test(markerDetailEditingSrc));
assert('editValueNote caps the note at 500 chars before storing',
/editValueNote[\s\S]{0,1200}result\.length > 500 \? result\.slice\(0, 500\) : result/.test(markerDetailSrc));
/editValueNote[\s\S]{0,1200}result\.length > 500 \? result\.slice\(0, 500\) : result/.test(markerDetailEditingSrc));

// editValueNote + deleteValueNote also route through _insulinMirrorNoteKey
// — see the bidirectional helper asserts below.
assert('deleteValueNote cleans the mirror note for insulin',
/deleteValueNote[\s\S]{0,800}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailSrc));
/deleteValueNote[\s\S]{0,800}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailEditingSrc));

// Greptile P1: insulin note mirror must be BIDIRECTIONAL — user may
// edit/delete via the hormones panel OR the diabetes panel.
assert('_insulinMirrorNoteKey helper defined and bidirectional',
/_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailSrc) &&
/if \(dotKey === 'hormones\.insulin'\) return 'diabetes\.insulin_d:' \+ date/.test(markerDetailSrc) &&
/if \(dotKey === 'diabetes\.insulin_d'\) return 'hormones\.insulin:' \+ date/.test(markerDetailSrc));
/_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailEditingSrc) &&
/if \(dotKey === 'hormones\.insulin'\) return 'diabetes\.insulin_d:' \+ date/.test(markerDetailEditingSrc) &&
/if \(dotKey === 'diabetes\.insulin_d'\) return 'hormones\.insulin:' \+ date/.test(markerDetailEditingSrc));
assert('saveManualEntry uses bidirectional mirror helper',
/saveManualEntry[\s\S]{0,2500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailSrc));
/saveManualEntry[\s\S]{0,2500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailEditingSrc));
assert('deleteMarkerValue uses bidirectional mirror helper',
/deleteMarkerValue[\s\S]{0,2500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailSrc));
/deleteMarkerValue[\s\S]{0,2500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailEditingSrc));
assert('editValueNote uses bidirectional mirror helper',
/editValueNote[\s\S]{0,1500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailSrc));
/editValueNote[\s\S]{0,1500}_insulinMirrorNoteKey\(dotKey, date\)/.test(markerDetailEditingSrc));

// CodeQL js/xss-through-dom: empty-cell onclick must use JSON.stringify
// so interpolated id/date survive the HTML-attr → JS-string round-trip.
Expand All @@ -181,7 +184,7 @@ const state = (await import('../js/state.js')).state;
assert('Populated card has × delete button',
/mv-value-note-delete[\s\S]{0,400}deleteValueNote\('/.test(markerDetailSrc));
assert('Inline onclicks stopPropagation so cell-edit doesn\'t fire',
/editValueNote[\s\S]{0,200}event\.stopPropagation\(\)/.test(markerDetailSrc));
/event\.stopPropagation\(\);editValueNote\('/.test(markerDetailSrc));

// ═══════════════════════════════════════
// 8. AI context emission — section:markerValueNotes
Expand Down
Loading