const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const { JSDOM } = require('jsdom'); const root = process.env.PEDAI_CMS_TEST_ROOT || path.join(__dirname, '..'); const read = file => fs.readFileSync(path.join(root, file), 'utf8'); const tick = () => new Promise(resolve => setImmediate(resolve)); test('actual CMS category form creates manually once, supports native submit and preserves failed drafts', async () => { const dom = new JSDOM('' + read('public/components/cms.html') + ''); const keys = ['window', 'document', 'fetch', 'getAuthHeaders', 'showToast']; const previous = Object.fromEntries(keys.map(key => [key, global[key]])); const calls = [], categories = [], notices = []; let fail = false; global.window = dom.window; global.document = dom.window.document; global.getAuthHeaders = () => ({ 'Content-Type': 'application/json' }); global.showToast = (message, type) => notices.push({ message, type }); global.fetch = async (url, options = {}) => { calls.push({ url, options }); assert.ok(!url.includes('ai-generate'), 'Manual categories must not call AI'); if (options.method === 'POST') { assert.equal(url, '/api/admin/learning/categories'); const payload = JSON.parse(options.body); if (fail) return { json: async () => ({ success: false, error: 'Synthetic rejection' }) }; categories.push({ id: categories.length + 1, name: payload.name, slug: 'category-' + (categories.length + 1), content_count: 0 }); return { json: async () => ({ success: true, id: categories.length }) }; } assert.ok(['/api/admin/learning/categories', '/api/learning/categories'].includes(url), url); return { json: async () => ({ success: true, categories }) }; }; try { const input = document.getElementById('lh-cms-cat-name'); const button = document.getElementById('btn-lh-add-cat'); const form = input.closest('form'); assert.ok(form, 'Category entry needs native form submission for Enter'); assert.equal(form.id, 'lh-cms-add-category'); assert.equal(button.type, 'submit'); assert.equal(button.textContent.trim(), 'Add', 'Creation remains discoverable without icon fonts'); assert.equal(input.getAttribute('aria-label'), 'New category name'); assert.match(window.getComputedStyle(input).minWidth, /^0(px)?$/, 'Input must shrink inside the sidebar'); assert.equal(window.getComputedStyle(button).flexShrink, '0', 'The Add control must remain visible'); await import(pathToFileURL(path.join(root, 'public/js/learningHub.js')).href); const prevented = []; document.addEventListener('submit', event => prevented.push(event.defaultPrevented)); input.value = ' Manual category '; button.click(); await tick(); await tick(); assert.equal(calls.filter(c => c.options.method === 'POST').length, 1); assert.equal(categories[0].name, 'Manual category'); assert.equal(input.value, ''); assert.match(document.getElementById('lh-cms-categories').textContent, /Manual category/); input.value = 'Keyboard category'; form.requestSubmit(); // Same submit path as the browser's implicit Enter action. await tick(); await tick(); assert.equal(categories.length, 2); assert.equal(calls.filter(c => c.options.method === 'POST').length, 2); input.value = ' '; form.requestSubmit(); await tick(); assert.equal(calls.filter(c => c.options.method === 'POST').length, 2); assert.equal(notices.at(-1).message, 'Enter category name'); fail = true; input.value = 'Keep this draft'; button.click(); await tick(); await tick(); assert.equal(input.value, 'Keep this draft'); assert.equal(notices.at(-1).message, 'Synthetic rejection'); assert.deepEqual(prevented, [true, true, true, true], 'Every path prevents native page navigation'); } finally { for (const key of keys) global[key] = previous[key]; dom.window.close(); } });