const test = require('node:test'); const assert = require('node:assert/strict'); const vm = require('node:vm'); const fs = require('node:fs'); const path = require('node:path'); const { adminUserRow, autosaveConflictFragment, autosaveStatusFragment, editorFragment, escapeHtml, folderListItem, folderListFragment, folderNotesPageFragment, historyModalFragment, historySnapshotPreviewFragment, layoutPage, loggedOutPage, mfaPage, recoveryPage, mobileEditorFragment, mobileFoldersFragment, mobileNotesFragment, mobileSearchFragment, navigationFragment, noteListItem, noteListFragment, noteMetaFragment, noteMetaText, noteSyncStateFragment, renderInlineMarkdown, renderMarkdown, searchResultsFragment, settingsPage, stripMarkdownForTitle } = require('../app/templates'); test('autosaveConflictFragment wires overwrite and create copy actions', () => { const html = autosaveConflictFragment('n1'); assert.ok(html.includes('hx-put="/fragments/editor/n1"')); assert.ok(html.includes('hx-vals=\'{"forceSave":"1"}\'')); assert.ok(html.includes('hx-vals=\'{"createCopy":"1"}\'')); assert.ok(html.includes('hx-include="#note-editor-form"')); }); test('editorFragment shows restore action for trashed note', () => { const html = editorFragment({ id: 'n1', title: 'Deleted', body: 'Body', parentId: 'f1', deletedTime: 123, createdTime: 1000, updatedTime: 2000 }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(html.includes('hx-post="/fragments/notes/n1/restore"')); assert.ok(html.includes('Restore')); assert.ok(html.includes('Permanently delete this note?')); assert.ok(!html.includes('Move this note to trash?')); }); test('editorFragment shows trash delete prompt for active note', () => { const html = editorFragment({ id: 'n1', title: 'Active', body: 'Body', parentId: 'f1', deletedTime: 0, createdTime: 1000, updatedTime: 2000 }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(html.includes('Move this note to trash?')); assert.ok(!html.includes('Permanently delete this note?')); }); test('editorFragment includes date and datetime toolbar buttons', () => { const html = editorFragment({ id: 'n1', title: 'Active', body: 'Body', parentId: 'f1', deletedTime: 0, createdTime: 1000, updatedTime: 2000 }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(html.includes('title="Insert date"')); assert.ok(html.includes('title="Insert date and time"')); assert.ok(html.includes('insertStamp(\'date\')')); assert.ok(html.includes('insertStamp(\'datetime\')')); assert.ok(html.includes('id="markdown-toggle"')); assert.ok(html.includes('id="preview-toggle"')); assert.ok(html.includes('onclick="setEditorMode(\'markdown\')"')); assert.ok(html.includes('onclick="setEditorMode(\'preview\')"')); assert.ok(html.includes('title="Rendered Markdown"')); }); test('editorFragment hides lock toggle for plaintext notes', () => { const html = editorFragment({ id: 'n1', title: 'Active', body: 'Body', parentId: 'f1', deletedTime: 0, createdTime: 1000, updatedTime: 2000, isEncrypted: false }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(!html.includes('id="lock-toggle-btn"')); }); test('editorFragment shows lock toggle for encrypted notes', () => { const html = editorFragment({ id: 'n1', title: 'Active', body: 'Body', parentId: 'f1', deletedTime: 0, createdTime: 1000, updatedTime: 2000, isEncrypted: true }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(html.includes('id="lock-toggle-btn"')); assert.ok(html.includes('title="Unlock note"')); }); test('editorFragment hides plaintext body for vault-protected notes', () => { const html = editorFragment({ id: 'n1', title: 'Active', body: 'Top secret', parentId: 'vault-1', deletedTime: 0, createdTime: 1000, updatedTime: 2000, isEncrypted: false, inVault: true, vaultId: 'vault-1', vaultTitle: 'Vault 1' }, [{ id: 'vault-1', title: 'Vault 1' }]); assert.ok(html.includes('id="lock-toggle-btn"')); assert.ok(html.includes('data-encrypted="1"')); assert.ok(html.includes('data-vault-id="vault-1"')); assert.ok(html.includes('id="note-body" style="display:none">Top secret')); assert.ok(html.includes('id="editor-toolbar" style="display:none"')); assert.ok(html.includes('id="note-preview"')); assert.ok(html.includes('contenteditable="true"')); assert.ok(html.includes('style="display:none"')); assert.ok(html.includes('id="cm-host" style="display:none"')); assert.ok(!html.includes('>Top secret')); assert.ok(html.includes('Vault: Vault 1')); assert.ok(!html.includes('editor-locked-back')); }); test('mobileEditorFragment hides plaintext preview for vault-protected notes', () => { const html = mobileEditorFragment({ id: 'n1', title: 'Active', body: 'Top secret', parentId: 'vault-1', deletedTime: 0, createdTime: 1000, updatedTime: 2000, isEncrypted: false, inVault: true, vaultId: 'vault-1', vaultTitle: 'Vault 1' }, [{ id: 'vault-1', title: 'Vault 1' }]); assert.ok(html.includes('id="editor-locked"')); assert.ok(html.includes('id="note-body" style="display:none">Top secret')); assert.ok(html.includes('id="editor-toolbar" style="display:none"')); assert.ok(html.includes('id="note-preview"')); assert.ok(html.includes('contenteditable="true"')); assert.ok(html.includes('style="display:none"')); assert.ok(html.includes('id="cm-host" style="display:none"')); assert.ok(!html.includes('>Top secret')); }); test('app script enforces single-screen mobile invariant via state machine', () => { const appJs = fs.readFileSync(path.join(__dirname, '../public/app.js'), 'utf8'); // State machine + reducer assert.ok(appJs.includes('function setMobileState(patch)'), 'has setMobileState reducer'); assert.ok(appJs.includes('function renderMobile()'), 'has renderMobile()'); assert.ok(appJs.includes('function assertSingleActiveScreen()'), 'has invariant assertion'); // renderMobile + the self-heal path in assertSingleActiveScreen are the ONLY togglers. const matches = appJs.match(/classList\.toggle\(['"]mobile-screen-active['"]/g) || []; assert.ok(matches.length >= 1 && matches.length <= 2, 'mobile-screen-active toggled only in render/self-heal (got '+matches.length+')'); // Old fragile API removed assert.ok(!appJs.includes('function showMobileScreen('), 'showMobileScreen removed'); assert.ok(!appJs.includes('function syncMobileFabVisibility('), 'syncMobileFabVisibility folded into renderMobile'); }); test('navigationFragment shows trash folder empty action', () => { const html = navigationFragment([{ id: 'de1e7ede1e7ede1e7ede1e7ede1e7ede', title: 'Trash', parentId: '' }], [], '', ''); assert.ok(html.includes('hx-post="/fragments/trash/empty"')); assert.ok(html.includes('Empty trash permanently?')); assert.ok(html.includes('🗑')); }); test('navigationFragment shows virtual all notes without notebook actions', () => { // In lazy mode, pass a counts Map — notes are NOT rendered inline const counts = new Map([['__all__', 1], ['f1', 1], ['__trash__', 0]]); const html = navigationFragment([ { id: '__all_notes__', title: 'All Notes', parentId: '', isVirtualAllNotes: true }, { id: 'f1', title: 'Folder 1', parentId: '' }, ], counts, '__all_notes__', 'n1', '', '__all_notes__'); assert.ok(html.includes('All Notes')); assert.ok(!html.includes('openFolderContextMenu(event,\'__all_notes__\'')); assert.ok(!html.includes('hx-vals=\'{"parentId":"__all_notes__"}\'')); }); test('navigationFragment only marks the selected note in the clicked context as active (folderNotesPageFragment)', () => { // Notes are lazy-loaded; test folderNotesPageFragment directly const html = folderNotesPageFragment( [{ id: 'n1', title: 'Note 1', parentId: 'f1', deletedTime: 0 }], '__all_notes__', 'n1', false, 1, 1, ); assert.ok(html.includes('id="note-item-__all_notes__-n1" class="notelist-item active"')); const html2 = folderNotesPageFragment( [{ id: 'n1', title: 'Note 1', parentId: 'f1', deletedTime: 0 }], 'f1', '', false, 1, 1, ); assert.ok(html2.includes('id="note-item-f1-n1" class="notelist-item"')); assert.ok(!html2.includes('class="notelist-item active"')); }); test('navigationFragment shows Search Results folder when query is active', () => { const html = navigationFragment([ { id: 'f1', title: 'Folder 1', parentId: '' }, { id: 'f2', title: 'Folder 2', parentId: '' }, ], [ { id: 'n1', title: 'Note 1', parentId: 'f1' }, ], '', '', 'note'); assert.ok(html.includes('Search Results')); assert.ok(!html.includes('Folder 1')); assert.ok(!html.includes('Folder 2')); assert.ok(html.includes('Note 1')); }); test('navigationFragment does not make empty folders expandable', () => { const html = navigationFragment([ { id: 'f1', title: 'Folder 1', parentId: '' }, ], [], '', ''); assert.ok(html.includes('nav-folder-empty')); assert.ok(!html.includes('onclick="toggleNavFolder(\'f1\')"')); assert.ok(html.includes('nav-folder-toggle-placeholder')); }); test('navigationFragment includes shared folder context menu and modal', () => { const html = navigationFragment([{ id: 'f1', title: 'Folder 1', parentId: '' }], [], '', ''); assert.ok(html.includes('oncontextmenu="openFolderContextMenu(event,\'f1\',\'Folder 1\')"')); assert.ok(html.includes('id="folder-context-menu"')); assert.ok(html.includes('Edit notebook')); assert.ok(html.includes('Delete notebook')); assert.ok(html.includes('id="folder-modal"')); assert.ok(html.includes('onsubmit="submitFolderEdit(event)"')); }); test('renderMarkdown rewrites raw html resource images without self-closing slash', () => { const html = renderMarkdown('x'); assert.ok(html.includes('src="/resources/49a3f012f300473d98a33b97940306b1"')); assert.ok(html.includes('width="313"')); assert.ok(html.includes('height="417"')); }); test('renderMarkdown opens resource links in another tab', () => { const html = renderMarkdown('[Manual](:/49a3f012f300473d98a33b97940306b1)'); assert.ok(html.includes('href="/resources/49a3f012f300473d98a33b97940306b1"')); assert.ok(html.includes('target="_blank"')); assert.ok(html.includes('rel="noopener"')); }); test('renderMarkdown handles backticks inside fenced code blocks', () => { const md = '```\n.-```-.\ntest\n```\n\n```\nblock2\n```'; const html = renderMarkdown(md); const preCount = (html.match(/
 {
	const md = '   ```\n   indented fence\n   ```';
	const html = renderMarkdown(md);
	assert.ok(html.includes(' {
	const md = '```\nbody\n```   ';
	const html = renderMarkdown(md);
	assert.ok(html.includes(' {
	const md = '- ```\n      Containers docker container rm $(docker container ls -a -q) -f\n  ```\n- ```\n      Images docker image rm $(docker image ls -a -q) -f\n  ```';
	const html = renderMarkdown(md);
	const preCount = (html.match(/
'), 'should still wrap items in 
    '); const liCount = (html.match(/
  • /g) || []).length; assert.strictEqual(liCount, 2, 'should still produce two
  • items'); assert.ok(html.includes('Containers docker container rm'), 'first code block content preserved'); assert.ok(html.includes('Images docker image rm'), 'second code block content preserved'); // The user-reported bug: bare ``` should not leak through as inline code or text assert.ok(!html.includes('```') && !html.includes('```'), 'no raw triple-backticks should leak through'); }); test('renderMarkdown handles fenced code block in single list item with language tag', () => { const md = '- ```bash\n echo hi\n ```'; const html = renderMarkdown(md); assert.ok(html.includes('') && html.includes('
  • '), 'list structure preserved'); }); test('renderMarkdown renders GFM tables', () => { const md = '| Name | Value |\n|------|-------|\n| foo | bar |'; const html = renderMarkdown(md); assert.ok(html.includes(''), 'should produce a table element'); assert.ok(html.includes(''), 'should produce a thead'); assert.ok(html.includes(''), 'should produce a tbody'); assert.ok(html.includes(''), 'header cell preserved'); assert.ok(html.includes(''), 'body cell preserved'); assert.ok(html.includes(''), 'body cell preserved'); }); test('renderMarkdown renders indented code blocks with spellcheck attrs', () => { const html = renderMarkdown(' indented code here'); assert.ok(html.includes('
    '), 'pre should have spellcheck=false');
    	assert.ok(html.includes(''), 'code should have spellcheck=false');
    	assert.ok(html.includes('indented code here'), 'code content preserved');
    });
    
    test('renderMarkdown renders setext headings as h1 and h2', () => {
    	const h1 = renderMarkdown('Hello World\n===========');
    	assert.ok(h1.includes('

    Hello World

    '), 'setext === should produce h1'); const h2 = renderMarkdown('Subheading\n----------'); assert.ok(h2.includes('

    Subheading

    '), 'setext --- should produce h2'); }); test('renderMarkdown renders hard line breaks', () => { const html = renderMarkdown('line one \nline two'); assert.ok(html.includes('
    '), 'two trailing spaces should produce a
    '); assert.ok(html.includes('line one'), 'first line preserved'); assert.ok(html.includes('line two'), 'second line preserved'); }); test('renderMarkdown renders strikethrough', () => { const html = renderMarkdown('~~struck through~~'); assert.ok(html.includes('struck through'), 'strikethrough should produce '); }); test('renderMarkdown expands reference-style links to inline', () => { const html = renderMarkdown('[Example][ref]\n\n[ref]: https://example.com'); assert.ok(html.includes('href="https://example.com"'), 'reference link href resolved'); assert.ok(html.includes('>Example'), 'link text preserved'); }); test('logged out layout clears client storage and service worker state', () => { const html = layoutPage({ user: null, loginError: '' }); assert.ok(html.includes('')); assert.ok(html.includes(' { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '' }); assert.ok(html.includes('')); assert.ok(html.includes('Logout')); assert.ok(html.includes('')); assert.ok(html.includes('/app.js')); assert.ok(!html.includes('logoutNow(event)')); assert.ok(!html.includes('hx-post="/logout"')); }); test('settings page renders font controls and MFA details', () => { const html = settingsPage({ user: { email: 'user@example.com' }, settings: { noteFontSize: 17, codeFontSize: 13, noteMonospace: true, dateFormat: 'DD/MM/YYYY', datetimeFormat: 'DD/MM/YYYY HH:mm', autoLogout: true, autoLogoutMinutes: 30 } }); assert.ok(html.includes('Joplock Settings')); assert.ok(html.includes('id="settings-note-font"')); assert.ok(html.includes('id="settings-code-font"')); assert.ok(html.includes('id="settings-note-monospace"')); assert.ok(html.includes('id="settings-date-format"')); assert.ok(html.includes('id="settings-datetime-format"')); assert.ok(html.includes('Two-Factor Authentication')); assert.ok(html.includes('Use monospace for note text')); assert.ok(html.includes('Reopen the last edited note on startup')); assert.ok(html.includes('Expire session after inactivity')); assert.ok(html.includes('name="autoLogoutMinutes"')); assert.ok(html.includes('class="login-eye"')); assert.ok(html.includes('saveSetting')); // auto-save function }); test('settings page renders backup section for admin', () => { const html = settingsPage({ user: { id: 'admin-1', email: 'admin@example.com' }, settings: {}, isAdmin: true, adminUsers: [], backupEnabled: true, backups: [{ name: 'joplock-backup-2026.dump', createdTime: Date.UTC(2026, 4, 18, 14, 22, 31), size: 1234 }], }); assert.ok(html.includes('Backup & Restore')); assert.ok(html.includes('/admin/backups')); assert.ok(html.includes('/admin/restore')); assert.ok(html.includes('/admin/status')); assert.ok(html.includes('joplock-backup-2026.dump')); assert.ok(html.includes('/recovery')); }); test('stripMarkdownForTitle removes common markdown markers from titles', () => { assert.equal(stripMarkdownForTitle('# **Hello** [world](https://example.com)'), 'Hello world'); assert.equal(stripMarkdownForTitle('![alt text](img.png) `code`'), 'alt text code'); assert.equal(stripMarkdownForTitle('a note in ++generals++'), 'a note in generals'); assert.equal(stripMarkdownForTitle('title
    '), 'title'); assert.equal(stripMarkdownForTitle('
    title
    '), 'title'); assert.equal(stripMarkdownForTitle('Filter downstairs ignore'), 'Filter downstairs'); }); test('navigation and editor render plain note titles without markdown formatting', () => { // Nav no longer renders note items inline — test folderNotesPageFragment for note title stripping const navNotesHtml = folderNotesPageFragment([{ id: 'n1', title: '# **Hello**', parentId: 'f1', deletedTime: 0 }], 'f1', 'n1', false, 1, 1); const editorHtml = editorFragment({ id: 'n1', title: '# **Hello**', body: 'Body', parentId: 'f1', createdTime: 1000, updatedTime: 2000 }, [{ id: 'f1', title: 'Folder 1' }]); assert.ok(navNotesHtml.includes('>Hello<')); assert.ok(!navNotesHtml.includes('Hello')); assert.ok(editorHtml.includes('data-placeholder="Note title">Hello')); assert.ok(editorHtml.includes('value="Hello"')); }); test('logged in layout can render resumed editor content', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '
    nav
    ', editorContent: '
    ' }); assert.ok(html.includes('
    ')); assert.ok(html.includes('
    ')); assert.ok(!html.includes('
    \n\t\t\t
    Select a note
    ')); }); test('logged in layout exposes mobile startup resume data', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '
    nav
    ', mobileEditorContent: mobileEditorFragment({ id: 'n1', title: 'Hello', body: 'Body', parentId: 'f1', createdTime: 1000, updatedTime: 2000 }, [{ id: 'f1', title: 'Folder 1' }], 'f1'), mobileStartup: { folderId: '__all_notes__', folderTitle: 'All Notes', noteId: 'n1', noteTitle: 'Hello' }, }); assert.ok(html.includes('"noteId":"n1","noteTitle":"Hello"')); assert.ok(html.includes('mobileStartup:{"folderId":"__all_notes__","folderTitle":"All Notes","noteId":"n1","noteTitle":"Hello"}')); assert.ok(html.includes('')); assert.ok(html.includes('id="mobile-editor-search-open"')); assert.ok(html.includes('id="mobile-editor-search-input"')); assert.ok(html.includes('id="mobile-search-nav-counter"')); assert.ok(html.includes('id="mobile-search-prev-btn"')); assert.ok(html.includes('id="mobile-search-next-btn"')); assert.ok(html.includes('/app.js')); assert.ok(html.includes('
    ')); assert.ok(html.includes('id="mobile-editor-menu-btn"')); assert.ok(html.includes('id="mobile-ctx-move"')); assert.ok(html.includes('id="mobile-ctx-delete"')); assert.ok(html.includes('id="mobile-folder-picker-sheet"')); assert.ok(html.includes('id="mobile-folder-picker-list"')); assert.ok(html.includes('hx-put="/fragments/editor/n1"')); assert.ok(html.includes('mobile-hidden-folder-select')); assert.ok(!html.includes('
    ')); assert.ok(html.includes('
    ')); // JS functions are in public/app.js assert.ok(html.includes('/app.js')); assert.ok(!html.includes('...C.searchKeymap...')); }); test('logged out layout does not show global auth code field', () => { const html = layoutPage({ user: null, loginError: '' }); assert.ok(!html.includes('Global auth code')); }); test('logged in layout preserves plain square brackets on preview round trip', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '' }); // These functions are now in public/app.js, not inline — check app.js is referenced assert.ok(html.includes('/app.js')); assert.ok(html.includes('_joplockConfig')); }); test('logged in layout includes extended Joplin theme options', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '' }); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); assert.ok(html.includes('')); }); test('logged in layout uses ordered list command and block transforms in preview toolbar', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '' }); // Editor functions are now in public/app.js — verify it's referenced and toolbar HTML is present assert.ok(html.includes('/app.js')); assert.ok(!html.includes('clean-md-toggle')); }); test('logged in layout emits inline config script that parses', () => { const html = layoutPage({ user: { email: 'user@example.com', fullName: 'User' }, navContent: '
    ' }); // The last script before is now just the config object const match = html.match(/'), '<script>alert("x")</script>'); assert.equal(escapeHtml("a'b"), 'a'b'); assert.equal(escapeHtml('a&b'), 'a&b'); }); test('escapeHtml coerces non-string values', () => { assert.equal(escapeHtml(42), '42'); assert.equal(escapeHtml(null), 'null'); assert.equal(escapeHtml(undefined), 'undefined'); }); // --- folderListItem --- test('folderListItem renders folder button with htmx attributes', () => { const html = folderListItem({ id: 'f1', title: 'Work', noteCount: 5 }, 'f1'); assert.ok(html.includes('class="sidebar-item active"')); assert.ok(html.includes('data-folder-id="f1"')); assert.ok(html.includes('hx-get="/fragments/notes?folderId=f1"')); assert.ok(html.includes('Work')); assert.ok(html.includes('>5<')); }); test('folderListItem not active when different folder selected', () => { const html = folderListItem({ id: 'f1', title: 'Work' }, 'f2'); assert.ok(!html.includes('class="sidebar-item active"')); }); test('folderListItem shows Untitled for empty title', () => { const html = folderListItem({ id: 'f1', title: '' }, ''); assert.ok(html.includes('Untitled')); }); // --- folderListFragment --- test('folderListFragment renders all folders', () => { const html = folderListFragment([ { id: 'f1', title: 'A' }, { id: 'f2', title: 'B' }, ], 'f1'); assert.ok(html.includes('data-folder-id="f1"')); assert.ok(html.includes('data-folder-id="f2"')); }); test('folderListFragment shows empty hint when no folders', () => { const html = folderListFragment([], ''); assert.ok(html.includes('No notebooks yet')); }); // --- noteListItem --- test('noteListItem renders note button with editor link', () => { const html = noteListItem({ id: 'n1', title: 'My Note' }, 'n1', 'f1'); assert.ok(html.includes('class="notelist-item active"')); assert.ok(html.includes('data-note-id="n1"')); assert.ok(html.includes('hx-get="/fragments/editor/n1?currentFolderId=f1"')); assert.ok(html.includes('My Note')); }); test('noteListItem not active when different note selected', () => { const html = noteListItem({ id: 'n1', title: 'Note' }, 'n2', 'f1'); assert.ok(!html.includes('class="notelist-item active"')); }); test('noteListItem strips markdown from title', () => { const html = noteListItem({ id: 'n1', title: '# **Bold Title**' }, '', ''); assert.ok(html.includes('Bold Title')); assert.ok(!html.includes('**')); }); test('noteListItem shows lock for notes inside vault notebooks', () => { const html = noteListItem({ id: 'n1', title: 'Vault Note', parentId: 'vault-1', inVault: true, isEncrypted: false }, '', 'vault-1'); assert.ok(html.includes('note-lock-icon')); assert.ok(html.includes('data-vault-id="vault-1"')); assert.ok(!html.includes('data-encrypted="1"')); }); // --- noteListFragment --- test('noteListFragment renders header with new note button and search', () => { const html = noteListFragment([{ id: 'n1', title: 'Note' }], 'n1', 'f1'); assert.ok(html.includes('+ New note')); assert.ok(html.includes('hx-post="/fragments/notes"')); assert.ok(html.includes('notelist-search')); assert.ok(html.includes('Note')); }); test('noteListFragment shows empty hint when no notes', () => { const html = noteListFragment([], '', 'f1'); assert.ok(html.includes('No notes')); }); test('noteListFragment omits new note button without folderId', () => { const html = noteListFragment([], '', ''); assert.ok(!html.includes('+ New note')); }); // --- noteSyncStateFragment --- test('noteSyncStateFragment includes updatedTime hidden field', () => { const html = noteSyncStateFragment({ updatedTime: 12345 }); assert.ok(html.includes('name="baseUpdatedTime" value="12345"')); assert.ok(html.includes('name="forceSave"')); assert.ok(html.includes('name="createCopy"')); }); // --- noteMetaFragment --- test('noteMetaFragment includes data attributes for times', () => { const html = noteMetaFragment({ createdTime: 100, updatedTime: 200 }); assert.ok(html.includes('data-created-time="100"')); assert.ok(html.includes('data-updated-time="200"')); assert.ok(html.includes('Created 01-Jan-70 | Edited 01-Jan-70')); }); test('noteMetaText returns empty string when no timestamps exist', () => { assert.equal(noteMetaText({ createdTime: 0, updatedTime: 0 }), ''); }); test('noteMetaFragment supports custom element ids', () => { const html = noteMetaFragment({ createdTime: 100, updatedTime: 200 }, 'status-note-meta'); assert.ok(html.includes('id="status-note-meta"')); }); test('layoutPage renders status bar note meta with dedicated id', () => { const html = layoutPage({ user: { email: 'a@b.com', fullName: '' }, navContent: '
    Nav
    ', editorContent: '
    Editor
    ', settings: {}, }); assert.ok(html.includes('id="status-note-meta"')); assert.ok(!html.includes('id="note-meta" class="note-meta" data-created-time="0" data-updated-time="0"')); }); // --- autosaveStatusFragment --- test('autosaveStatusFragment returns Saved span', () => { const html = autosaveStatusFragment(); assert.ok(html.includes('autosave-ok')); assert.ok(html.includes('Saved')); }); // --- historyModalFragment --- test('historyModalFragment renders snapshots list', () => { const snapshots = [ { id: 's1', savedTime: Date.now(), title: 'First' }, { id: 's2', savedTime: Date.now() - 60000, title: 'Second' }, ]; const html = historyModalFragment('n1', snapshots); assert.ok(html.includes('data-snapshot-id="s1"')); assert.ok(html.includes('data-snapshot-id="s2"')); assert.ok(html.includes('history-item-active')); assert.ok(html.includes('selectHistorySnapshot')); assert.ok(html.includes('restoreHistorySnapshot')); assert.ok(html.includes('closeHistoryModal')); assert.ok(html.includes('hx-get="/fragments/history-snapshot/s1"')); }); test('historyModalFragment shows empty state', () => { const html = historyModalFragment('n1', []); assert.ok(html.includes('No saved snapshots')); assert.ok(!html.includes('restoreHistorySnapshot')); }); // --- historySnapshotPreviewFragment --- test('historySnapshotPreviewFragment renders body preview', () => { const html = historySnapshotPreviewFragment({ body: 'Hello world' }); assert.ok(html.includes('Hello world')); assert.ok(html.includes('history-snapshot-body')); }); test('historySnapshotPreviewFragment truncates long body', () => { const body = 'x'.repeat(4000); const html = historySnapshotPreviewFragment({ body }); assert.ok(html.includes('…')); }); // --- renderInlineMarkdown --- test('renderInlineMarkdown renders bold, italic, strikethrough, underline, code', () => { assert.equal(renderInlineMarkdown('**bold**'), 'bold'); assert.equal(renderInlineMarkdown('*italic*'), 'italic'); assert.equal(renderInlineMarkdown('~~strike~~'), 'strike'); assert.equal(renderInlineMarkdown('++under++'), 'under'); assert.equal(renderInlineMarkdown('`code`'), 'code'); }); test('renderInlineMarkdown returns empty for falsy input', () => { assert.equal(renderInlineMarkdown(''), ''); assert.equal(renderInlineMarkdown(null), ''); assert.equal(renderInlineMarkdown(undefined), ''); }); // --- adminUserRow --- test('adminUserRow renders enabled user with actions', () => { const html = adminUserRow({ id: 'u1', email: 'a@b.com', full_name: 'Alice', enabled: true, created_time: 1700000000000 }, 'u2'); assert.ok(html.includes('a@b.com')); assert.ok(html.includes('Alice')); assert.ok(html.includes('badge-ok')); assert.ok(html.includes('Enabled')); assert.ok(html.includes('Disable User')); assert.ok(html.includes('Delete User')); assert.ok(html.includes('/admin/users/u1/password')); assert.ok(html.includes('/admin/users/u1/disable')); assert.ok(html.includes('/admin/users/u1/delete')); }); test('adminUserRow hides disable/delete for self', () => { const html = adminUserRow({ id: 'u1', email: 'a@b.com', enabled: true, created_time: 0 }, 'u1'); assert.ok(html.includes('your admin account')); assert.ok(!html.includes('Disable User')); assert.ok(!html.includes('Delete User')); }); test('adminUserRow shows MFA badge when totp enabled', () => { const html = adminUserRow({ id: 'u1', email: 'a@b.com', enabled: true, created_time: 0, totpEnabled: true, totpSeed: 'ABC', totpQr: 'data:qr' }, 'u2'); assert.ok(html.includes('badge-mfa')); assert.ok(html.includes('MFA')); assert.ok(html.includes('Disable MFA')); assert.ok(html.includes('/admin/users/u1/mfa/disable')); }); test('adminUserRow shows enable MFA when totp not enabled', () => { const html = adminUserRow({ id: 'u1', email: 'a@b.com', enabled: true, created_time: 0, totpEnabled: false }, 'u2'); assert.ok(html.includes('Enable MFA')); assert.ok(html.includes('/admin/users/u1/mfa/enable')); }); test('adminUserRow shows disabled badge and enable button', () => { const html = adminUserRow({ id: 'u1', email: 'a@b.com', enabled: false, created_time: 0 }, 'u2'); assert.ok(html.includes('badge-off')); assert.ok(html.includes('Disabled')); assert.ok(html.includes('Enable User')); assert.ok(html.includes('/admin/users/u1/enable')); }); // --- mfaPage --- test('mfaPage renders MFA challenge form', () => { const html = mfaPage(); assert.ok(html.includes('Two-Factor Authentication')); assert.ok(html.includes('action="/login/mfa"')); assert.ok(html.includes('name="totp"')); assert.ok(html.includes('pattern="[0-9]{6}"')); assert.ok(html.includes('Back to login')); }); test('mfaPage shows error when provided', () => { const html = mfaPage({ error: 'Invalid code' }); assert.ok(html.includes('Invalid code')); }); test('mfaPage escapes error HTML', () => { const html = mfaPage({ error: '' }); assert.ok(html.includes('<script>')); assert.ok(!html.includes('
    Namefoobar