add support for custom regions, by detecting what is used in index.html (#3518)

read index.html to discover the regions used, make them the list checked
by app.js and check:config test

fixes #3504   supercedes #3506 

no config.js param required
This commit is contained in:
sam detweiler 2024-08-27 15:52:59 -05:00 committed by GitHub
parent 56736786fd
commit 2b97e0d26e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 154 additions and 29 deletions

View File

@ -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) - [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
- [core] Updated dependencies including stylistic-eslint - [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] 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 ### Fixed

View File

@ -55,6 +55,7 @@
<script type="text/javascript" src="js/loader.js"></script> <script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script> <script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/animateCSS.js"></script> <script type="text/javascript" src="js/animateCSS.js"></script>
<script type="text/javascript" src="js/positions.js"></script>
<script type="text/javascript" src="js/main.js"></script> <script type="text/javascript" src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -256,6 +256,9 @@ function App () {
Log.setLogLevel(config.logLevel); Log.setLogLevel(config.logLevel);
// get the used module positions
Utils.getModulePositions();
let modules = []; let modules = [];
for (const module of config.modules) { for (const module of config.modules) {
if (module.disabled) continue; if (module.disabled) continue;
@ -266,10 +269,10 @@ function App () {
modules.push(module.module); modules.push(module.module);
} }
} else { } 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 { } 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)}`);
} }
} }

View File

@ -11,6 +11,7 @@ const ajv = new Ajv();
const rootPath = path.resolve(`${__dirname}/../`); const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`); const Log = require(`${rootPath}/js/logger.js`);
const Utils = require(`${rootPath}/js/utils.js`);
/** /**
* Returns a string with path of configuration file. * Returns a string with path of configuration file.
@ -68,6 +69,7 @@ function checkConfigFile () {
Log.info("Checking modules structure configuration... "); Log.info("Checking modules structure configuration... ");
const position_list = Utils.getModulePositions();
// Make Ajv schema confguration of modules config // Make Ajv schema confguration of modules config
// only scan "module" and "position" // only scan "module" and "position"
const schema = { const schema = {
@ -83,21 +85,7 @@ function checkConfigFile () {
}, },
position: { position: {
type: "string", type: "string",
enum: [ enum: position_list
"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"
]
} }
}, },
required: ["module"] required: ["module"]
@ -116,10 +104,10 @@ function checkConfigFile () {
let position = validate.errors[0].instancePath.split("/")[3]; let position = validate.errors[0].instancePath.split("/")[3];
Log.error(colors.red("This module configuration contains errors:")); 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) { if (position) {
Log.error(colors.red(`${position}: ${validate.errors[0].message}`)); 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 { } else {
Log.error(colors.red(validate.errors[0].message)); Log.error(colors.red(validate.errors[0].message));
} }

View File

@ -27,7 +27,6 @@ const defaults = {
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage. // (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
checkServerInterval: 30 * 1000, checkServerInterval: 30 * 1000,
reloadAfterServerRestart: false, reloadAfterServerRestart: false,
allowCustomModulePositions: false,
modules: [ modules: [
{ {

View File

@ -50,7 +50,7 @@ const Loader = (function () {
* @returns {object[]} module data as configured in config * @returns {object[]} module data as configured in config
*/ */
const getAllModules = function () { 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; return AllModules;
}; };

View File

@ -1,4 +1,4 @@
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */ /* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
const MM = (function () { const MM = (function () {
let modules = []; 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 * an ugly top margin. By using this function, the top bar will be hidden if the
* update notification is not visible. * 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 () { const updateWrapperStates = function () {
modulePositions.forEach(function (position) { modulePositions.forEach(function (position) {

View File

@ -1,7 +1,17 @@
const execSync = require("node:child_process").execSync; 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 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 = { module.exports = {
async logSystemInformation () { async logSystemInformation () {
@ -29,13 +39,32 @@ module.exports = {
// return all available module positions // return all available module positions
getAvailableModulePositions () { 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) // return if postion is on modulePositions Array (true/false)
moduleHasValidPosition (position) { moduleHasValidPosition (position) {
if (config.allowCustomModulePositions) return true;
if (this.getAvailableModulePositions().indexOf(position) === -1) return false; if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
return true; 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;
} }
}; };

View File

@ -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;
}

View File

@ -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);
});

View File

@ -1,5 +1,20 @@
const os = require("node:os");
const fs = require("node:fs");
const jsdom = require("jsdom"); 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) => { exports.startApplication = async (configFilename, exec) => {
jest.resetModules(); jest.resetModules();
if (global.app) { if (global.app) {
@ -45,11 +60,12 @@ exports.getDocument = () => {
}); });
}; };
exports.waitForElement = (selector, ignoreValue = "") => { exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
return new Promise((resolve) => { return new Promise((resolve) => {
let oldVal = "dummy12345"; let oldVal = "dummy12345";
let element = null;
const interval = setInterval(() => { const interval = setInterval(() => {
const element = document.querySelector(selector); element = document.querySelector(selector);
if (element) { if (element) {
let newVal = element.textContent; let newVal = element.textContent;
if (newVal === oldVal) { if (newVal === oldVal) {
@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => {
} }
} }
}, 100); }, 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); expect(elem.textContent).toMatch(regex);
return true; 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, " <div class=\"region row3 left\"><div class=\"container\"></div></div>");
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 });
}
};