💋 Sync 2026-10-16 23:55:30

This commit is contained in:
github-actions[bot] committed 2026-10-16 23:55:30 +08:00
1 parent 76f10c2a74
commit 1849cc6156
254 files changed
+25484 -8840

No files matched your search

+93 -37
View File
@@ -35,17 +35,57 @@ String.prototype.format = function(...values) {
return this.replace(/%s/g, () => values[i++]);
};
function loadMenu(dispatchpath, container) {
function loadMenu(dispatchpath, container, icons = '') {
return new Function('baseclass', 'ui', 'L', 'E', 'document', '_', menuSource)(
{ extend: value => value },
{ menu: { getChildren: node => node.children || [] } },
{ env: { dispatchpath }, url: (...parts) => parts.join('/') },
{ menu: { getChildren: node => node.children || [] }, createHandlerFn: (object, name) => object[name].bind(object) },
{ env: { dispatchpath }, url: (...parts) => parts.join('/'), media: (...parts) => '/luci-static/argon/' + parts.join('/') },
element,
{ querySelector: () => container },
{ querySelectorAll: () => [], querySelector: selector => selector == 'meta[name="argon-menu-icons"]'
? (icons === null ? null : { getAttribute: () => icons }) : container },
value => value
);
}
test('menu icons use exact node names and fall back without requesting missing files', () => {
const menu = loadMenu([], null, 'default.svg status.svg system.svg');
for (const name of ['status', 'system']) {
const icon = menu.renderMenuIcon(name);
assert.equal(icon.attrs['data-icon'], name);
assert.equal(icon.attrs['aria-hidden'], 'true');
assert.match(icon.attrs.style, new RegExp('/icon/' + name + '\\.svg'));
}
for (const name of ['Status', '状态', 'missing', '../status', 'status.svg', 'status";color:red']) {
const icon = menu.renderMenuIcon(name);
assert.equal(icon.attrs['data-icon'], 'default');
assert.match(icon.attrs.style, /\/icon\/default\.svg/);
}
assert.equal(loadMenu([], null, null).renderMenuIcon('status').attrs['data-icon'], 'default');
});
test('translated menu labels keep node-name icons and nested links keep their layout', () => {
const container = element('div');
const menu = loadMenu([], container, 'default.svg status.svg');
const rendered = menu.renderMainMenu({ name: 'admin', children: [{
name: 'status', title: '状态', children: [{ name: 'overview', title: '概览' }]
}] }, 'admin');
const link = rendered.children[0].children[0];
assert.equal(link.children[0].attrs['data-icon'], 'status');
assert.equal(link.children[1], '状态');
assert.equal(rendered.children[0].children[1].children[0].children[0].children[0], '概览');
});
test('clicking a decorative SVG is handled by its navigation link', () => {
const menu = loadMenu([], null);
const link = { parentNode: { querySelector: () => null }, nextElementSibling: {}, blur() {} };
const icon = { parentNode: link, nextElementSibling: null };
let prevented = false, stopped = false;
menu.handleMenuExpand({ target: icon, currentTarget: link,
preventDefault: () => { prevented = true; }, stopPropagation: () => { stopped = true; } });
assert.equal(prevented, true);
assert.equal(stopped, true);
});
test('nested LuCI tabs stay in parent-to-child order', () => {
const container = element('div');
const menu = loadMenu(['admin', 'system', 'demo', 'a', 'b', 'c', 'd'], container);
@@ -73,6 +113,8 @@ function runUcodeSnippet(file, start, end, setup, result, settings = {}, hasConf
writeFileSync(script, `${setup}\n${source.slice(first, last)}\nprint(sprintf("%J", ${result}));\n`);
try {
const output = execFileSync(process.env.UCODE_BIN || 'ucode', [
'-L', path.join(path.dirname(process.env.UCODE_BIN || 'ucode'), '*.so'),
'-L', path.join(root, 'root/usr/share/ucode/*.uc'),
`-Dsettings=${JSON.stringify(settings)}`,
`-DhasConfig=${JSON.stringify(hasConfig)}`,
script
@@ -84,44 +126,58 @@ function runUcodeSnippet(file, start, end, setup, result, settings = {}, hasConf
}
const configSetup = `
import { dark_palette, appearance_modes } from 'luci.argon-palette';
let cfg = { get_first: (packageName, section, key) => settings[key] };
function access(path) { return hasConfig; }
`;
test('the SVG catalog includes only safe regular SVG files and tolerates a missing directory', () => {
const setup = `
function lsdir(path) { return settings.files; }
function lstat(path) {
let parts = split(path, '/');
return { type: settings.types?.[parts[length(parts) - 1]] ?? 'file' };
}
`;
const catalog = settings => runUcodeSnippet('ucode/template/themes/argon/head.ut',
'const icon_dir =', '\n\t%}', setup, 'menu_icons', settings);
assert.deepEqual(catalog({ files: ['status.svg', 'custom-menu.svg', 'favicon.png', '../system.svg',
'bad name.svg', 'bad".svg', 'folder.svg', 'linked.svg'], types: { 'folder.svg': 'directory', 'linked.svg': 'symlink' } }),
['status.svg', 'custom-menu.svg']);
assert.deepEqual(catalog({ files: null }), []);
});
const configResult = '{mode, primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity, blur_opacity_dark, bar_color}';
for (const template of ['header.ut', 'header_login.ut']) {
const file = `ucode/template/themes/argon/${template}`;
test(`${template} uses safe defaults for missing and invalid settings`, () => {
const empty = runUcodeSnippet(file, 'function validColor', '\n-%}', configSetup, configResult);
assert.deepEqual(empty, {
mode: 'normal', primary: '#5e72e4', dark_primary: '#483d8b',
blur_radius: '10', blur_radius_dark: '10',
blur_opacity: '0.5', blur_opacity_dark: '0.5', bar_color: '#5e72e4'
});
assert.deepEqual(runUcodeSnippet(file, 'function validColor', '\n-%}', configSetup, configResult, {}, false), empty);
const invalid = runUcodeSnippet(file, 'function validColor', '\n-%}', configSetup, configResult, {
primary: 'red;}', blur: '-1', transparency: '2', mode: 'unknown'
});
assert.deepEqual(invalid, empty);
const dark = runUcodeSnippet(file, 'function validColor', '\n-%}', configSetup, configResult, {
primary: '#123456', dark_primary: '#abcdef', blur: '0', transparency: '0.7', mode: 'dark'
});
assert.equal(dark.mode, 'dark');
assert.equal(dark.bar_color, '#abcdef');
assert.equal(dark.blur_radius, '0');
assert.equal(dark.blur_opacity, '0.7');
const light = runUcodeSnippet(file, 'function validColor', '\n-%}', configSetup, configResult, {
primary: '#123456', mode: 'light'
});
assert.equal(light.mode, 'light');
assert.equal(light.bar_color, '#123456');
test('shared head uses safe defaults for missing and invalid settings', () => {
const empty = runUcodeSnippet('ucode/template/themes/argon/head.ut', 'function validColor', '\n-%}', configSetup, configResult);
assert.deepEqual(empty, {
mode: 'normal', primary: '#5e72e4', dark_primary: '#5a6cc5',
blur_radius: '10', blur_radius_dark: '10',
blur_opacity: '0.5', blur_opacity_dark: '0.5', bar_color: '#5e72e4'
});
}
assert.deepEqual(runUcodeSnippet('ucode/template/themes/argon/head.ut', 'function validColor', '\n-%}', configSetup, configResult, {}, false), empty);
const invalid = runUcodeSnippet('ucode/template/themes/argon/head.ut', 'function validColor', '\n-%}', configSetup, configResult, {
primary: 'red;}', blur: '-1', transparency: '2', mode: 'unknown'
});
assert.deepEqual(invalid, empty);
const dark = runUcodeSnippet('ucode/template/themes/argon/head.ut', 'function validColor', '\n-%}', configSetup, configResult, {
primary: '#123456', dark_primary: '#abcdef', blur: '0', transparency: '0.7', mode: 'dark'
});
assert.equal(dark.mode, 'dark');
assert.equal(dark.bar_color, '#abcdef');
assert.equal(dark.blur_radius, '0');
assert.equal(dark.blur_opacity, '0.7');
const light = runUcodeSnippet('ucode/template/themes/argon/head.ut', 'function validColor', '\n-%}', configSetup, configResult, {
primary: '#123456', mode: 'light'
});
assert.equal(light.mode, 'light');
assert.equal(light.bar_color, '#123456');
});
test('login layouts allow supported styles and fall back for old or invalid configs', () => {
const selected = (value, hasConfig = true) => runUcodeSnippet(
'ucode/template/themes/argon/sysauth.ut',
"let login_style = 'classic';", '\n\t// Per-request',
"let login_style = 'classic';", '\n\tconst boardinfo',
configSetup + '\nlet fs = { access };', 'login_style', { login_style: value }, hasConfig
);
assert.equal(selected('classic'), 'classic');
@@ -137,7 +193,7 @@ let fs = { lsdir: path => ['photo.jpg', 'my.photo.jpg', 'space name.webp', 'hash
let http = { urlencode: value => replace(replace(replace(value, ' ', '%20'), '#', '%23'), '?', '%3F') };
`;
const backgrounds = runUcodeSnippet(
'ucode/template/themes/argon/sysauth.ut',
'ucode/template/themes/argon/login_background.ut',
'const imageTypes', '\n\tfunction selectBackground',
setup, 'fetchMedia("/www/luci-static/argon/background", "/luci-static/argon/background/")'
);
@@ -166,14 +222,14 @@ function fetchMedia() {
}
function randomIndex() { return 0; }
`;
const file = 'ucode/template/themes/argon/sysauth.ut';
const file = 'ucode/template/themes/argon/login_background.ut';
const start = 'function selectBackground';
const end = '\n\tconst boardinfo';
const end = '\n\tconst bgArry';
const selected = settings => runUcodeSnippet(file, start, end, setup, 'selectBackground("/luci-static/argon/background/")', settings);
assert.equal(selected({ url: 'https://example.com/image.jpg' }).bgUrl, 'https://example.com/image.jpg');
for (const invalid of ['http://example.com/image.jpg', 'https://example.com/a b.jpg', 'javascript:alert(1)', 'https://example.com/"bad'])
assert.equal(selected({ url: invalid }).bgUrl, '/luci-static/argon/background/local.jpg');
assert.equal(selected({ url: 'http://example.com/image.jpg', empty: true }).bgUrl, '/luci-static/argon/img/bg1.jpg');
assert.equal(selected({ url: 'http://example.com/image.jpg', empty: true }).bgUrl, '/luci-static/argon/img/bg1.webp');
assert.deepEqual(selected({ url: '', video: true }), {
bgUrl: '/luci-static/argon/background/local.mp4',
backgroundType: 'Video',
@@ -182,7 +238,7 @@ function randomIndex() { return 0; }
});
test('authentication fields preserve numeric length limits and escape attributes', () => {
const source = fs.readFileSync(path.join(root, 'ucode/template/themes/argon/sysauth.ut'), 'utf8');
const source = fs.readFileSync(path.join(root, 'ucode/template/themes/argon/login_form.ut'), 'utf8');
const first = source.indexOf('{%- if (auth_fields): %}');
const last = source.indexOf('{%- if (auth_html): %}', first);
assert.ok(first >= 0 && last > first);