251 lines
13 KiB
JavaScript
251 lines
13 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import vm from 'node:vm';
|
|
|
|
const root = process.cwd();
|
|
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
|
|
const adapters = read('public/safe-action-adapters.js');
|
|
const loader = read('public/module-extension-workspace.js');
|
|
const pkg = JSON.parse(read('package.json'));
|
|
const failures = [];
|
|
const expect = (condition, message) => { if (!condition) failures.push(message); };
|
|
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
|
|
|
|
const actionIds = [
|
|
'workspace:modules:refresh',
|
|
'workspace:modules:filter-all',
|
|
'workspace:modules:filter-active',
|
|
'workspace:modules:filter-disabled',
|
|
'workspace:modules:filter-external',
|
|
'workspace:modules:filter-problems',
|
|
'workspace:extensions:filter-all',
|
|
'workspace:extensions:filter-ready',
|
|
'workspace:extensions:filter-connected',
|
|
'workspace:extensions:filter-attention',
|
|
'workspace:articles:open-current',
|
|
'workspace:articles:view-cards',
|
|
'workspace:articles:view-list',
|
|
'workspace:articles:reset-filters',
|
|
'workspace:media:open-current-preview',
|
|
'workspace:media:density-compact',
|
|
'workspace:media:density-comfortable',
|
|
'workspace:media:density-large',
|
|
'workspace:media:view-grid',
|
|
'workspace:media:view-list',
|
|
'workspace:media:reset-filters',
|
|
];
|
|
|
|
includesAll(adapters, [
|
|
'uxSafeActionAdaptersReady',
|
|
'uxSafeActionAdaptersRegistered',
|
|
'window.VendooActions',
|
|
'window.VendooNavigation?.currentView',
|
|
'workspaceVisible',
|
|
"workspaceVisible('admin', 'admin-modules-panel')",
|
|
"workspaceVisible('extensions')",
|
|
"workspaceVisible('history')",
|
|
"workspaceVisible('media-library')",
|
|
'buttonAction',
|
|
'actions.registerMany(registered)',
|
|
"surfaces: ['command-palette']",
|
|
"danger: 'none'",
|
|
'vendoo:actions-ready',
|
|
'vendoo:safe-action-adapters-ready',
|
|
'baselineCount: 9',
|
|
'expansionCount: registered.length - 9',
|
|
...actionIds,
|
|
'#module-manager-refresh',
|
|
'#history-reset-filters',
|
|
'#media-view-grid',
|
|
'#media-view-list',
|
|
'[data-article-view-choice="cards"]',
|
|
'[data-article-view-choice="list"]',
|
|
'[data-media-density-option="compact"]',
|
|
'[data-media-density-option="comfortable"]',
|
|
'[data-media-density-option="large"]',
|
|
'.ux-media-filter-reset',
|
|
'focusin',
|
|
'pointerdown',
|
|
], 'Safe Action Adapter-Vertrag fehlt');
|
|
|
|
includesAll(loader, [
|
|
'loadCommandPlatformAssets',
|
|
'loadActionRegistryAssets',
|
|
'loadGlobalNavigationAssets',
|
|
'loadSafeActionAdapters',
|
|
'safe-action-adapters.js?v=1.44.0',
|
|
'data-ux-safe-action-adapters',
|
|
'loadSafeActionAdapters();',
|
|
], 'Safe-Action-Loader fehlt');
|
|
expect(loader.indexOf('loadActionRegistryAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Action Registry muss vor Safe Action Adapters geladen werden');
|
|
expect(loader.indexOf('loadGlobalNavigationAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Globale Navigation muss vor Safe Action Adapters geladen werden');
|
|
|
|
expect(pkg.scripts?.['verify:safe-action-adapters'] === 'node tools/verify-safe-action-adapters-1.44.0.mjs', 'verify:safe-action-adapters fehlt');
|
|
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:safe-action-adapters'), 'verify:all enthält Safe-Action-Gate nicht');
|
|
|
|
expect(!/\.innerHTML\s*=/.test(adapters), 'Safe Action Adapters dürfen innerHTML nicht setzen');
|
|
expect(!/\bfetch\s*\(/.test(adapters), 'Safe Action Adapters dürfen keine Netzwerkaufrufe ausführen');
|
|
expect(!/\bconfirm\s*\(/.test(adapters), 'Safe Action Adapters dürfen window.confirm nicht verwenden');
|
|
expect(!/setInterval\s*\(/.test(adapters), 'Safe Action Adapters dürfen kein Polling starten');
|
|
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable|start|stop)/i.test(adapters), 'Safe Action Adapters dürfen keine produktive Fachlogik direkt aufrufen');
|
|
expect(!/danger:\s*['"](?:caution|destructive)['"]/.test(adapters), 'Safe Action Adapters dürfen keine vorsichtigen oder destruktiven Actions registrieren');
|
|
|
|
function element({ id = '', selector = '', parent = null } = {}) {
|
|
return {
|
|
id,
|
|
selector,
|
|
hidden: false,
|
|
disabled: false,
|
|
isConnected: true,
|
|
clicks: 0,
|
|
classList: { contains: () => false },
|
|
getAttribute: () => null,
|
|
click() { this.clicks += 1; },
|
|
closest(query) {
|
|
if (query === '#history-list .history-row' && parent?.kind === 'article-row') return parent;
|
|
if (query === '[data-media-preview]' && this.selector === '[data-media-preview]') return this;
|
|
if (query === '.history-row' && parent?.kind === 'article-row') return parent;
|
|
if (query === '.media-card' && parent?.kind === 'media-card') return parent;
|
|
return null;
|
|
},
|
|
querySelector(query) { return parent?.querySelector?.(query) || null; },
|
|
};
|
|
}
|
|
|
|
function verifyRuntime() {
|
|
const registered = [];
|
|
const documentListeners = new Map();
|
|
const events = [];
|
|
const rootElement = { dataset: {} };
|
|
|
|
const sectionMap = new Map([
|
|
['admin-modules-panel', element({ id: 'admin-modules-panel' })],
|
|
['extensions', element({ id: 'extensions' })],
|
|
['history', element({ id: 'history' })],
|
|
['media-library', element({ id: 'media-library' })],
|
|
]);
|
|
|
|
const selectors = [
|
|
'#module-manager-refresh',
|
|
'[data-ux-module-group="modules"][data-ux-module-filter="all"]',
|
|
'[data-ux-module-group="modules"][data-ux-module-filter="active"]',
|
|
'[data-ux-module-group="modules"][data-ux-module-filter="disabled"]',
|
|
'[data-ux-module-group="modules"][data-ux-module-filter="external"]',
|
|
'[data-ux-module-group="modules"][data-ux-module-filter="problems"]',
|
|
'[data-ux-module-group="extensions"][data-ux-module-filter="all"]',
|
|
'[data-ux-module-group="extensions"][data-ux-module-filter="ready"]',
|
|
'[data-ux-module-group="extensions"][data-ux-module-filter="connected"]',
|
|
'[data-ux-module-group="extensions"][data-ux-module-filter="attention"]',
|
|
'[data-article-view-choice="cards"]',
|
|
'[data-article-view-choice="list"]',
|
|
'#history-reset-filters',
|
|
'[data-media-density-option="compact"]',
|
|
'[data-media-density-option="comfortable"]',
|
|
'[data-media-density-option="large"]',
|
|
'#media-view-grid',
|
|
'#media-view-list',
|
|
'.ux-media-filter-reset',
|
|
];
|
|
const selectorMap = new Map(selectors.map(selector => [selector, element({ selector })]));
|
|
|
|
const articleTitle = element({ selector: '.history-article-title' });
|
|
const articleRow = {
|
|
kind: 'article-row',
|
|
hidden: false,
|
|
disabled: false,
|
|
isConnected: true,
|
|
getAttribute: () => null,
|
|
querySelector: query => query === '.history-article-title' ? articleTitle : null,
|
|
};
|
|
const articleTarget = element({ parent: articleRow });
|
|
const mediaPreview = element({ selector: '[data-media-preview]' });
|
|
|
|
const documentObject = {
|
|
documentElement: rootElement,
|
|
addEventListener(type, listener) { documentListeners.set(type, listener); },
|
|
getElementById(id) { return sectionMap.get(id) || null; },
|
|
querySelector(selector) { return selectorMap.get(selector) || null; },
|
|
};
|
|
const windowObject = {
|
|
VendooNavigation: { currentView: 'admin' },
|
|
VendooActions: {
|
|
registerMany(actions) { registered.push(...actions); },
|
|
refresh() {},
|
|
},
|
|
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
|
addEventListener() {},
|
|
dispatchEvent(event) { events.push(event); return true; },
|
|
};
|
|
|
|
const context = vm.createContext({
|
|
window: windowObject,
|
|
document: documentObject,
|
|
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
|
|
console,
|
|
});
|
|
vm.runInContext(adapters, context, { filename: 'public/safe-action-adapters.js' });
|
|
|
|
expect(registered.length === 21, `Runtime: erwartet 21 Adapter-Actions, erhalten ${registered.length}`);
|
|
expect(new Set(registered.map(action => action.id)).size === registered.length, 'Runtime: Action-IDs sind nicht eindeutig');
|
|
expect(actionIds.every(id => registered.some(action => action.id === id)), 'Runtime: mindestens eine verbindliche Action-ID fehlt');
|
|
expect(registered.every(action => action.danger === 'none'), 'Runtime: nicht sichere Gefahrstufe registriert');
|
|
expect(registered.every(action => action.surfaces?.length === 1 && action.surfaces[0] === 'command-palette'), 'Runtime: unerlaubte Action-Surface registriert');
|
|
|
|
const byId = id => registered.find(action => action.id === id);
|
|
const clicked = selector => selectorMap.get(selector)?.clicks === 1;
|
|
const invoke = (id, selector, message) => {
|
|
expect(byId(id)?.enabled() === true, `${message}: Action ist deaktiviert`);
|
|
expect(byId(id)?.handler() === true && clicked(selector), `${message}: Delegation fehlt`);
|
|
};
|
|
|
|
expect(byId('workspace:modules:refresh')?.visible() === true, 'Runtime: Modulaktion ist in Verwaltung nicht sichtbar');
|
|
expect(byId('workspace:extensions:filter-all')?.visible() === false, 'Runtime: Extension-Aktion ist außerhalb des Extension-Bereichs sichtbar');
|
|
expect(byId('workspace:articles:view-list')?.visible() === false, 'Runtime: Artikelaktion ist außerhalb des Artikelbereichs sichtbar');
|
|
invoke('workspace:modules:refresh', '#module-manager-refresh', 'Modul-Refresh');
|
|
invoke('workspace:modules:filter-active', '[data-ux-module-group="modules"][data-ux-module-filter="active"]', 'Aktive Module');
|
|
invoke('workspace:modules:filter-disabled', '[data-ux-module-group="modules"][data-ux-module-filter="disabled"]', 'Deaktivierte Module');
|
|
invoke('workspace:modules:filter-external', '[data-ux-module-group="modules"][data-ux-module-filter="external"]', 'Installierte Module');
|
|
invoke('workspace:modules:filter-problems', '[data-ux-module-group="modules"][data-ux-module-filter="problems"]', 'Problematische Module');
|
|
|
|
windowObject.VendooNavigation.currentView = 'extensions';
|
|
expect(byId('workspace:modules:refresh')?.visible() === false, 'Runtime: Modulaktion bleibt außerhalb der Verwaltung sichtbar');
|
|
expect(byId('workspace:extensions:filter-connected')?.visible() === true, 'Runtime: Extension-Aktion ist im Extension-Bereich nicht sichtbar');
|
|
invoke('workspace:extensions:filter-ready', '[data-ux-module-group="extensions"][data-ux-module-filter="ready"]', 'Bereite Extensions');
|
|
invoke('workspace:extensions:filter-connected', '[data-ux-module-group="extensions"][data-ux-module-filter="connected"]', 'Verbundene Extensions');
|
|
invoke('workspace:extensions:filter-attention', '[data-ux-module-group="extensions"][data-ux-module-filter="attention"]', 'Extensions mit Handlungsbedarf');
|
|
|
|
windowObject.VendooNavigation.currentView = 'history';
|
|
expect(byId('workspace:articles:view-list')?.visible() === true, 'Runtime: Artikelaktion ist im Artikelbereich nicht sichtbar');
|
|
invoke('workspace:articles:view-list', '[data-article-view-choice="list"]', 'Artikel-Listenansicht');
|
|
invoke('workspace:articles:reset-filters', '#history-reset-filters', 'Artikelfilter-Reset');
|
|
documentListeners.get('focusin')?.({ target: articleTarget });
|
|
expect(byId('workspace:articles:open-current')?.enabled() === true, 'Runtime: fokussierter Artikel wird nicht als ausführbar erkannt');
|
|
expect(byId('workspace:articles:open-current')?.handler() === true && articleTitle.clicks === 1, 'Runtime: Artikel öffnen delegiert nicht an bestehenden Titel-Handler');
|
|
|
|
windowObject.VendooNavigation.currentView = 'media-library';
|
|
expect(byId('workspace:media:view-grid')?.visible() === true, 'Runtime: Medienaktion ist im Medienbereich nicht sichtbar');
|
|
invoke('workspace:media:density-compact', '[data-media-density-option="compact"]', 'Kompakte Galeriedichte');
|
|
invoke('workspace:media:density-large', '[data-media-density-option="large"]', 'Große Galeriedichte');
|
|
invoke('workspace:media:view-grid', '#media-view-grid', 'Medien-Rasteransicht');
|
|
invoke('workspace:media:view-list', '#media-view-list', 'Medien-Listenansicht');
|
|
invoke('workspace:media:reset-filters', '.ux-media-filter-reset', 'Medienfilter-Reset');
|
|
documentListeners.get('pointerdown')?.({ target: mediaPreview });
|
|
expect(byId('workspace:media:open-current-preview')?.enabled() === true, 'Runtime: fokussierte Medienvorschau wird nicht als ausführbar erkannt');
|
|
expect(byId('workspace:media:open-current-preview')?.handler() === true && mediaPreview.clicks === 1, 'Runtime: Medienvorschau delegiert nicht an bestehenden Preview-Handler');
|
|
|
|
expect(events.some(event => event.type === 'vendoo:safe-action-adapters-ready'
|
|
&& event.detail?.count === 21
|
|
&& event.detail?.baselineCount === 9
|
|
&& event.detail?.expansionCount === 12), 'Runtime: vollständiges Ready-Event fehlt');
|
|
}
|
|
|
|
verifyRuntime();
|
|
|
|
if (failures.length) {
|
|
console.error('Safe Action Adapter Gate fehlgeschlagen:');
|
|
failures.forEach(failure => console.error(`- ${failure}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('Safe Action Adapter Gate bestanden: 21 sichere Actions.');
|