- Issue created by @NicholasS
- ๐บ๐ธUnited States NicholasS
Here is our route list patch to convert it to json
- ๐บ๐ธUnited States NicholasS
Also here is the way I use cypress to visit all the CMS routes, and look for PHP errors, I hobbled this together myself so I am sure it could be improved, but I wanted cypress to find all the Drupal CMS error messages (watchdog messages) as well as find PHP white screens of death that had stack traces.
// Pass in the pages HTML and look for Drupal or PHP errors Cypress.Commands.add("checkBodyForErrors", ($body) => { function checkIfShouldIgnore(msg) { // This function will check to see if the error message is a known one and should be ignored or not if (msg.includes("There are security updates available for one or more of your modules or themes")) { // developers will be aware of outdated modules via other means return true; } if ( msg.includes("One or more problems were detected with your Drupal installation. Check the status report for more information.")) { // This just means to check the status page // was on admin/config return true; } if (msg.includes(`The config-set for the Solr server Solr could not be generated`)) { // fine on dev config set is on the server return true; } return false; } // see if any PHP errors happen only way I see to check is look for the // php error template HTML on the page if ($body.html().includes("</b> on line <b>")) { var ignore = false; if ($body.html().includes('Undefined index: search_views_query in') && $body.html().includes('core/modules/views/src/Views.php')) { ignore = true; } if ($body.html().includes('Invalid argument supplied for foreach()') && $body.html().includes('search_api_solr/src/Form/SolrFieldTypeForm.php')) { ignore = true; } if (!ignore) { throw new Error(`PHP issue found`); } else { return } } // Check if the body contains a drupal error class message if ($body.find(".messages--error").length > 0) { // There is either a single error or multiple error messages if ($body.find(".messages--error .messages__item").length > 0) { // if multiple they are put into an HTML list cy.get(".messages--error .messages__item").then(function($elems) { // Loop over messages and evaluate let i = 0; for (i = 0; i < $elems.length; i++) { let thisMsg = $elems[i].innerText.trim(); let shouldIgnore = checkIfShouldIgnore(thisMsg) ?? FALSE; if (shouldIgnore) { // ignore this message cy.log("Ignoring:" + thisMsg); } else { // This is a problem message so trigger a fail cy.log(thisMsg); throw new Error(thisMsg); } } }); } else { // else single cy.get(".messages--error").then(function($elem) { let thisMsg = $elem.text().trim(); let shouldIgnore = checkIfShouldIgnore(thisMsg) ?? FALSE; if (shouldIgnore) { // ignore this message cy.log("Ignoring:" + thisMsg); } else { // This is a problem message so trigger a fail cy.log(thisMsg); throw new Error(thisMsg); } }); } } });
I use this function in all my tests really like
it("Can visit /technology and view revision", () => { cy.login("simple", userPass); // used technology page because it changes daily so its guaranteed to have revisions cy.visit("/technology/about-us"); cy.get("body").then(($body) => { cy.checkBodyForErrors($body); }); .....
The Fixtures file that I have to ignore URLs from the routes_list module looks somehting like... You end up having to add Ajax and autocomplete routes alot so not sure if there is a better way to filter them out.
// file: fixtures/urls-to-ignore.js // export an array of urls to ignore // to import into multiple tests export const urlsToIgnore = [ "/node", "/batch", "/machine_name/transliterate", "/admin/reports/status/run-cron", "/system/temporary", "/admin/appearance/install", '/admin/flush/cssjs', '/admin/flush', '/admin/flush/menu', '/admin/flush/rendercache', '/admin/flush/static-caches', '/admin/flush/twig', '/admin/flush/views', '/admin/flush/plugin', '/admin/flush/theme_rebuild', '/admin/reports/auditfiles/managednotused', '/admin/reports/auditfiles/mergefilereferences', '/admin/reports/auditfiles/notonserver', '/admin/reports/auditfiles/referencednotused', '/admin/reports/auditfiles/usednotmanaged', '/admin/reports/auditfiles/usednotreferenced', '/fullcalendar-view-event-add', '/fullcalendar-view-event-update', '/geocoder/api/geocode', '/geocoder/api/reverse_geocode', '/rest/session/token', "/admin/update/ready", // Does not load in acquia dev '/admin/reports/updates/check', "/admin/modules/install", //Acquia blocks this with git deploys '/admin/structure/context/groups/autocomplete', ....... ];
- ๐บ๐ธUnited States NicholasS
Oh an I use node to login headlessly, fetch the routes list from Drupal before opening cypress. so that cypress can use the fixture file while testing, so that it generates a test for each CMS route.
// File name: ./refresh-routes.js /* eslint-env node */ // This script is used to refresh the routes list fixture file. // It should be run before running the tests. // pass it arguments like this: // node refresh-routes.js site=inet.ddev.site:9445 pass=admin const execSync = require("child_process").execSync; const axios = require("axios"); const fs = require("fs"); const https = require("https"); const env = Object.create(process.env); process.argv.slice(2).forEach((arg) => { var [key, value] = arg.split("="); env[key] = value || true; }); let siteDomain; var password; if (!env.site) { siteDomain = `https://yoursite.ddev.site`; password = "admin" } else { siteDomain = `https://${env.site}`; password = env.pass; } const baseUrl = siteDomain; async function loginAndSaveStatusReport() { try { // Drupal 9 site URL and credentials const siteUrl = baseUrl; const username = "admin"; // At instance level const instance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }) }); // Perform login const loginResponse = await instance.post( `${siteUrl}/user/login?_format=json`, { name: username, pass: password, }, { headers: { "Content-Type": "application/json", } } ); if (loginResponse.status === 200 && loginResponse.data.current_user.uid) { console.log("Login successful"); // Get routes list page const statusReportResponse = await instance.get( `${siteUrl}/admin/reports/routes-list-json`, { headers: { Cookie: `X-CSRF-Token=${ loginResponse.data.csrf_token }; ${loginResponse.headers["set-cookie"].join("; ")}`, }, responseType: "json", } ); if (statusReportResponse.status === 200) { // Save routes list to a file const fixturePath = `cypress/fixtures/routes-list.json`; const jsonData = JSON.stringify(statusReportResponse.data, null, "\t"); fs.writeFileSync(fixturePath, jsonData); console.log("routes list saved successfully"); } else { console.log("Failed to retrieve routes list"); } } else { console.log("Login failed"); } } catch (error) { console.error("An error occurred:", error.message); } } async function main() { try { // first login and save status report await loginAndSaveStatusReport(); } catch (error) { console.error("An error occurred:", error.message); } } main();
Used like
node ./refresh-routes.js && ./npx cypress run
So super complex I guess... not sure if you can use any of this. But it has caught the most regressions for us.
- ๐บ๐ธUnited States aangel
Thanks for this contribution. I've started to work on it in the issue fork. First step was check out Routes List (looks great) and to get the patch working; I also updated the documentation to include Routes List and how to patch it.
I really like this approach because I wanted to test all the entries in a sitemap.xml fileโbut this seems better.
Next I'll take a look at the test itself.