296 lines
9.1 KiB
JavaScript
296 lines
9.1 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
|
|
function loadScheduleModule(overrides) {
|
|
const timeDatePlaceholdersScript = fs.readFileSync(require.resolve('../src/web/public/js/shared/time-date-placeholders.js'), 'utf8');
|
|
const placeholderScript = fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8');
|
|
const scriptPath = require.resolve('../src/web/public/js/regions/type/schedule.js');
|
|
const script = fs.readFileSync(scriptPath, 'utf8');
|
|
const registry = new Map();
|
|
const customWindow = overrides && overrides.window ? overrides.window : {};
|
|
const sandbox = {
|
|
document: {
|
|
addEventListener() {}
|
|
},
|
|
window: {
|
|
pulseRegionTypes: {
|
|
register(type, module) {
|
|
registry.set(type, module);
|
|
}
|
|
},
|
|
pulseRegionUtils: {
|
|
escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
},
|
|
sanitizeRichText(value) {
|
|
return String(value === undefined || value === null ? '' : value);
|
|
}
|
|
},
|
|
placeholderChips: {
|
|
renderChips(tokens) {
|
|
return tokens.map((token) => '<span class="chip">{{' + token + '}}</span>').join(' ');
|
|
}
|
|
},
|
|
initialData: {
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'Main board',
|
|
timezone: 'UTC',
|
|
entries: [
|
|
{
|
|
id: 101,
|
|
title: 'Launch',
|
|
short_description: 'Doors open',
|
|
start_datetime: '2026-08-09T10:00:00.000Z',
|
|
end_datetime: '2026-08-09T11:00:00.000Z'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
Intl: Intl,
|
|
Date: Date,
|
|
Object: Object,
|
|
Array: Array,
|
|
Number: Number,
|
|
String: String,
|
|
Boolean: Boolean,
|
|
Math: Math,
|
|
JSON: JSON,
|
|
RegExp: RegExp,
|
|
console: console
|
|
},
|
|
...customWindow
|
|
};
|
|
sandbox.window = Object.assign({}, sandbox.window, customWindow);
|
|
|
|
vm.runInNewContext(timeDatePlaceholdersScript, sandbox, { filename: 'time-date-placeholders.js' });
|
|
vm.runInNewContext(placeholderScript, sandbox, { filename: 'placeholder-utils.js' });
|
|
if (customWindow.placeholderUtils) {
|
|
sandbox.window.placeholderUtils = customWindow.placeholderUtils;
|
|
}
|
|
vm.runInNewContext(script, sandbox, { filename: scriptPath });
|
|
return registry.get('timetable');
|
|
}
|
|
|
|
test('timetable region editor lists timezone transforms in helper text', () => {
|
|
const module = loadScheduleModule();
|
|
const html = module.renderEditorCard({
|
|
region: { id: 47, label: 'Timetable' },
|
|
current: {
|
|
value: '<p>{{title}} {{start.tz()}} {{start.tz_short()}}</p>',
|
|
timetable_group_id: 1,
|
|
display_mode: 'current',
|
|
max_items: 3
|
|
},
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'Main board',
|
|
timezone: 'UTC',
|
|
entries: []
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.match(html, /Placeholder values support transforms/);
|
|
assert.match(html, /<code>\{\{start\.tz\(\)\}\}<\/code>/);
|
|
assert.match(html, /<code>\{\{start\.tz_short\(\)\}\}<\/code>/);
|
|
assert.doesNotMatch(html, /class="chip">\{\{start\.tz\(\)\}\}<\/span>/);
|
|
});
|
|
|
|
test('timetable region editor shows supported date format tokens', () => {
|
|
const module = loadScheduleModule();
|
|
const html = module.renderEditorCard({
|
|
region: { id: 47, label: 'Timetable' },
|
|
current: {
|
|
value: '<p>{{start.format("MMM D, YYYY h:mm A")}}</p>',
|
|
timetable_group_id: 1,
|
|
display_mode: 'current',
|
|
max_items: 3
|
|
},
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'Main board',
|
|
timezone: 'UTC',
|
|
entries: []
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.match(html, /Supported date format tokens/);
|
|
assert.match(html, /<th scope="col">Format<\/th>/);
|
|
assert.ok(html.indexOf('<td><code>D</code></td><td>1-31</td><td>The day of the month</td>') < html.indexOf('<td><code>ddd</code></td><td>Mon</td><td>The abbreviated weekday name</td>'));
|
|
assert.ok(html.indexOf('<td><code>ddd</code></td><td>Mon</td><td>The abbreviated weekday name</td>') < html.indexOf('<td><code>M</code></td><td>1-12</td><td>The month, beginning at 1</td>'));
|
|
assert.ok(html.indexOf('<td><code>YY</code></td><td>18</td><td>The two-digit year</td>') < html.indexOf('<td><code>YYYY</code></td><td>2018</td><td>The four-digit year</td>'));
|
|
assert.doesNotMatch(html, /<td><code>y<\/code><\/td>/);
|
|
assert.doesNotMatch(html, /<td><code>yyyy<\/code><\/td>/);
|
|
assert.doesNotMatch(html, /<td><code>yy<\/code><\/td>/);
|
|
assert.doesNotMatch(html, /<td><code>tz<\/code><\/td>/);
|
|
assert.doesNotMatch(html, /<td><code>tz_short<\/code><\/td>/);
|
|
assert.match(html, /Use these tokens inside <code>\.format\(\.\.\.\)<\/code>/);
|
|
});
|
|
|
|
test('timetable region preview resolves timezone placeholders from explicit timezone transforms', () => {
|
|
const module = loadScheduleModule();
|
|
const preview = module.renderPreview(
|
|
{ id: 47, width: 500, height: 280 },
|
|
{
|
|
value: '<p>{{start.tz("UTC")}} {{start.tz_short("UTC")}}</p>',
|
|
timetable_group_id: 1,
|
|
display_mode: 'both',
|
|
max_items: 3
|
|
},
|
|
{
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'Main board',
|
|
timezone: 'UTC',
|
|
entries: [
|
|
{
|
|
id: 101,
|
|
title: 'Launch',
|
|
short_description: 'Doors open',
|
|
start_datetime: '2026-08-10T10:00:00.000Z',
|
|
end_datetime: '2026-08-10T11:00:00.000Z'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
);
|
|
|
|
assert.match(preview, /UTC/);
|
|
});
|
|
|
|
test('timetable placeholder format transform respects uppercase and lowercase day period tokens', () => {
|
|
const placeholderScript = fs.readFileSync(require.resolve('../src/web/public/js/shared/placeholder-utils.js'), 'utf8');
|
|
const sandbox = {
|
|
window: {},
|
|
Intl: Intl,
|
|
Date: Date,
|
|
Object: Object,
|
|
Array: Array,
|
|
Number: Number,
|
|
String: String,
|
|
Boolean: Boolean,
|
|
Math: Math,
|
|
JSON: JSON,
|
|
RegExp: RegExp,
|
|
console: console
|
|
};
|
|
|
|
vm.runInNewContext(placeholderScript, sandbox, { filename: 'placeholder-utils.js' });
|
|
|
|
const upper = sandbox.window.placeholderUtils.resolvePlaceholderExpression(
|
|
{ start: '2026-08-09T13:05:00.000Z' },
|
|
'start.format("MMM D, YYYY h:mm A")'
|
|
);
|
|
const lower = sandbox.window.placeholderUtils.resolvePlaceholderExpression(
|
|
{ start: '2026-08-09T13:05:00.000Z' },
|
|
'start.format("MMM D, YYYY h:mm a")'
|
|
);
|
|
|
|
assert.match(String(upper), /PM$/);
|
|
assert.match(String(lower), /pm$/);
|
|
});
|
|
|
|
test('timetable timezone abbreviation changes across daylight saving boundaries', () => {
|
|
const module = loadScheduleModule();
|
|
const preview = module.renderPreview(
|
|
{ id: 47, width: 500, height: 280 },
|
|
{
|
|
value: '<p>{{start.tz_short("Europe/London")}}</p>',
|
|
timetable_group_id: 1,
|
|
display_mode: 'both',
|
|
max_items: 10
|
|
},
|
|
{
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'London board',
|
|
timezone: 'Europe/London',
|
|
entries: [
|
|
{
|
|
id: 201,
|
|
title: 'Before DST ends',
|
|
short_description: '',
|
|
start_datetime: '2026-10-24T12:00:00.000Z',
|
|
end_datetime: '2026-10-24T13:00:00.000Z'
|
|
},
|
|
{
|
|
id: 202,
|
|
title: 'After DST ends',
|
|
short_description: '',
|
|
start_datetime: '2026-10-26T12:00:00.000Z',
|
|
end_datetime: '2026-10-26T13:00:00.000Z'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
);
|
|
|
|
assert.match(preview, /BST/);
|
|
assert.match(preview, /(GMT|UTC)/);
|
|
});
|
|
|
|
test('timetable preview does not force the group timezone into placeholder transforms', () => {
|
|
const calls = [];
|
|
const module = loadScheduleModule({
|
|
window: {
|
|
placeholderUtils: {
|
|
resolvePlaceholderExpression(value, expression, options) {
|
|
calls.push({ expression, options: options || {} });
|
|
return options && options.timeZone ? options.timeZone : 'local';
|
|
},
|
|
formatPlaceholderValue(value) {
|
|
return String(value === undefined || value === null ? '' : value);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const preview = module.renderPreview(
|
|
{ id: 47, width: 500, height: 280 },
|
|
{
|
|
value: '<p>{{start.tz_short()}}</p>',
|
|
timetable_group_id: 1,
|
|
display_mode: 'both',
|
|
max_items: 3
|
|
},
|
|
{
|
|
timetableGroups: [
|
|
{
|
|
id: 1,
|
|
name: 'Madrid board',
|
|
timezone: 'Europe/Madrid',
|
|
entries: [
|
|
{
|
|
id: 101,
|
|
title: 'Launch',
|
|
short_description: 'Doors open',
|
|
start_datetime: '2026-08-10T10:00:00.000Z',
|
|
end_datetime: '2026-08-10T11:00:00.000Z'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
);
|
|
|
|
assert.match(preview, /local/);
|
|
assert.equal(calls.length > 0 ? Boolean(calls[0].options && calls[0].options.timeZone) : false, false);
|
|
});
|