mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-25 20:48:28 +09:00
* fix(theme): preserve Midwinter authentication controls Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): describe theme fix Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ui): preserve form state and notification lifecycles Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix: replace the blinking timer during skin setup Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
151 lines
4.5 KiB
JavaScript
151 lines
4.5 KiB
JavaScript
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const vm = require('node:vm');
|
|
|
|
const layoutSource = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'include', 'layout.js'),
|
|
'utf8'
|
|
);
|
|
|
|
function extractFunction(name) {
|
|
const start = layoutSource.indexOf(`function ${name}(`);
|
|
|
|
assert.notEqual(start, -1, `${name}() must exist`);
|
|
|
|
const bodyStart = layoutSource.indexOf('{', start);
|
|
let depth = 0;
|
|
|
|
for (let offset = bodyStart; offset < layoutSource.length; offset++) {
|
|
if (layoutSource[offset] === '{') {
|
|
depth++;
|
|
} else if (layoutSource[offset] === '}') {
|
|
depth--;
|
|
|
|
if (depth === 0) {
|
|
return layoutSource.slice(start, offset + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Unable to extract ${name}()`);
|
|
}
|
|
|
|
test('scroll handlers close initialized select menus without duplicate bindings', () => {
|
|
const windowObject = {};
|
|
const selects = [{ initialized: true, closes: 0 }, { initialized: false, closes: 0 }];
|
|
let boundEvent;
|
|
let menuOpen = true;
|
|
let selectScans = 0;
|
|
let scrollHandler;
|
|
let selectedContainers;
|
|
|
|
function jquery(selector) {
|
|
if (typeof selector === 'string' && selector.startsWith('.cactiConsoleContentArea')) {
|
|
selectedContainers = selector;
|
|
|
|
return {
|
|
add: (target) => {
|
|
assert.equal(target, windowObject);
|
|
|
|
return {
|
|
rebind: (bound, handler) => {
|
|
assert.equal(bound, 'scroll.cactiSelectmenu');
|
|
|
|
boundEvent = bound;
|
|
scrollHandler = handler;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
if (selector === 'select') {
|
|
selectScans++;
|
|
|
|
return {
|
|
each: (callback) => selects.forEach(select => callback.call(select))
|
|
};
|
|
}
|
|
|
|
if (selector === '.ui-selectmenu-open') {
|
|
return { length: menuOpen ? 1 : 0 };
|
|
}
|
|
|
|
return {
|
|
selectmenu: (action) => {
|
|
if (action === 'instance') {
|
|
return selector.initialized ? {} : undefined;
|
|
}
|
|
|
|
assert.equal(action, 'close');
|
|
selector.closes++;
|
|
}
|
|
};
|
|
}
|
|
|
|
const context = vm.createContext({ $: jquery, window: windowObject });
|
|
vm.runInContext(extractFunction('setupSelectmenuScrollClose'), context, { filename: 'include/layout.js' });
|
|
context.setupSelectmenuScrollClose();
|
|
|
|
assert.match(selectedContainers, /\.cactiConsoleContentArea/);
|
|
assert.match(selectedContainers, /\.cactiGraphContentArea/);
|
|
assert.match(selectedContainers, /\.cactiTreeNavigationArea/);
|
|
assert.equal(boundEvent, 'scroll.cactiSelectmenu');
|
|
assert.equal(typeof scrollHandler, 'function');
|
|
|
|
scrollHandler();
|
|
assert.equal(selects[0].closes, 1);
|
|
assert.equal(selects[1].closes, 0);
|
|
assert.equal(selectScans, 1);
|
|
|
|
menuOpen = false;
|
|
scrollHandler();
|
|
assert.equal(selectScans, 1);
|
|
});
|
|
|
|
test('applySkin binds scroll handling after theme widgets initialize', () => {
|
|
const themeReadyPosition = layoutSource.indexOf('themeReady();');
|
|
const setupPosition = layoutSource.indexOf('setupSelectmenuScrollClose();', themeReadyPosition);
|
|
|
|
assert.notEqual(themeReadyPosition, -1);
|
|
assert.ok(setupPosition > themeReadyPosition);
|
|
});
|
|
|
|
test('repeated skin setup keeps one blinking interval and preserves other timers', () => {
|
|
const start = layoutSource.indexOf('// Setup blinking');
|
|
const end = layoutSource.indexOf('/* applySkin() runs more than once', start);
|
|
assert.ok(start > 0 && end > start);
|
|
const intervals = new Map([[1, () => {}]]);
|
|
let nextId = 1;
|
|
const context = vm.createContext({
|
|
setInterval(callback, delay) {
|
|
assert.equal(delay, 1200);
|
|
const id = ++nextId;
|
|
intervals.set(id, callback);
|
|
return id;
|
|
},
|
|
clearInterval(id) { intervals.delete(id); },
|
|
});
|
|
vm.runInContext(`function applySkin() { ${layoutSource.slice(start, end)} }`, context);
|
|
for (let transition = 0; transition < 100; transition++) context.applySkin();
|
|
assert.equal(intervals.size, 2);
|
|
assert.equal(intervals.has(1), true);
|
|
});
|