Raiju/tests/editor-shared-content-smoke.js
2026-10-25 06:05:42 +03:00

86 lines
5.3 KiB
JavaScript

/* Regression: custom-layout helpers must load full shared documents before editing. */
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const source = fs.readFileSync(path.join(__dirname, '../js/inline-editor.js'), 'utf8');
function implementation(name) {
const match = source.match(new RegExp(` (?:async )?function ${name}\\([^]*?\\n }`));
assert.ok(match, name);
return match[0];
}
const fixtures = [
{'data-content-page': 'de_home'},
{class: 'editable-field', 'data-page': 'de_shared'},
{class: 'editable-field editable-field-html', 'data-page': 'de_shared'},
{'data-editable-group': '', 'data-page': 'de_groups'},
{'data-editable-link': '', 'data-page': 'de_links'},
{'data-editable-image': '', 'data-page': 'de_images'},
{'data-editable-icon': '', 'data-page': 'de_icons'},
{'data-editable-list': '', 'data-list-page': 'de_lists'},
{'data-editable-image': '', 'data-page': 'events'},
{'data-page': 'unrelated-widget'}
];
const nodes = fixtures.map(attrs => ({attrs, dataset: Object.fromEntries(Object.entries(attrs)
.filter(([key]) => key.startsWith('data-'))
.map(([key, value]) => [key.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), value]))}));
const matches = (node, selector) => selector.split(',').some(part => {
const classes = [...part.matchAll(/\.([\w-]+)/g)].map(match => match[1]);
const attributes = [...part.matchAll(/\[([\w-]+)\]/g)].map(match => match[1]);
return classes.every(cls => (node.attrs.class || '').split(' ').includes(cls))
&& attributes.every(attr => Object.hasOwn(node.attrs, attr));
});
const config = {apiUrl: '/admin/api.php', contentData: {}, contentRevisions: {}, loadedPages: [], dirtyPages: new Set()};
const loads = [];
const shared = {ui: {jurists: 'Juristen'}, footer: {company: 'Keep the complete footer'}, contact: {phone: '123'}};
const context = vm.createContext({
console, URL, Set, JSON, EditorConfig: config,
document: {
addEventListener() {},
querySelector(selector) {
return selector === 'meta[name="csrf-token"]' ? {content: 'fixture-token'}
: selector === 'meta[name="content-page"]' ? {content: 'de_home'} : null;
},
querySelectorAll: selector => nodes.filter(node => matches(node, selector))
},
window: {}, localStorage: {getItem: () => null}, sessionStorage: {getItem: () => null},
fetch: async url => {
const page = new URL(url, 'http://localhost').searchParams.get('page');
loads.push(page);
return {json: async () => ({success: true, data: structuredClone(page === 'de_shared' ? shared : {page}), revision: 'revision-' + page})};
}
});
for (const name of ['exitEditModeClean', 'loadEvents', 'attachEditorChromeHoverStates', 'createEditorUI',
'createGroupEditorUI', 'createEventEditorUI', 'createAddSectionUI', 'attachEditHandlers',
'attachEventEditHandlers', 'attachFooterEditHandlers', 'attachGroupEditHandlers', 'attachFieldEditHandlers',
'attachLinkEditHandlers', 'attachImageEditHandlers', 'attachListEditHandlers', 'attachComparisonRowHandlers',
'attachComparisonCellToggles', 'showAdminBar', 'attachEditModeHint', 'pushUndoState', 'updateUndoRedoButtons']) context[name] = () => {};
vm.runInContext(['initEditor', 'loadContent', 'setNestedValue', 'saveField', 'toggleListItemHidden'].map(implementation).join('\n'), context);
(async () => {
vm.runInContext('initEditor()', context);
await new Promise(setImmediate);
assert.deepEqual(loads.sort(), ['de_home', 'de_shared', 'de_groups', 'de_links', 'de_images', 'de_icons', 'de_lists'].sort());
assert.equal(config.contentRevisions.de_shared, 'revision-de_shared');
context.edited = {textContent: ''};
vm.runInContext("saveField('de_shared', 'ui.jurists', 'Updated heading', edited, false)", context);
assert.deepEqual(config.contentData.de_shared, {...shared, ui: {jurists: 'Updated heading'}});
assert.equal(context.edited.textContent, 'Updated heading');
assert.ok(config.dirtyPages.has('de_shared'));
config.contentData.de_team = {staff: [{name: 'First'}, {name: 'Second'}]};
Object.assign(context, {
getNestedObj: (value, key) => value[key], t: key => key, showToast() {},
Icons: {eyeClosed: 'closed', eyeOpen: 'open'}, item: {dataset: {}},
button: {dataset: {}, setAttribute(key, value) { this[key] = value; }}
});
for (const hidden of [true, false]) {
vm.runInContext("toggleListItemHidden('de_team', 'staff', 0, item, button)", context);
assert.equal(!!config.contentData.de_team.staff[0].hidden, hidden);
assert.equal(context.item.dataset.hidden, hidden ? 'true' : undefined);
assert.equal(context.button['aria-label'], hidden ? 'show' : 'hide');
assert.equal(context.button.dataset.editorTooltip, hidden ? 'show' : 'hide');
assert.deepEqual(config.contentData.de_team.staff[1], {name: 'Second'});
}
console.log('Shared editor content: all helper types loaded once with revisions; unrelated fields preserved; event images use their separate API.');
console.log('List visibility: hide/show updates data, accessible labels and tooltips without affecting the next item.');
})().catch(error => { console.error(error); process.exitCode = 1; });