// 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, '&').replace(/ `${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('
'), '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, ``); }