diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db5f2669a..c06146ae71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,8 @@ _This release is scheduled to be released on 2024-10-01._ - [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424) - [core] Updated dependencies including stylistic-eslint -- [core] Allow custom module positions by setting `allowCustomModulePositions` in `config.js` (fixes #3504, related to https://github.com/MagicMirrorOrg/MagicMirror/pull/3445) - [core] Updated SocketIO catch all to new API +- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded(fixes #3504) ### Fixed diff --git a/index.html b/index.html index b97124be10..09addfe37f 100644 --- a/index.html +++ b/index.html @@ -55,6 +55,7 @@ + diff --git a/js/app.js b/js/app.js index e18ff589da..f6d510a64a 100644 --- a/js/app.js +++ b/js/app.js @@ -256,6 +256,9 @@ function App () { Log.setLogLevel(config.logLevel); + // get the used module positions + Utils.getModulePositions(); + let modules = []; for (const module of config.modules) { if (module.disabled) continue; @@ -266,10 +269,10 @@ function App () { modules.push(module.module); } } else { - Log.warn("Invalid module position found for this configuration:", module); + Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); } } else { - Log.warn("No module name found for this configuration:", module); + Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); } } diff --git a/js/check_config.js b/js/check_config.js index 6a514de3eb..2df47b7df8 100644 --- a/js/check_config.js +++ b/js/check_config.js @@ -11,6 +11,7 @@ const ajv = new Ajv(); const rootPath = path.resolve(`${__dirname}/../`); const Log = require(`${rootPath}/js/logger.js`); +const Utils = require(`${rootPath}/js/utils.js`); /** * Returns a string with path of configuration file. @@ -68,6 +69,7 @@ function checkConfigFile () { Log.info("Checking modules structure configuration... "); + const position_list = Utils.getModulePositions(); // Make Ajv schema confguration of modules config // only scan "module" and "position" const schema = { @@ -83,21 +85,7 @@ function checkConfigFile () { }, position: { type: "string", - enum: [ - "top_bar", - "top_left", - "top_center", - "top_right", - "upper_third", - "middle_center", - "lower_third", - "bottom_left", - "bottom_center", - "bottom_right", - "bottom_bar", - "fullscreen_above", - "fullscreen_below" - ] + enum: position_list } }, required: ["module"] @@ -116,10 +104,10 @@ function checkConfigFile () { let position = validate.errors[0].instancePath.split("/")[3]; Log.error(colors.red("This module configuration contains errors:")); - Log.error(data.modules[module]); + Log.error(`\n${JSON.stringify(data.modules[module], null, 2)}`); if (position) { Log.error(colors.red(`${position}: ${validate.errors[0].message}`)); - Log.error(validate.errors[0].params.allowedValues); + Log.error(`\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`); } else { Log.error(colors.red(validate.errors[0].message)); } diff --git a/js/defaults.js b/js/defaults.js index 1cbef63566..c4efa77e95 100644 --- a/js/defaults.js +++ b/js/defaults.js @@ -27,7 +27,6 @@ const defaults = { // (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage. checkServerInterval: 30 * 1000, reloadAfterServerRestart: false, - allowCustomModulePositions: false, modules: [ { diff --git a/js/loader.js b/js/loader.js index 1ce6f27bd1..e8dff1907c 100644 --- a/js/loader.js +++ b/js/loader.js @@ -50,7 +50,7 @@ const Loader = (function () { * @returns {object[]} module data as configured in config */ const getAllModules = function () { - const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined" || config.allowCustomModulePositions)); + const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined")); return AllModules; }; diff --git a/js/main.js b/js/main.js index bc69f0bb93..e50a67f0fc 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,4 @@ -/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */ +/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */ const MM = (function () { let modules = []; @@ -450,7 +450,6 @@ const MM = (function () { * an ugly top margin. By using this function, the top bar will be hidden if the * update notification is not visible. */ - const modulePositions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; const updateWrapperStates = function () { modulePositions.forEach(function (position) { diff --git a/js/utils.js b/js/utils.js index df2e91757d..a8211a3a7b 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,7 +1,17 @@ const execSync = require("node:child_process").execSync; -const Log = require("logger"); +const path = require("node:path"); + +const rootPath = path.resolve(`${__dirname}/../`); +const Log = require(`${rootPath}/js/logger.js`); +const os = require("node:os"); +const fs = require("node:fs"); const si = require("systeminformation"); +const modulePositions = []; // will get list from index.html +const regionRegEx = /"region ([^"]*)/i; +const indexFileName = "index.html"; +const discoveredPositionsJSFilename = "js/positions.js"; + module.exports = { async logSystemInformation () { @@ -29,13 +39,32 @@ module.exports = { // return all available module positions getAvailableModulePositions () { - return ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; + return modulePositions; }, // return if postion is on modulePositions Array (true/false) moduleHasValidPosition (position) { - if (config.allowCustomModulePositions) return true; if (this.getAvailableModulePositions().indexOf(position) === -1) return false; return true; + }, + + getModulePositions () { + // get the lines of the index.html + const lines = fs.readFileSync(indexFileName).toString().split(os.EOL); + // loop thru the lines + lines.forEach((line) => { + // run the regex on each line + const results = regionRegEx.exec(line); + // if the regex returned something + if (results && results.length > 0) { + // get the postition parts and replace space with underscore + const positionName = results[1].replace(" ", "_"); + // add it to the list + modulePositions.push(positionName); + } + }); + fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`); + // return the list to the caller + return modulePositions; } }; diff --git a/tests/configs/customregions.js b/tests/configs/customregions.js new file mode 100644 index 0000000000..5fe4ee28c4 --- /dev/null +++ b/tests/configs/customregions.js @@ -0,0 +1,23 @@ +let config = { + modules: + // Using exotic content. This is why don't accept go to JSON configuration file + (() => { + let positions = ["row3_left", "top3_left1"]; + let modules = Array(); + for (let idx in positions) { + modules.push({ + module: "helloworld", + position: positions[idx], + config: { + text: `Text in ${positions[idx]}` + } + }); + } + return modules; + })() +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/e2e/custom_module_regions_spec.js b/tests/e2e/custom_module_regions_spec.js new file mode 100644 index 0000000000..48d09712da --- /dev/null +++ b/tests/e2e/custom_module_regions_spec.js @@ -0,0 +1,30 @@ +const helpers = require("./helpers/global-setup"); + +describe("Custom Position of modules", () => { + beforeAll(async () => { + await helpers.fixupIndex(); + await helpers.startApplication("tests/configs/customregions.js"); + await helpers.getDocument(); + }); + afterAll(async () => { + await helpers.stopApplication(); + await helpers.restoreIndex(); + }); + + const positions = ["row3_left", "top3_left1"]; + let i = 0; + const className1 = positions[i].replace("_", "."); + let message1 = positions[i]; + it(`should show text in ${message1}`, async () => { + const elem = await helpers.waitForElement(`.${className1}`); + expect(elem).not.toBeNull(); + expect(elem.textContent).toContain(`Text in ${message1}`); + }); + i = 1; + const className2 = positions[i].replace("_", "."); + let message2 = positions[i]; + it(`should NOT show text in ${message2}`, async () => { + const elem = await helpers.waitForElement(`.${className2}`, "", 1500); + expect(elem).toBeNull(); + }, 1510); +}); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 31a3e036c2..ef90c0fdd9 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -1,5 +1,20 @@ +const os = require("node:os"); +const fs = require("node:fs"); const jsdom = require("jsdom"); +const indexFile = `${__dirname}/../../../index.html`; +const cssFile = `${__dirname}/../../../css/custom.css`; +const sampleCss = [ + ".region.row3 {", + " top: 0;", + "}", + ".region.row3.left {", + " top: 100%;", + "}" +]; +var indexData = []; +var cssData = []; + exports.startApplication = async (configFilename, exec) => { jest.resetModules(); if (global.app) { @@ -45,11 +60,12 @@ exports.getDocument = () => { }); }; -exports.waitForElement = (selector, ignoreValue = "") => { +exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => { return new Promise((resolve) => { let oldVal = "dummy12345"; + let element = null; const interval = setInterval(() => { - const element = document.querySelector(selector); + element = document.querySelector(selector); if (element) { let newVal = element.textContent; if (newVal === oldVal) { @@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => { } } }, 100); + if (timeout !== 0) { + setTimeout(() => { + if (interval) clearInterval(interval); + resolve(null); + }, timeout); + } }); }; @@ -91,3 +113,34 @@ exports.testMatch = async (element, regex) => { expect(elem.textContent).toMatch(regex); return true; }; + +exports.fixupIndex = async () => { + // read and save the git level index file + indexData = (await fs.promises.readFile(indexFile)).toString(); + // make lines of the content + let workIndexLines = indexData.split(os.EOL); + // loop thru the lines to find place to insert new region + for (let l in workIndexLines) { + if (workIndexLines[l].includes("region top right")) { + // insert a new line with new region definition + workIndexLines.splice(l, 0, "
"); + break; + } + } + // write out the new index.html file, not append + await fs.promises.writeFile(indexFile, workIndexLines.join(os.EOL), { flush: true }); + // read in the current custom.css + cssData = (await fs.promises.readFile(cssFile)).toString(); + // write out the custom.css for this testcase, matching the new region name + await fs.promises.writeFile(cssFile, sampleCss.join(os.EOL), { flush: true }); +}; + +exports.restoreIndex = async () => { + // if we read in data + if (indexData.length > 1) { + //write out saved index.html + await fs.promises.writeFile(indexFile, indexData, { flush: true }); + // write out saved custom.css + await fs.promises.writeFile(cssFile, cssData, { flush: true }); + } +};