Files
gree-controller/scripts/test_flow_block_help_ui.js
2026-10-20 22:52:13 +02:00

96 lines
5.9 KiB
JavaScript

// Verify contextual help for the constant and room heating Flow blocks.
// Does not need a browser or a running HVAC controller.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
const root = path.resolve(__dirname, '..');
const translations = {};
for (const language of ['pl', 'en']) {
translations[language] = JSON.parse(fs.readFileSync(path.join(root, 'lang', `${language}.json`), 'utf8')).translations;
}
function localize(language, key, params = {}) {
const result = translations[language][key];
assert.ok(result, `missing translation ${language}:${key}`);
return String(result).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => params[name] ?? `{${name}}`);
}
const app = {
zones: [{ id: 'zone1', name: 'Pokój' }],
devices: [], groups: [], flowDraft: null, flowSelectedNodeId: null,
};
const host = {
innerHTML: '',
expandedNode: null,
classList: { toggle() {}, remove() {} },
querySelector(selector) {
return selector === '.flow-block-help' && this.expandedNode
? { open: true, dataset: { flowHelpNode: this.expandedNode } } : null;
},
};
const ctx = vm.createContext({
app,
tr: (key, params) => localize(ctx.language, key, params),
language: 'pl',
esc: value => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;'),
fmtTemp: value => `${Number(value).toFixed(1)}°C`,
uiIcon: () => '',
$: selector => selector === '#flowInspector' ? host : null,
document: { addEventListener() {} }, window: { addEventListener() {} },
});
vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/flows.js'), 'utf8'), ctx);
const meta = vm.runInContext('FLOW_NODE_META', ctx);
assert.equal(meta.constant.descriptionKey, 'flow.constantDescription');
assert.equal(meta.zone_heat_cycle.descriptionKey, 'flow.simpleHeatDescription');
for (const language of ['pl', 'en']) {
ctx.language = language;
for (const file of fs.readdirSync(path.join(root, 'flows/simple-heating'))) {
if (!file.endsWith('.flow.json')) continue;
const json = JSON.parse(fs.readFileSync(path.join(root, 'flows/simple-heating', file), 'utf8'));
const setting = json.flow.nodes.find(node => node.kind === 'zone_heat_cycle').config;
const text = ctx.flowSimpleHeatHelp(setting, 'room', false);
const night = setting.night_target;
assert.ok(text.includes(`${night.toFixed(1)}°C`), `${language} ${file} missing room target`);
assert.ok(text.includes(`${(night - 0.8).toFixed(1)}°C`), `${language} ${file} missing boost threshold`);
assert.ok(text.includes(`${Math.floor(night - 1).toFixed(1)}°C`), `${language} ${file} missing standby`);
assert.ok(text.includes(`${Math.floor(night) + 1}.0°C`), `${language} ${file} missing gentle heat`);
assert.ok(text.includes(`${Math.ceil(night + 2)}.0°C`), `${language} ${file} missing boost`);
assert.ok(text.includes(`${setting.day_min.toFixed(1)}°C`), `${language} ${file} missing daytime minimum`);
assert.ok(text.includes('21.0°C') && text.includes('22.0°C'), `${language} ${file} missing daytime setpoints`);
assert.ok(text.includes('19.2°C'), `${language} ${file} missing daytime immediate boost threshold`);
assert.ok(text.includes(String(setting.day_off_after_minutes)), `${language} ${file} missing off timer`);
assert.ok(text.includes(String(setting.boost_after_minutes)), `${language} ${file} missing boost timer`);
assert.ok(text.includes(localize(language, 'flow.simpleHeatHelpSensors')));
}
assert.ok(ctx.flowSimpleHeatHelp({ night_target: 22.4, day_target: 21.3, day_min: 20.6, boost_after_minutes: 12, day_off_after_minutes: 45 }).includes('25.0°C'), 'help must recalculate for edited targets');
}
ctx.language = 'pl';
function inspect(kind, config) {
app.flowDraft = { nodes: [{ id: 'selected', kind, config }], edges: [] };
app.flowSelectedNodeId = 'selected';
ctx.renderFlowInspector();
return host.innerHTML;
}
const constantHtml = inspect('constant', { value: true });
assert.ok(constantHtml.includes(localize('pl', 'flow.constantDescription')));
assert.ok(constantHtml.includes(localize('pl', 'flow.constantHelp')));
assert.ok(constantHtml.includes('data-flow-config="value"'));
const heat = { zone_id: 'zone1', day_target: 20, night_target: 20.6, day_min: 19.7, boost_after_minutes: 10, day_off_after_minutes: 30 };
const simpleHtml = inspect('zone_heat_cycle', heat);
assert.ok(simpleHtml.includes(localize('pl', 'flow.simpleHeatDescription')));
assert.ok(simpleHtml.includes('<details class="flow-block-help"'));
assert.ok(simpleHtml.includes(localize('pl', 'flow.simpleHeatHowItWorks')));
assert.ok(simpleHtml.includes('19.0°C') && simpleHtml.includes('23.0°C'));
assert.ok(simpleHtml.includes('data-flow-config="night_target"'));
host.expandedNode = 'selected';
assert.ok(inspect('zone_heat_cycle', heat).includes('data-flow-help-node="selected" open>'), 'expanded help should stay open when a user edits settings');
const htmlFile = fs.readFileSync(path.join(root, 'web/index.html'), 'utf8');
assert.ok(htmlFile.includes('data-i18n-title="flow.constantDescription"'));
assert.ok(htmlFile.includes('data-i18n-title="flow.simpleHeatDescription"'));
console.log('OK: 2 blocks, 3 rooms, PL/EN tips, dynamic temperature examples, inspector and expanded-state behavior');
if (process.env.FLOW_HELP_PREVIEW_HTML) {
host.expandedNode = 'selected';
const content = inspect('zone_heat_cycle', heat);
fs.writeFileSync(process.env.FLOW_HELP_PREVIEW_HTML, `<!doctype html><html lang="pl"><head><meta charset="utf-8"><link rel="stylesheet" href="file://${path.join(root, 'web/css/styles.css')}"><style>body{margin:0;padding:28px;background:var(--bg)} .flow-inspector{width:370px;max-height:none;box-sizing:border-box;border:1px solid var(--line-strong);border-radius:14px} .flow-inspector-head button{font-size:11px;padding:7px}</style></head><body><aside class="flow-inspector">${content}</aside></body></html>`);
}