Release v2.14.2
This commit is contained in:
1 parent
c3299abaac
commit
ddcc665f81
47 files changed
+1209
-66
No files matched your search
@@ -34,7 +34,7 @@ test('font sweep registers a generic cleanup recurring task without player metad
|
||||
await registeredTask.run();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['put', '/media/fonts/fonts.json', '/tmp/media'],
|
||||
['put', '/media/fonts/fonts.css', '/tmp/media']
|
||||
['put', '/media/assets/fonts.json', '/tmp/media'],
|
||||
['put', '/media/assets/fonts.css', '/tmp/media']
|
||||
]);
|
||||
});
|
||||
@@ -64,6 +64,7 @@ test('dashboard state counts active players from the registry heartbeat', async
|
||||
const state = await service.buildDashboardState();
|
||||
|
||||
assert.equal(state.connectedPlayersCount, 6);
|
||||
assert.equal(state.playerServiceConnected, true);
|
||||
assert.equal(state.connectedClientsCount, 1);
|
||||
assert.equal(state.clients[0].player_identifier, 'player-alpha');
|
||||
assert.equal(state.kioskPlayers.length, 1);
|
||||
|
||||
@@ -30,6 +30,66 @@ test('buildFontStylesheet keeps disabled fonts available for rendering', () => {
|
||||
|
||||
assert.match(stylesheet, /Enabled Font/);
|
||||
assert.match(stylesheet, /Disabled Font/);
|
||||
assert.match(stylesheet, /\/media\/assets\/fonts\/enabled-font.ttf/);
|
||||
});
|
||||
|
||||
test('legacy fonts and weather icons migrate into shared assets without losing metadata', async () => {
|
||||
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-assets-'));
|
||||
try {
|
||||
const fonts = path.join(root, 'fonts');
|
||||
const weather = path.join(root, 'weather-icons', 'abc');
|
||||
await fs.promises.mkdir(fonts, { recursive: true });
|
||||
await fs.promises.mkdir(weather, { recursive: true });
|
||||
await fs.promises.writeFile(path.join(fonts, 'custom.ttf'), 'font-data');
|
||||
await fs.promises.writeFile(path.join(fonts, 'fonts.json'), JSON.stringify([{ id: 'custom', family: 'Custom', fileName: 'custom.ttf', enabled: false }]));
|
||||
await fs.promises.writeFile(path.join(fonts, 'fonts.css'), "src:url('/media/fonts/custom.ttf')");
|
||||
await fs.promises.writeFile(path.join(weather, 'clear-123abc.png'), 'icon-data');
|
||||
await fs.promises.writeFile(path.join(root, 'weather-icons', 'sets.json'), JSON.stringify([{ id: 'abc', name: 'Custom', icons: { clear: '/media/weather-icons/abc/clear-123abc.png' } }]));
|
||||
const library = require('../src/web/lib/media/font-library');
|
||||
await library.ensureFontLibrary(path.join(root, 'uploads'));
|
||||
const loaded = library.loadFontLibrary(root);
|
||||
assert.equal(loaded.fonts[0].id, 'custom');
|
||||
assert.equal(loaded.fonts[0].enabled, false);
|
||||
assert.equal(loaded.manifestPath, path.join(root, 'assets', 'fonts.json'));
|
||||
assert.equal(loaded.stylesheetPath, path.join(root, 'assets', 'fonts.css'));
|
||||
assert.match(loaded.stylesheetHref, /^\/media\/assets\/fonts.css/);
|
||||
assert.equal(fs.readFileSync(path.join(root, 'assets', 'fonts', 'custom.ttf'), 'utf8'), 'font-data');
|
||||
assert.match(fs.readFileSync(loaded.stylesheetPath, 'utf8'), /\/media\/assets\/fonts\/custom.ttf/);
|
||||
const weatherLibrary = require('../src/web/lib/media/weather-icon-library');
|
||||
assert.equal(weatherLibrary.readWeatherIconSets(root)[0].icons.clear, '/media/assets/weather-icons/abc/clear-123abc.png');
|
||||
assert.equal(weatherLibrary.getWeatherManifestPath(root), path.join(root, 'assets', 'weather.json'));
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets', 'weather-icons', 'sets.json')), false);
|
||||
assert.equal(fs.readFileSync(path.join(root, 'assets', 'weather-icons', 'abc', 'clear-123abc.png'), 'utf8'), 'icon-data');
|
||||
assert.equal(fs.existsSync(fonts), false);
|
||||
assert.equal(fs.existsSync(path.join(root, 'weather-icons')), false);
|
||||
await library.ensureFontLibrary(root);
|
||||
assert.equal(library.loadFontLibrary(root).fonts.length, 1);
|
||||
} finally {
|
||||
await fs.promises.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('shared assets migration tolerates simultaneous service startup', async () => {
|
||||
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-assets-race-'));
|
||||
try {
|
||||
const fonts = path.join(root, 'fonts');
|
||||
await fs.promises.mkdir(fonts);
|
||||
await fs.promises.writeFile(path.join(fonts, 'fonts.json'), '[]');
|
||||
await fs.promises.writeFile(path.join(fonts, 'fonts.css'), "src:url('/media/fonts/custom.ttf')");
|
||||
await Promise.all(Array.from({ length: 100 }, function (_unused, index) {
|
||||
return fs.promises.writeFile(path.join(fonts, 'font-' + index + '.ttf'), 'font-data');
|
||||
}));
|
||||
const run = require('node:util').promisify(require('node:child_process').execFile);
|
||||
const modulePath = require.resolve('../src/web/lib/media/assets-storage');
|
||||
await Promise.all(Array.from({ length: 4 }, function () {
|
||||
return run(process.execPath, ['-e', 'require(' + JSON.stringify(modulePath) + ').migrateLegacyAssets(' + JSON.stringify(root) + ')']);
|
||||
}));
|
||||
assert.equal(fs.existsSync(fonts), false);
|
||||
assert.equal(fs.readdirSync(path.join(root, 'assets', 'fonts')).length, 100);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(root, 'assets', 'fonts.json'))).length, 0);
|
||||
} finally {
|
||||
await fs.promises.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('collectFontLibrarySyncOperations keeps disabled fonts as puts', async () => {
|
||||
@@ -43,7 +103,7 @@ test('collectFontLibrarySyncOperations keeps disabled fonts as puts', async () =
|
||||
|
||||
const operations = collectFontLibrarySyncOperations(tempRoot);
|
||||
const fontOps = operations.filter(function (operation) {
|
||||
return String(operation.uploadPath || '').indexOf('/media/fonts/') === 0 && /\.ttf$/.test(String(operation.uploadPath || ''));
|
||||
return String(operation.uploadPath || '').indexOf('/media/assets/fonts/') === 0 && /\.ttf$/.test(String(operation.uploadPath || ''));
|
||||
});
|
||||
|
||||
assert.deepEqual(fontOps.map(function (operation) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function loadRegionRenderer(regionType, dataKey, data) {
|
||||
let renderer;
|
||||
const window = {
|
||||
placeholderUtils: {},
|
||||
pulsePlayerRegionTypes: {
|
||||
register: function (_type, module) {
|
||||
renderer = module;
|
||||
}
|
||||
},
|
||||
pulsePlayerRenderingShared: {
|
||||
normalizeRegionValue: function (value) {
|
||||
return String(value || '');
|
||||
}
|
||||
}
|
||||
};
|
||||
const context = {
|
||||
window,
|
||||
initialData: { [dataKey]: data },
|
||||
escapeHtml: String,
|
||||
sanitizeFontFamily: String,
|
||||
sanitizeFontSize: function (value) { return Number(value) || 16; },
|
||||
sanitizeTextColor: String,
|
||||
sanitizeRichText: String,
|
||||
renderEditorJsContent: String
|
||||
};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), context);
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/player/regions', regionType + '.js'), 'utf8'), context);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
test('image placeholders align inline with text in player API and RSS regions', function () {
|
||||
const sourceUrl = 'https://example.com/image.png';
|
||||
const api = loadRegionRenderer('api', 'apiSources', [{
|
||||
id: 1,
|
||||
responseJson: { image: sourceUrl }
|
||||
}]);
|
||||
const rss = loadRegionRenderer('rss', 'rssFeeds', [{
|
||||
id: 1,
|
||||
items: [{ image: sourceUrl }]
|
||||
}]);
|
||||
const region = { pixelWidth: 400, pixelHeight: 100, canvasScale: 1, baseStyle: '' };
|
||||
|
||||
for (const [renderer, contentKey] of [[api, 'source_id'], [rss, 'feed_id']]) {
|
||||
const content = { [contentKey]: 1, value: 'Before {{image.image(120,80,bottom)}} after' };
|
||||
const rendered = renderer.renderRegion(region, content);
|
||||
assert.match(rendered, /vertical-align:bottom/);
|
||||
assert.match(rendered, /width:120px;height:80px/);
|
||||
|
||||
const legacy = renderer.renderRegion(region, { [contentKey]: 1, value: 'Before {{image.image(120,80)}} after' });
|
||||
assert.doesNotMatch(legacy, /vertical-align:/);
|
||||
assert.match(legacy, /width:120px;height:80px/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
require('../src/common');
|
||||
const library = require('../src/web/lib/media/weather-icon-library');
|
||||
const weatherIcons = require('../src/web/public/js/shared/weather-icons');
|
||||
const vm = require('node:vm');
|
||||
const placeholderContext = { window: {} };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/shared/placeholder-utils.js'), 'utf8'), placeholderContext);
|
||||
const placeholders = placeholderContext.window.placeholderUtils;
|
||||
|
||||
test('weather icon picker warns about each missing condition', function () {
|
||||
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/weather-icons.js'), 'utf8');
|
||||
assert.match(source, /window\.showToast\('Add images for: ' \+ labels\.join\(', '\) \+ '\.', 'warning'\)/);
|
||||
assert.match(source, /replace\(\/\^Upload\\s\+\|\\s\+icon\$\/g, ''\)/);
|
||||
});
|
||||
|
||||
test('weather editor and player render day or night variants from one selected set', function () {
|
||||
const sets = [{ id: 'abc', name: 'Colour', icons: { clear: '/media/weather-icons/abc/clear-123abc.png' }, nightIcons: { clear: '/media/weather-icons/abc/night-clear-456def.png' } }];
|
||||
const location = { id: 1, responseJson: { current: { weather_code: 0, is_day: 1 } } };
|
||||
let editor;
|
||||
const editorContext = { window: { pulseWeatherIconSets: sets, pulseWeatherIcons: weatherIcons, placeholderUtils: placeholders,
|
||||
pulseRegionTypes: { register: function (_type, module) { editor = module; } } }, document: { addEventListener: function () {} } };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/web/public/js/regions/type/weather.js'), 'utf8'), editorContext);
|
||||
const region = { id: 1, region_key: 'weather', label: 'Forecast', baseStyle: '' };
|
||||
const content = { weather_location_id: 1, weather_icon_set_id: 'abc', value: '{{current.icon(48,24)}}' };
|
||||
const card = editor.renderEditorCard({ region: region, current: content, weatherLocations: [location] });
|
||||
assert.match(card, /name="region_weather_icon_set_id_1"/);
|
||||
assert.match(card, /value="abc" selected/);
|
||||
assert.match(card, /row g-3 align-items-end"><div class="col-12 col-lg-6"/);
|
||||
assert.match(card, /region_weather_forecast_mode_1"[^]*?region_weather_icon_set_id_1/);
|
||||
assert.doesNotMatch(card, /region_weather_night_icon_set_id_1/);
|
||||
sets[0].enabled = false;
|
||||
const disabledCard = editor.renderEditorCard({ region: region, current: content, weatherLocations: [location] });
|
||||
assert.match(disabledCard, /<option value="abc" selected>Colour<\/option>/);
|
||||
assert.doesNotMatch(disabledCard, /value="abc" selected disabled/);
|
||||
sets[0].enabled = true;
|
||||
const preview = editor.renderPreview(region, content, { weatherLocations: [location] });
|
||||
assert.match(preview, /clear-123abc.png/);
|
||||
assert.match(preview, /width:48px;height:24px/);
|
||||
const topPreview = editor.renderPreview(region, Object.assign({}, content, { value: '{{current.icon(48,96,top)}}' }), { weatherLocations: [location] });
|
||||
assert.match(topPreview, /vertical-align:top/);
|
||||
let player;
|
||||
const playerContext = { window: { pulseWeatherIcons: weatherIcons, placeholderUtils: placeholders,
|
||||
pulsePlayerRegionTypes: { register: function (_type, module) { player = module; } } },
|
||||
initialData: { weatherLocations: [location], weatherIconSets: sets }, escapeHtml: String, renderEditorJsContent: String };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../src/player/regions/weather.js'), 'utf8'), playerContext);
|
||||
assert.match(player.renderRegion(region, content), /clear-123abc.png/);
|
||||
assert.match(player.renderRegion(region, Object.assign({}, content, { value: '{{current.icon(48,96,bottom)}}' })), /vertical-align:bottom/);
|
||||
location.responseJson.current.is_day = 0;
|
||||
assert.match(player.renderRegion(region, content), /night-clear-456def.png/);
|
||||
location.responseJson.current.is_day = 1;
|
||||
assert.match(player.renderRegion(region, Object.assign({}, content, { weather_icon_set_id: '' })), /bi-sun/);
|
||||
location.responseJson.current.weather_code = 61;
|
||||
assert.match(player.renderRegion(region, content), /bi-cloud-rain/);
|
||||
});
|
||||
|
||||
test('weather icons map exact WMO codes and fall back for missing or unsafe images', function () {
|
||||
assert.equal(weatherIcons.conditionForCode(3), 'overcast');
|
||||
assert.equal(weatherIcons.conditionForCode(85), 'snow');
|
||||
assert.equal(weatherIcons.conditionForCode(null), '');
|
||||
assert.equal(weatherIcons.conditionForCode(4), '');
|
||||
const sets = [{ id: 'abc', icons: { clear: '/media/weather-icons/abc/clear-123abc.png', rain: 'javascript:alert(1)' } }];
|
||||
assert.equal(weatherIcons.imageForCode(sets, 'abc', 0), sets[0].icons.clear);
|
||||
assert.equal(weatherIcons.imageForCode(sets, 'abc', 61), '');
|
||||
assert.equal(weatherIcons.imageForCode(sets, 'abc', 45), '');
|
||||
sets[0].enabled = false;
|
||||
assert.equal(weatherIcons.imageForCode(sets, 'abc', 0), sets[0].icons.clear);
|
||||
assert.match(weatherIcons.renderImage(sets[0].icons.clear, 48, 24), /width:48px;height:24px/);
|
||||
assert.match(weatherIcons.renderImage(sets[0].icons.clear, 48, 24), /display:inline-flex;vertical-align:middle;align-items:center;justify-content:center/);
|
||||
assert.match(weatherIcons.renderImage(sets[0].icons.clear, 48, 24), /vertical-align:middle.*line-height:0/);
|
||||
assert.match(weatherIcons.renderImage(sets[0].icons.clear, 48, 96, 'bottom'), /vertical-align:bottom/);
|
||||
assert.match(weatherIcons.renderImage(sets[0].icons.clear, 48, 24), /object-fit:contain/);
|
||||
});
|
||||
|
||||
test('weather management template renders Assets and permission gated uploads', function () {
|
||||
const html = require('../src/web/view').renderView('settings/weather-icons/list', {
|
||||
title: 'Weather icons', active: 'weather-icons', assetsMenuOpen: true, conditions: library.CONDITIONS,
|
||||
sets: [], currentUser: { permissionKeys: ['weather-icons.read', 'weather-icons.create'] }
|
||||
});
|
||||
assert.match(html, /Weather icons/);
|
||||
assert.match(html, /<p>Assets/);
|
||||
assert.match(html, /nav-icon bi bi-folder2-open/);
|
||||
assert.ok(html.indexOf('User management') < html.indexOf('Assets'));
|
||||
assert.match(html, /action="\/settings\/weather-icons"/);
|
||||
assert.match(html, /name="icon_clear"/);
|
||||
assert.match(html, /class="page-header"/);
|
||||
assert.match(html, /Upload and manage shared icon sets for weather regions/);
|
||||
assert.match(html, /class="card card-outline card-primary mb-4 collapsed-card"/);
|
||||
assert.match(html, /class="card-title mb-0">Upload icon set/);
|
||||
assert.doesNotMatch(html, /Add a PNG or WebP image for every weather condition/);
|
||||
assert.match(html, /data-lte-toggle="card-collapse"/);
|
||||
assert.match(html, /data-lte-icon="expand"/);
|
||||
assert.match(html, /data-lte-icon="collapse"/);
|
||||
assert.match(html, /action="\/settings\/weather-icons" enctype="multipart\/form-data" class="card-body p-0"/);
|
||||
assert.match(html, /class="card-title">Managed icon sets/);
|
||||
assert.match(html, /data-table-sort-key="status">Status/);
|
||||
assert.equal((html.match(/class="app-content"/g) || []).length, 1);
|
||||
assert.doesNotMatch(html, /<details|<summary/);
|
||||
assert.match(html, /card-footer d-flex justify-content-end/);
|
||||
assert.match(html, /data-table-search-container data-table-pagination-card/);
|
||||
assert.match(html, /data-table-searchable/);
|
||||
assert.match(html, /data-table-search-empty-default/);
|
||||
assert.equal((html.match(/data-weather-icon-picker/g) || []).length, library.CONDITIONS.length * 2);
|
||||
assert.equal((html.match(/weather-icon-upload-row-label">Day icons/g) || []).length, 1);
|
||||
assert.equal((html.match(/Night icons <span>Optional/g) || []).length, 1);
|
||||
assert.match(html, /data-weather-icon-preview/);
|
||||
assert.match(html, /data-weather-icon-status>Required/);
|
||||
assert.match(html, /hidden aria-label="Clear icon" data-weather-icon-file/);
|
||||
assert.match(html, /accept="image\/png,image\/webp,image\/svg\+xml"/);
|
||||
assert.match(html, /data-weather-icon-set-form/);
|
||||
});
|
||||
|
||||
test('weather management route paginates, sorts and preserves search state', async function () {
|
||||
const originalRead = library.readWeatherIconSets;
|
||||
const handlers = {};
|
||||
library.readWeatherIconSets = function () {
|
||||
return Array.from({ length: 30 }, function (_unused, index) {
|
||||
return { id: 'set-' + index, name: 'Set ' + String(index).padStart(2, '0'), icons: {} };
|
||||
});
|
||||
};
|
||||
try {
|
||||
require('../src/web/routes/settings/weather-icons')({
|
||||
get: function (url, ...callbacks) { handlers[url] = callbacks; },
|
||||
post: function () {}
|
||||
}, { mediaDir: 'media', pool: { query: async function () { return [[]]; } }, requirePermission: function () { return function () {}; } });
|
||||
const response = { status: function () { return this; }, send: function (html) { this.html = html; } };
|
||||
const currentUser = { permissionKeys: ['weather-icons.read', 'weather-icons.update'] };
|
||||
const fail = function (error) { throw error; };
|
||||
await handlers['/settings/weather-icons'][1]({ query: { page: '2', sort: 'name', direction: 'asc' }, currentUser: currentUser }, response, fail);
|
||||
assert.equal((response.html.match(/<tr data-table-search-row>/g) || []).length, 5);
|
||||
assert.match(response.html, /Set 25/);
|
||||
assert.doesNotMatch(response.html, /Set 00/);
|
||||
assert.match(response.html, /data-bs-toggle="modal"/);
|
||||
assert.match(response.html, /href="\/assets\/css\/weather-icons.css"/);
|
||||
await handlers['/settings/weather-icons'][1]({ query: { search: 'Set 29', page: '2' }, currentUser: currentUser }, response, fail);
|
||||
assert.equal((response.html.match(/<tr data-table-search-row>/g) || []).length, 1);
|
||||
assert.match(response.html, /Set 29/);
|
||||
await handlers['/settings/weather-icons'][1]({ query: { search: 'Set', page: '2' }, currentUser: currentUser }, response, fail);
|
||||
assert.match(response.html, /search(?:=|=)Set/);
|
||||
} finally {
|
||||
library.readWeatherIconSets = originalRead;
|
||||
}
|
||||
});
|
||||
|
||||
test('weather sets require every condition image and support replacement', async function () {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'weather-icons-'));
|
||||
try {
|
||||
const buffer = await sharp({ create: { width: 16, height: 16, channels: 4, background: '#ff0000' } }).png().toBuffer();
|
||||
const allFiles = library.CONDITIONS.map(function (condition) { return { fieldname: 'icon_' + condition.key, buffer: buffer }; });
|
||||
const svgBuffer = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#00f"><animate attributeName="fill" values="#00f;#0ff;#00f" dur="1s" repeatCount="indefinite"/></rect></svg>');
|
||||
await assert.rejects(library.saveWeatherIconSet(root, 'Partial', allFiles.slice(0, 2)), /every weather condition.*Overcast/);
|
||||
const first = await library.saveWeatherIconSet(root, 'Colour', allFiles);
|
||||
const second = await library.saveWeatherIconSet(root, 'Outline', allFiles);
|
||||
assert.notEqual(first.id, second.id);
|
||||
assert.equal(Object.keys(first.icons).length, library.CONDITIONS.length);
|
||||
assert.match(first.icons.clear, /^\/media\/assets\/weather-icons\//);
|
||||
const disabled = library.setWeatherIconSetEnabled(root, first.id, false);
|
||||
assert.equal(disabled.enabled, false);
|
||||
assert.equal(library.readWeatherIconSets(root).find(function (set) { return set.id === first.id; }).enabled, false);
|
||||
assert.ok(library.collectWeatherIconSyncOperations(root).some(function (operation) {
|
||||
return operation.type === 'put' && operation.uploadPath === first.icons.clear;
|
||||
}));
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets', 'weather-icons', first.id, path.basename(first.icons.clear))), true);
|
||||
library.setWeatherIconSetEnabled(root, first.id, true);
|
||||
assert.equal(library.getWeatherManifestPath(root), path.join(root, 'assets', 'weather.json'));
|
||||
const svgSet = await library.saveWeatherIconSet(root, 'Vector', library.CONDITIONS.map(function (condition) { return { fieldname: 'icon_' + condition.key, buffer: svgBuffer }; }));
|
||||
const svgFileName = path.basename(svgSet.icons.clear);
|
||||
assert.match(svgFileName, /\.svg$/);
|
||||
assert.match(fs.readFileSync(path.join(root, 'assets', 'weather-icons', svgSet.id, svgFileName), 'utf8'), /<animate/);
|
||||
const replacementBuffer = await sharp({ create: { width: 16, height: 16, channels: 4, background: '#0000ff' } }).png().toBuffer();
|
||||
const updated = await library.saveWeatherIconSet(root, 'Colour updated', [{ fieldname: 'icon_snow', buffer: buffer }, { fieldname: 'night_icon_clear', buffer: replacementBuffer }], first.id);
|
||||
assert.equal(Object.keys(updated.icons).length, library.CONDITIONS.length);
|
||||
assert.match(updated.nightIcons.clear, /night-clear-/);
|
||||
assert.equal(library.readWeatherIconSets(root).length, 3);
|
||||
assert.deepEqual(library.collectWeatherIconRemovalOperations([first], [updated]), []);
|
||||
const replaced = await library.saveWeatherIconSet(root, 'Colour updated', [{ fieldname: 'icon_clear', buffer: replacementBuffer }], first.id);
|
||||
assert.deepEqual(library.collectWeatherIconRemovalOperations([updated], [replaced]), [{ type: 'delete', uploadPath: updated.icons.clear }]);
|
||||
const removed = library.deleteWeatherIconSet(root, second.id);
|
||||
const removalOperations = library.collectWeatherIconRemovalOperations([removed], []);
|
||||
assert.equal(removalOperations.length, library.CONDITIONS.length);
|
||||
assert.deepEqual(removalOperations.find(function (operation) { return operation.uploadPath === second.icons.snow; }), { type: 'delete', uploadPath: second.icons.snow });
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets', 'weather-icons', second.id)), false);
|
||||
await assert.rejects(library.saveWeatherIconSet(root, 'Colour updated', []), /already exists/);
|
||||
await assert.rejects(library.saveWeatherIconSet(root, 'Invalid', allFiles.map(function (file, index) {
|
||||
return index === 0 ? { fieldname: file.fieldname, buffer: Buffer.from('not an image') } : file;
|
||||
})));
|
||||
await assert.rejects(library.saveWeatherIconSet(root, 'Unsafe SVG', library.CONDITIONS.map(function (condition) {
|
||||
return { fieldname: 'icon_' + condition.key, buffer: Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><script>alert(1)</script></svg>') };
|
||||
})), /cannot contain scripts/);
|
||||
assert.equal(library.readWeatherIconSets(root).length, 2);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('weather icon usage recognizes one set reference', function () {
|
||||
const content = { weather: { weather_icon_set_id: 'set' } };
|
||||
const isUsed = function (setId) {
|
||||
return Object.values(content).some(function (region) {
|
||||
return region && region.weather_icon_set_id === setId;
|
||||
});
|
||||
};
|
||||
assert.equal(isUsed('set'), true);
|
||||
assert.equal(isUsed('other'), false);
|
||||
});
|
||||
Reference in new issue
Block a user