From dee960a5213224f8552cd72426b12707d7d0c24e Mon Sep 17 00:00:00 2001 From: jimwashbrook <131891854+jimwashbrook@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:05:36 +0000 Subject: [PATCH] tests: Refactor existing work on dynamic E2E tests (#496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Hotfixes (#492) * draft: docs: Caching documentation (#460) * docs: initial documentation templates * docs: add other headers * docs: overview of readpage * docs: add db schema * docs: more caching process documentation * terraform-docs: automated action * docs: Update CMS schema diagram * docs: Add dbdiagram code for schema * docs: Finish Contentful caching process doc * docs: initial documentation templates * docs: add other headers * docs: overview of readpage * docs: add db schema * docs: more caching process documentation * docs: Update CMS schema diagram * docs: Add dbdiagram code for schema * docs: Finish Contentful caching process doc * docs: wip templates for adrs * docs: draft highlevel mapping info --------- Co-authored-by: github-actions[bot] * fix: handle textBodyDB entity being empty - possible fix * Linted Code in Dfe.PlanTech.Domain/ * ignore mapping of rich text id * fix: Check answers page title (#487) * fix: check answers page title * fix: remove line * Linted Code in Dfe.PlanTech.Web/ --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat: Add caching docs (#486) * docs: initial documentation templates * docs: add other headers * docs: overview of readpage * docs: add db schema * docs: more caching process documentation * terraform-docs: automated action * docs: Update CMS schema diagram * docs: Add dbdiagram code for schema * docs: Finish Contentful caching process doc * docs: initial documentation templates * docs: add other headers * docs: overview of readpage * docs: add db schema * docs: more caching process documentation * docs: Update CMS schema diagram * docs: Add dbdiagram code for schema * docs: Finish Contentful caching process doc * docs: wip templates for adrs * docs: draft highlevel mapping info * docs: pad out adrs * docs: add caching adr * docs: Expand contentful caching process --------- Co-authored-by: github-actions[bot] * feat: Support preview mode in Content db (#489) * wip * feat: process options * feat: support preview in cmsdbcontext * tests: tests for save + autosave * feat: add contentfuloptions to web proj * fix: add default usepreview * fix: correctly get ContentfulOptions in dbcontext * chore: remove unneeded param, add docs * chore: refactor ignore message condition * fix: Azure Function Tests (#490) * fix: Add parameterless constructor again to fix tests * chore: code cleanup recommendations * fix: Remove this.getservice as causing errors * fix: service config issue * refactor: use guard statement for cleaner code * refactor: create only adds contentcomponent * refactor: revert changes and skip create event * refactor: only save or auto_save when a new entity has all its required fields * test: update tests --------- Co-authored-by: github-actions[bot] Co-authored-by: uahussain12 <“umar.hussain@and.digital”> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: uahussain12 Co-authored-by: Zac King Co-authored-by: Iain STANGER Co-authored-by: Zac <119302767+Zac-AND-Digital@users.noreply.github.com> * wip: validating pages dynamically * wip: validate textbodies * wip * wip: move validations to separate files * feat: validate paragraphs * feat: validate table * feat: use class, add comments * chore: rearrange files * chore: add docs * chore: delete moved files * fix: missing comma * refactor: cleanup duplicated code. * merge development * chore: delete old dupe files --------- Co-authored-by: github-actions[bot] Co-authored-by: uahussain12 <“umar.hussain@and.digital”> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: uahussain12 Co-authored-by: Zac King Co-authored-by: Iain STANGER Co-authored-by: Zac <119302767+Zac-AND-Digital@users.noreply.github.com> --- .../.gitignore | 0 contentful/export-processor/README.md | 5 + .../export-processor/content-types}/answer.js | 0 .../content-types/content-type.js} | 0 .../content-types/content-types.js | 11 + .../content-types}/question.js | 2 +- .../content-types/recommendation.js} | 0 .../content-types}/section.js | 3 +- .../export-processor}/data-mapper.js | 4 +- contentful/export-processor/index.js | 6 + .../user-journey}/README.md | 0 .../user-journey}/question-answer.js | 0 .../user-journey/test-suite-generator.js} | 7 +- .../user-journey}/user-journey.js | 0 contentful/user-journey-scripts/answer.mjs | 13 - .../user-journey-scripts/data-mapper.mjs | 240 -------------- contentful/user-journey-scripts/functions.mjs | 0 .../user-journey-scripts/question-answer.mjs | 9 - contentful/user-journey-scripts/question.mjs | 15 - contentful/user-journey-scripts/runner.mjs | 8 - contentful/user-journey-scripts/section.mjs | 300 ------------------ .../user-journey-scripts/user-journey.mjs | 83 ----- .../e2e/dynamic-page-validator-readme.md | 4 +- .../cypress/e2e/dynamic-page-validator.cy.js | 10 +- .../rich-text-content-validator.js | 96 +++--- .../rich-text-validators/table-validator.js | 137 +++++--- .../helpers/contentful-helpers/README.md | 28 -- .../contentful-helpers/content-type.js | 39 --- .../helpers/contentful-helpers/functions.js | 0 .../contentful-helpers/recommendation.js | 16 - .../contentful-helpers/test-suit-generator.js | 36 --- 31 files changed, 171 insertions(+), 901 deletions(-) rename contentful/{user-journey-scripts => export-processor}/.gitignore (100%) create mode 100644 contentful/export-processor/README.md rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor/content-types}/answer.js (100%) rename contentful/{user-journey-scripts/content-type.mjs => export-processor/content-types/content-type.js} (100%) create mode 100644 contentful/export-processor/content-types/content-types.js rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor/content-types}/question.js (86%) rename contentful/{user-journey-scripts/recommendation.mjs => export-processor/content-types/recommendation.js} (100%) rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor/content-types}/section.js (99%) rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor}/data-mapper.js (98%) create mode 100644 contentful/export-processor/index.js rename contentful/{user-journey-scripts => export-processor/user-journey}/README.md (100%) rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor/user-journey}/question-answer.js (100%) rename contentful/{user-journey-scripts/test-suit-generator.mjs => export-processor/user-journey/test-suite-generator.js} (81%) rename {tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers => contentful/export-processor/user-journey}/user-journey.js (100%) delete mode 100644 contentful/user-journey-scripts/answer.mjs delete mode 100644 contentful/user-journey-scripts/data-mapper.mjs delete mode 100644 contentful/user-journey-scripts/functions.mjs delete mode 100644 contentful/user-journey-scripts/question-answer.mjs delete mode 100644 contentful/user-journey-scripts/question.mjs delete mode 100644 contentful/user-journey-scripts/runner.mjs delete mode 100644 contentful/user-journey-scripts/section.mjs delete mode 100644 contentful/user-journey-scripts/user-journey.mjs delete mode 100644 tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/README.md delete mode 100644 tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/content-type.js delete mode 100644 tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/functions.js delete mode 100644 tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/recommendation.js delete mode 100644 tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/test-suit-generator.js diff --git a/contentful/user-journey-scripts/.gitignore b/contentful/export-processor/.gitignore similarity index 100% rename from contentful/user-journey-scripts/.gitignore rename to contentful/export-processor/.gitignore diff --git a/contentful/export-processor/README.md b/contentful/export-processor/README.md new file mode 100644 index 000000000..d494a8d98 --- /dev/null +++ b/contentful/export-processor/README.md @@ -0,0 +1,5 @@ +# Contentful Export Processing + +Classes/functions/etc to process exported Contentful entries for various purposes. + +[Test Suite Generator](./user-journey/README.md) diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/answer.js b/contentful/export-processor/content-types/answer.js similarity index 100% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/answer.js rename to contentful/export-processor/content-types/answer.js diff --git a/contentful/user-journey-scripts/content-type.mjs b/contentful/export-processor/content-types/content-type.js similarity index 100% rename from contentful/user-journey-scripts/content-type.mjs rename to contentful/export-processor/content-types/content-type.js diff --git a/contentful/export-processor/content-types/content-types.js b/contentful/export-processor/content-types/content-types.js new file mode 100644 index 000000000..63aa67cf8 --- /dev/null +++ b/contentful/export-processor/content-types/content-types.js @@ -0,0 +1,11 @@ +import { Answer } from "./answer"; +import ContentType from "./content-type"; +import { Question } from "./question"; +import { Recommendation } from "./recommendation"; +import { Section } from "./section"; + +const exportObj = { + ContentTypes: [Answer, ContentType, Question, Recommendation, Section], +}; + +export default exportObj; diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/question.js b/contentful/export-processor/content-types/question.js similarity index 86% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/question.js rename to contentful/export-processor/content-types/question.js index 6b0603fa7..106023d9f 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/question.js +++ b/contentful/export-processor/content-types/question.js @@ -1,4 +1,4 @@ -import { Answer } from "./answer"; +import { Answer } from "./answer.js"; export class Question { answers; diff --git a/contentful/user-journey-scripts/recommendation.mjs b/contentful/export-processor/content-types/recommendation.js similarity index 100% rename from contentful/user-journey-scripts/recommendation.mjs rename to contentful/export-processor/content-types/recommendation.js diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/section.js b/contentful/export-processor/content-types/section.js similarity index 99% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/section.js rename to contentful/export-processor/content-types/section.js index ffdc35705..1a142c2f0 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/section.js +++ b/contentful/export-processor/content-types/section.js @@ -1,7 +1,6 @@ -import fs from "fs"; import { Recommendation } from "./recommendation"; import { Question } from "./question"; -import { UserJourney } from "./user-journey"; +import { UserJourney } from "../user-journey/user-journey"; export class Section { recommendations; diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/data-mapper.js b/contentful/export-processor/data-mapper.js similarity index 98% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/data-mapper.js rename to contentful/export-processor/data-mapper.js index 3613f5a11..ef4fc2563 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/data-mapper.js +++ b/contentful/export-processor/data-mapper.js @@ -1,5 +1,5 @@ -import { Section } from "./section"; -import ContentType from "./content-type"; +import { Section } from "./content-types/section"; +import ContentType from "./content-types/content-type"; /** * DataMapper class for mapping and combining data from a file diff --git a/contentful/export-processor/index.js b/contentful/export-processor/index.js new file mode 100644 index 000000000..240cfa02b --- /dev/null +++ b/contentful/export-processor/index.js @@ -0,0 +1,6 @@ +import DataMapper from "./data-mapper"; +import ContentTypes from "./content-types/content-types"; + +const exportObj = { DataMapper, ...ContentTypes }; + +export default exportObj; diff --git a/contentful/user-journey-scripts/README.md b/contentful/export-processor/user-journey/README.md similarity index 100% rename from contentful/user-journey-scripts/README.md rename to contentful/export-processor/user-journey/README.md diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/question-answer.js b/contentful/export-processor/user-journey/question-answer.js similarity index 100% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/question-answer.js rename to contentful/export-processor/user-journey/question-answer.js diff --git a/contentful/user-journey-scripts/test-suit-generator.mjs b/contentful/export-processor/user-journey/test-suite-generator.js similarity index 81% rename from contentful/user-journey-scripts/test-suit-generator.mjs rename to contentful/export-processor/user-journey/test-suite-generator.js index a9297e2c1..69aca4734 100644 --- a/contentful/user-journey-scripts/test-suit-generator.mjs +++ b/contentful/export-processor/user-journey/test-suite-generator.js @@ -1,9 +1,12 @@ import fs from "fs"; -import DataMapper from "./data-mapper.mjs"; +import DataMapper from "../data-mapper"; +import fs from "fs"; const { file, writeAllPossiblePaths } = getArguments(); -const dataMapper = new DataMapper(file); +const contentfulData = fs.readFileSync(file, "utf8"); +const parsed = JSON.parse(contentfulData); +const dataMapper = new DataMapper(parsed); for (const section of dataMapper.mappedSections) { const output = dataMapper.convertToMinimalSectionInfo( diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/user-journey.js b/contentful/export-processor/user-journey/user-journey.js similarity index 100% rename from tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/user-journey.js rename to contentful/export-processor/user-journey/user-journey.js diff --git a/contentful/user-journey-scripts/answer.mjs b/contentful/user-journey-scripts/answer.mjs deleted file mode 100644 index 40b2fc0d3..000000000 --- a/contentful/user-journey-scripts/answer.mjs +++ /dev/null @@ -1,13 +0,0 @@ -export class Answer { - id; - text; - maturity; - nextQuestion; - - constructor({ fields, sys }) { - this.maturity = fields.maturity; - this.text = fields.text; - this.id = sys.id; - this.nextQuestion = fields.nextQuestion; - } -} diff --git a/contentful/user-journey-scripts/data-mapper.mjs b/contentful/user-journey-scripts/data-mapper.mjs deleted file mode 100644 index 17638bfeb..000000000 --- a/contentful/user-journey-scripts/data-mapper.mjs +++ /dev/null @@ -1,240 +0,0 @@ -import { Section } from "./section.mjs"; -import fs from "fs"; -import ContentType from "./content-type.mjs"; - -/** - * DataMapper class for mapping and combining data from a file - */ -export default class DataMapper { - // Maps for different content types - contents = new Map(); - - contentTypes = new Map(); - - _alreadyMappedSections; - /** - * Get the mapped sections - * @returns {IterableIterator
} Iterator for mapped sections - */ - get mappedSections() { - if (!this._alreadyMappedSections) - this._alreadyMappedSections = Array.from( - this.sectionsToClasses(this.contents["section"]) - ); - - return this._alreadyMappedSections; - } - - /** - * Constructor for DataMapper - * @param {string} filePath - Path to the file to map data from - */ - constructor({ entries, contentTypes }) { - this.mapData(entries, contentTypes); - } - - /** - * Map data from the provided file - * @param {string} filePath - Path to the file to map data from - */ - mapData(entries, contentTypes) { - this.mapContentTypes(contentTypes); - this.mapEntries(entries); - this.combineEntries(); - } - - mapContentTypes(contentTypes) { - for (const contentType of contentTypes) { - const mapped = new ContentType(contentType); - this.contentTypes.set(mapped.id, mapped); - } - } - - /** - * Map entries from the provided data - * @param {Array} entries - Array of entries to map - */ - mapEntries(entries) { - for (const entry of entries) { - this.tryAddToSet(entry); - } - } - - /** - * Try to add an entry to the corresponding set based on content type - * @param {object} entry - Entry to add to the set - */ - tryAddToSet(entry) { - // Get content type of the entry - const contentType = entry.sys.contentType.sys.id; - - // Get the set for the content type - const setForContentType = this.getContentTypeSet(contentType); - // Add the entry to the set - const id = entry.sys.id; - - this.stripLocalisationFromAllFields(entry); - - setForContentType.set(id, entry); - } - - getContentTypeSet(contentType) { - let setForContentType = this.contents[contentType]; - - if (!setForContentType) { - setForContentType = new Map(); - this.contents[contentType] = setForContentType; - } - - return setForContentType; - } - - /** - * Strip localisation from all fields of an entry - * @param {object} entry - Entry to strip localisation from - */ - stripLocalisationFromAllFields(entry) { - const mappedFields = {}; - Object.entries(entry.fields).forEach(([key, value]) => { - mappedFields[key] = this.stripLocalisationFromField(value); - }); - entry.fields = mappedFields; - } - - /** - * Convert sections to classes - * @param {Map} sections - Map of sections to convert - * @returns {IterableIterator
} Iterator for mapped sections - */ - *sectionsToClasses(sections) { - for (const [id, section] of sections) { - yield new Section(section); - } - } - - /** - * Convert section to minimal section info; only information we care for writing to file - * @param {Section} section - Section to convert - * @param {boolean} writeAllPossiblePaths - Whether to write all possible paths - * @returns {object} Minimal section info - */ - convertToMinimalSectionInfo(section, writeAllPossiblePaths = false) { - return { - section: section.name, - allPathsStats: section.stats, - minimumQuestionPaths: section.minimumPathsToNavigateQuestions, - minimumRecommendationPaths: section.minimumPathsForRecommendations, - allPossiblePaths: writeAllPossiblePaths - ? section.paths.map((path) => { - var result = { - recommendation: - path.recommendation != null - ? { - name: path.recommendation?.displayName, - maturity: path.recommendation?.maturity, - } - : null, - path: path.path.map((s) => { - return { - question: s.question.text, - answer: s.answer.text, - }; - }), - }; - - return result; - }) - : undefined, - }; - } - - /** - * Combine entries for all tracked content types (i.e. answers, pages, questions, recommendations, sections) - */ - combineEntries() { - for (const [contentTypeId, contents] of Object.entries(this.contents)) { - const contentType = this.contentTypes.get(contentTypeId); - - if (contentTypeId == "recommendationPage") { - // console.log("rec page", contents); - } else { - // console.log("not rec page", contentTypeId); - } - - for (const [id, entry] of contents) { - this.mapRelationshipsForEntry(entry, contentType); - } - } - } - - mapRelationshipsForEntry(entry, contentType) { - Object.entries(entry.fields).forEach(([key, value]) => { - const referencedTypesForField = - contentType.getReferencedTypesForField(key); - - if (!referencedTypesForField) return; - - if (Array.isArray(value)) { - entry.fields[key] = value.map((item) => - this.getMatchingContentForReference(referencedTypesForField, item) - ); - } else { - entry.fields[key] = this.getMatchingContentForReference( - referencedTypesForField, - value - ); - - if (contentType.id == "recommendationPage" && key == "page") { - console.log(`rec page`, entry.fields[key]); - } - } - }); - } - - getContentForFieldId(referencedTypesForField, id) { - const matchingItem = referencedTypesForField - .map((type) => { - const matchingContents = this.contents[type]; - const matchingContent = matchingContents.get(id); - - return matchingContent; - }) - .find((matching) => matching != null); - - if (!matchingItem) { - console.error(`Error finding ${id} for ${referencedTypesForField}`); - } - - return matchingItem; - } - - getMatchingContentForReference(referencedTypesForField, reference) { - const referenceId = reference["sys"]?.["id"]; - - if (!referenceId) { - console.log("no reference", reference); - return; - } - - return this.getContentForFieldId(referencedTypesForField, referenceId); - } - - /** - * Strip localisation from a field - * @param {object} obj - Object to strip localisation from - */ - stripLocalisationFromField(obj) { - return obj["en-US"]; - } - - /** - * Copies relationships from parent to children. - * - * @param {type} parent - description of parameter - * @param {type} children - description of parameter - * @return {type} relationships copied from parent to children - */ - copyRelationships(parent, children) { - return parent.map((child) => children.get(child.sys.id)); - } -} diff --git a/contentful/user-journey-scripts/functions.mjs b/contentful/user-journey-scripts/functions.mjs deleted file mode 100644 index e69de29bb..000000000 diff --git a/contentful/user-journey-scripts/question-answer.mjs b/contentful/user-journey-scripts/question-answer.mjs deleted file mode 100644 index 723673cd5..000000000 --- a/contentful/user-journey-scripts/question-answer.mjs +++ /dev/null @@ -1,9 +0,0 @@ -export class QuestionAnswer { - question; - answer; - - constructor({ question, answer }) { - this.question = question; - this.answer = answer; - } -} diff --git a/contentful/user-journey-scripts/question.mjs b/contentful/user-journey-scripts/question.mjs deleted file mode 100644 index 5ae097642..000000000 --- a/contentful/user-journey-scripts/question.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { Answer } from "./answer.mjs"; - -export class Question { - answers; - text; - slug; - id; - - constructor({ fields, sys }) { - this.answers = fields.answers.map((answer) => new Answer(answer)); - this.text = fields.text; - this.slug = fields.slug; - this.id = sys.id; - } -} diff --git a/contentful/user-journey-scripts/runner.mjs b/contentful/user-journey-scripts/runner.mjs deleted file mode 100644 index 6827565b4..000000000 --- a/contentful/user-journey-scripts/runner.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import { contentful } from "./contentful.mjs"; -import DataMapper from "./data-mapper.mjs"; - -const dataMapper = new DataMapper(contentful); - -const test = dataMapper.mappedSections; - -console.log(""); diff --git a/contentful/user-journey-scripts/section.mjs b/contentful/user-journey-scripts/section.mjs deleted file mode 100644 index 5e78eeb4e..000000000 --- a/contentful/user-journey-scripts/section.mjs +++ /dev/null @@ -1,300 +0,0 @@ -import fs from "fs"; -import { Recommendation } from "./recommendation.mjs"; -import { Question } from "./question.mjs"; -import { UserJourney } from "./user-journey.mjs"; - -export class Section { - recommendations; - questions; - name; - id; - - paths; - minimumPathsToNavigateQuestions; - minimumPathsForRecommendations; - - constructor({ fields, sys }) { - this.recommendations = fields.recommendations.map( - (recommendation) => new Recommendation(recommendation) - ); - - this.questions = fields.questions.map((question) => new Question(question)); - this.id = sys.id; - this.name = fields.name; - - this.setNextQuestions(); - this.paths = this.getAllPaths(this.questions[0]).map((path) => { - const userJourney = new UserJourney(path, this); - userJourney.setRecommendation(this.recommendations); - - return userJourney; - }); - - this.getMinimumPathsForQuestions(); - this.getMinimumPathsForRecommendations(); - } - - /** - * Find the minimum amount of paths possible that allows a user to navigate through every question. - */ - getMinimumPathsForQuestions() { - const sortedPaths = this.paths - .slice() - .sort((a, b) => b.path.length - a.path.length); - this.minimumPathsToNavigateQuestions = this.calculateMinimumPaths( - sortedPaths, - this.questions - ); - } - - /** - * Calculate the minimum amount of paths possible for each recommendation. - * Also tries to ensure the paths are as sort as possible. - */ - getMinimumPathsForRecommendations() { - const pathsShortedByShortestFirst = this.paths - .slice() - .sort((a, b) => a.path.length - b.path.length); - this.minimumPathsForRecommendations = - this.calculateMinimumPathsForRecommendations(pathsShortedByShortestFirst); - } - - /** - * Calculate the minimum paths to answer all target questions. - * - * @param {array} sortedPaths - The sorted paths to be considered. - * @param {array} targetQuestions - The target questions to be answered. - * @return {array} An array containing the minimum paths to answer the target questions. - */ - calculateMinimumPaths(sortedPaths, targetQuestions) { - const remainingQuestions = targetQuestions.map((question) => question.id); - let minimumPaths = []; - - const uniquePaths = this.getUniquePaths(sortedPaths); - - // Find minimum paths to answer all questions - for (const path of uniquePaths) { - const matchingQuestions = remainingQuestions.filter((questionId) => - path.some((id) => id == questionId) - ); - - if (matchingQuestions.length > 0) { - minimumPaths.push(path); - - for (const questionId of path) { - removeFromArray(remainingQuestions, questionId); - } - } - } - - minimumPaths = minimumPaths.map((minPath) => { - const matchingPath = this.paths.find((path) => - minPath.every((questionId) => - path.path.some((pathPart) => pathPart.question.id == questionId) - ) - ); - - if (!matchingPath) { - console.error(`Couldn't find matching path`, minPath); - return; - } - - return matchingPath.mapPathToOnlyQuestionAnswerTexts(); - }); - - // If there are remaining questions, find paths for each question - for (const questionId of remainingQuestions) { - const pathContainingQuestion = this.getFirstPathContainingQuestion( - sortedPaths, - questionId - ); - - if (pathContainingQuestion == null) continue; - - minimumPaths.push( - pathContainingQuestion.mapPathToOnlyQuestionAnswerTexts() - ); - } - - return minimumPaths; - } - - /** - * Calculate the minimum paths for recommendations. - * - * @param {Array} paths - The paths to calculate minimums for - * @return {Object} minimumPathsForRecommendations - The calculated minimum paths for recommendations - */ - calculateMinimumPathsForRecommendations(paths) { - const possibleMaturities = ["Low", "Medium", "High"]; - const minimumPathsForRecommendations = {}; - - for (const maturity of possibleMaturities) { - const pathForRecommendation = paths.find( - (path) => path.maturity == maturity - ); - - if (!pathForRecommendation) { - console.error(`No path found for ${maturity} in ${this.name}`); - continue; - } - - minimumPathsForRecommendations[maturity] = - pathForRecommendation.mapPathToOnlyQuestionAnswerTexts(); - } - - return minimumPathsForRecommendations; - } - - /** - * Calculates the statistics of the paths. - * Currently just counts the number of paths for each maturity level. - * - * @param {type} paramName - description of parameter - * @return {type} description of return value - */ - get stats() { - return this.paths.reduce((count, path) => { - const maturity = path.maturity; - return count[maturity] ? ++count[maturity] : (count[maturity] = 1), count; - }, {}); - } - - /** - * Retrieves all possible paths from the current question. - * - * @param {Object} currentQuestion - The current question object - * @return {Array} An array of all possible paths - */ - getAllPaths(currentQuestion) { - const paths = []; - const stack = []; - - stack.push({ - currentPath: [], - currentQuestion, - }); - - while (stack.length > 0) { - const { currentPath, currentQuestion } = stack.pop(); - - if (!currentQuestion) { - paths.push(currentPath); - continue; - } - - currentQuestion.answers.forEach((answer) => { - const newPath = [ - ...currentPath, - { question: currentQuestion, answer: answer }, - ]; - - const nextQuestion = this.questions.find( - (q) => q.id === answer.nextQuestion?.id - ); - - stack.push({ - currentPath: newPath, - currentQuestion: nextQuestion, - }); - }); - } - - return paths; - } - - /** - * Get unique paths from the sorted paths array. - * - * @param {Array} sortedPaths - The array of sorted paths - * @return {Array} The array of unique paths - */ - getUniquePaths(sortedPaths) { - const pathsWithQuestions = sortedPaths.map((path) => ({ - path: path.path, - questionsAnswered: path.path.map((pathPart) => pathPart.question.id), - })); - const uniqueQuestions = new Set( - pathsWithQuestions.map((path) => JSON.stringify(path.questionsAnswered)) - ); - const uniquePaths = Array.from(uniqueQuestions).map(JSON.parse); - return uniquePaths; - } - - /** - * Find the first path containing the specified question ID. - * - * @param {array} sortedPaths - The array of sorted paths to search through. - * @param {number} questionId - The ID of the question to find in the paths. - * @return {object} The path containing the specified question ID, or undefined if not found. - */ - getFirstPathContainingQuestion(sortedPaths, questionId) { - // Find the first path that contains the remaining question - const pathsForQuestion = sortedPaths.find((path) => - path.path.some((pathPart) => pathPart.question.id == questionId) - ); - - if (pathsForQuestion) { - return pathsForQuestion; - } - - const question = this.questions.find( - (question) => question.id == questionId - ); - if (!question) { - console.error(`Couldn't find question ${questionId} in ${this.name}`); - return; - } - console.error( - `Unable to find a path containing question ${questionId} (${question.text}) in ${this.name}` - ); - - return; - } - - /** - * For each answer, in each question (in the this.questions property), - * find the matching question for the 'nextQuestion' property, and set it if found - */ - setNextQuestions() { - for (const question of this.questions) { - for (const answer of question.answers) { - const nextQuestionId = answer.nextQuestion?.sys.id; - if (nextQuestionId == null) { - continue; - } - - const matchingQuestions = this.questions.filter( - (q) => q.id == nextQuestionId - ); - - if (matchingQuestions.length == 0) { - console.error( - `Error finding question for ${nextQuestionId} in ${this.name} - answer ${answer.text} ${answer.id} in ${question.text} ${question.id}` - ); - console.log(""); - - answer.nextQuestion = null; - continue; - } - - answer.nextQuestion = matchingQuestions[0]; - } - } - } -} - -/** - * Removes the first occurrence of the specified value from the array. - * - * @param {Array} arr - The array to remove the value from - * @param {any} value - The value to be removed from the array - * @return {Array} The array with the first occurrence of the value removed - */ -const removeFromArray = (arr, value) => { - var index = arr.indexOf(value); - if (index > -1) { - arr.splice(index, 1); - } - return arr; -}; diff --git a/contentful/user-journey-scripts/user-journey.mjs b/contentful/user-journey-scripts/user-journey.mjs deleted file mode 100644 index 9cb5544e4..000000000 --- a/contentful/user-journey-scripts/user-journey.mjs +++ /dev/null @@ -1,83 +0,0 @@ -export class UserJourney { - path; - maturity; - section; - recommendation; - - constructor(path, section) { - this.path = path; - this.section = section; - - this.maturity = this.maturityRanking( - path - .map((questionAnswer) => questionAnswer.answer.maturity) - .filter(onlyUnique) - .map(this.maturityRanking) - .filter((maturity) => maturity != null) - .sort()[0] - ); - } - - /** - * Finds and sets the recommendation property from the recommendations received - * - * @param {Array} recommendations - the list of recommendations to look through - */ - setRecommendation(recommendations) { - const recommendation = recommendations.filter( - (recommendation) => recommendation.maturity == this.maturity - ); - - if (recommendation == null || recommendation.length == 0) { - console.error( - `could not find recommendation for ${this.maturity} in ${this.section.name}`, - recommendations - ); - return; - } - - this.recommendation = recommendation[0]; - } - - /** - * A function that maps maturity levels to integers, for comparing/sorting easily. - * - * @param {string|number} maturity - the maturity level to be ranked - * @return {number|string|null} the ranked maturity level or null if not found - */ - maturityRanking(maturity) { - switch (maturity) { - case "Low": - return 0; - case "Medium": - return 1; - case "High": - return 2; - case 0: - return "Low"; - case 1: - return "Medium"; - case 2: - return "High"; - } - - return null; - } - - /** - * Maps the path to only the question and answer texts. - * - * @param {type} pathPart - description of parameter - * @return {type} the mapped question and answer texts - */ - mapPathToOnlyQuestionAnswerTexts() { - return this.path.map((pathPart) => { - return { - question: pathPart.question.text, - answer: pathPart.answer.text, - }; - }); - } -} - -const onlyUnique = (value, index, array) => array.indexOf(value) === index; diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator-readme.md b/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator-readme.md index 2eeb89dd4..17056bc59 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator-readme.md +++ b/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator-readme.md @@ -3,9 +3,9 @@ ## Instructions 1. Extract Contentful content using CLI -2. Rename file to `contentful.mjs`, and make the JSON an object called Contentful. E.g. `const contentful = { ... JSON}`; +2. Rename file to `contentful.js`, and make the JSON an object called Contentful. E.g. `const contentful = { ... JSON}`; 3. Export that object -4. Place the `contentful.mjs` in the same folder as `dynamic-page-validator.cy.js` +4. Place the `contentful.js` in the same folder as `dynamic-page-validator.cy.js` 5. Remove the `skip` command on line 14. diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator.cy.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator.cy.js index 81a177fe1..2cf7be743 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator.cy.js +++ b/tests/Dfe.PlanTech.Web.E2ETests/cypress/e2e/dynamic-page-validator.cy.js @@ -1,4 +1,4 @@ -import DataMapper from "../helpers/contentful-helpers/data-mapper"; +import DataMapper from "../../../../contentful/export-processor/data-mapper"; import { contentful } from "./contentful"; import ValidatePage from "../helpers/content-validators/page-validator"; @@ -6,11 +6,13 @@ describe("Pages should have content", () => { let dataMapper; before(() => { - dataMapper = new DataMapper(contentful); + if (contentful && contentful.entries && contentful.entries.length > 0) { + dataMapper = new DataMapper(contentful); + } }); - it.skip("Should work for unauthorised pages", () => { - if (dataMapper.pages == null) { + it("Should work for unauthorised pages", () => { + if (dataMapper?.pages == null) { console.log("Datamapper has not processed data correctly"); return; } diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-content-validator.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-content-validator.js index 7816a4cd7..9ce8f9160 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-content-validator.js +++ b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-content-validator.js @@ -40,70 +40,58 @@ function validateByNodeType(content) { } function validateParagraph(content) { - const children = Array.from(getChildrenRecursive(content)); - const values = children.map((child) => child.value); - - const htmlValue = getContentValues(values); - - if (!htmlValue || htmlValue.length == 0) return; - - cy.get("p").then(($p) => { - const htmls = Array.from( - $p.map((i, el) => { - return Cypress.$(el).text(); - }) + const expectedHtml = buildExpectedHtml(content).trim(); + + const parsedElement = parse(expectedHtml); + + cy.get("p").then(($paragraphs) => { + const paragraphHtmls = Array.from( + Array.from($paragraphs.map((i, el) => Cypress.$(el).html())) + .map((paragraph) => { + const withoutWhitespaceEscaped = paragraph.replace(" ", " "); + return { + original: withoutWhitespaceEscaped, + parsed: parse(withoutWhitespaceEscaped), + }; + }) + .filter((paragraph) => paragraph.original != "") ); - const matched = htmls.find((paragraph) => - htmlValue.every((html) => paragraph.indexOf(html) != -1) + const anyMatches = paragraphHtmls.find( + (paragraph) => + paragraph.original == expectedHtml || + paragraph.original.indexOf(expectedHtml) != -1 || + paragraph.parsed.innerHTML?.indexOf(parsedElement.innerHTML) != -1 || + paragraph.parsed.innerText?.indexOf(parsedElement.innerText) != -1 ); - if (matched) return; - - const parsed = htmlValue.map((html) => parse(html)).map((p) => p.innerText); - - const matchedHtmlContent = htmls - .map((html) => html.replace(/\ /g, "")) - .some((paragraph) => { - const matches = parsed.every((html) => { - const m = paragraph.indexOf(html) != -1; - return m; - }); + expect(anyMatches).to.exist; + }); +} - return matches; - }); +function buildExpectedHtml(content) { + let html = ""; + for (const child of content.content) { + if (child.value) { + html += child.value.replace(/\r\n/g, "\n"); + } - if (!matchedHtmlContent) { - throw new Error(`Unable to find ${htmlValue}`); + if (child.nodeType == "hyperlink") { + html += ``; } - }); -} -function getContentValues(values) { - return Array.from( - values - .filter((value) => value != null && value != "
") - .map((value) => value.trim()) - .filter((value) => value != null) - .flatMap((value) => { - if (value.match(regex)) { - return value.split(regex); - } else { - return [value]; + if (child.content) { + for (const grandchild of child.content) { + if (grandchild.value) { + html += grandchild.value; } - }) - .filter( - (value) => value && value != "" && value != "\n" && value != "\r\n" - ) - ); -} - -function* getChildrenRecursive(content) { - if (content.content) { - for (const child of content.content) { - yield child; + } + } - yield* getChildrenRecursive(child); + if (child.nodeType == "hyperlink") { + html += `
`; } } + + return html; } diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-validators/table-validator.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-validators/table-validator.js index 18171e934..e1f91197d 100644 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-validators/table-validator.js +++ b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/content-validators/rich-text-validators/table-validator.js @@ -17,9 +17,11 @@ const isRowType = (type) => (row) => row.every(isNodeType(type)); const isTableHeaderRow = isRowType("table-header-cell"); const isTableDataRow = isRowType("table-cell"); -class TableValidator { - foundTable; +//Constants used for finding specific rows in a table +const HEADER_ROW = "HEADER"; +const BODY_ROW = "BODY"; +class TableValidator { constructor() {} /** @@ -27,67 +29,106 @@ class TableValidator { * @param {object} content - The content to validate. */ validateTable(content) { + const expectedHeaders = content.content.find( + (row) => this.getRowType(row.content) == HEADER_ROW + ); + + if (!expectedHeaders) { + throw new Error("Couldn't find headers"); + } + + const expectedRows = Array.from( + content.content.filter((row) => this.getRowType(row.content) == BODY_ROW) + ); + cy.get("table").then(($table) => { - for (const row of content.content) { - this.validateRow(row, $table); - } + const mapped = $table.map((i, table) => { + const jqueryTable = Cypress.$(table); + const headerRow = jqueryTable.find("thead tr"); + const bodyRows = jqueryTable.find("tbody tr"); + + return { + header: headerRow, + body: bodyRows, + }; + }); + + const matchingTableFound = this.anyTableMatches( + mapped, + expectedHeaders, + expectedRows + ); - this.foundTable = null; + expect(matchingTableFound).to.be.true; }); } /** - * Validates a row of the table. - * @param {object} row - The row to validate. - * @param {object} tableElement - The table element. + * Checks if any table matches the expected headers and rows. + * + * @param {Array} tables - The mapped tables + * @param {Array} expectedHeaders - The expected headers + * @param {Array} expectedRows - The expected rows + * @return {boolean} Whether any table matches the expected headers and rows */ - validateRow(row, tableElement) { - const cells = row.content; + anyTableMatches(tables, expectedHeaders, expectedRows) { + for (const table of tables) { + const { header, body } = table; + const headerCells = Cypress.$(header).children(); - const rowType = this.getRowType(cells, row); + if (!this.rowMatches(expectedHeaders.content, headerCells)) { + continue; + } - for (let index = 0; index < cells.length; index++) { - const cellSelector = this.getCellSelector(rowType, index); - const cell = cells[index]; - const tableToCheck = this.foundTable ?? cy.wrap(tableElement); + const matches = this.tableBodyMatches(body, expectedRows); - this.validateCell(cell, tableToCheck, cellSelector); + if (matches) { + return true; + } } + return false; } /** - * Method to validate a cell of the table. - * @param {object} cell - The cell to validate. - * @param {object} tableToCheck - The table to check. - * @param {string} cellSelector - The selector for the cell. + * Check if the table body matches the expected rows. + * + * @param {Array} body - the table body to compare + * @param {Array} expectedRows - the expected rows to match against + * @return {Boolean} true if the table body matches the expected rows, false otherwise */ - validateCell(cell, tableToCheck, cellSelector) { - const cellText = this.getCellText(cell); - - tableToCheck - .get(cellSelector) - .contains(cellText) - .then((matchingTable) => { - if (this.foundTable == null) { - this.foundTable = matchingTable; - } - }); + tableBodyMatches(body, expectedRows) { + for (let rowIndex = 0; rowIndex < body.length; rowIndex++) { + const rowCells = Cypress.$(body[rowIndex]).children(); + const expectedCells = expectedRows[rowIndex].content; + + if (!this.rowMatches(expectedCells, rowCells)) { + return false; + } + } + + return true; } /** - * Method to get the selector for a row based on its type and position. - * @param {string} rowType - The type of the row. - * @param {number} index - The position of the cell in the row. - * @returns {string} - The selector for the row. + * Checks if the cells in a row match the expected cells. + * + * @param {array} expectedCells - the array of expected cells + * @param {array} rowCells - the array of cells in the row + * @return {boolean} true if all row cells match the expected cells, false otherwise */ - getCellSelector(rowType, index) { - if (rowType === "HEADER") { - return "thead tr th"; - } else if (index === 0) { - return "tbody tr th"; - } else { - return "tbody tr td"; + rowMatches(expectedCells, rowCells) { + for (let index = 0; index < expectedCells.length; index++) { + const expectedCellText = this.getCellText(expectedCells[index]); + const actualCellText = Cypress.$(rowCells[index]).text(); + + const matches = actualCellText == expectedCellText; + + if (!matches) { + return false; + } } + + return true; } /** @@ -96,9 +137,9 @@ class TableValidator { * @returns {string} - The type of the row. * @throws {Error} - If the row type is invalid. */ - getRowType(rowCells, row) { - if (isTableHeaderRow(rowCells)) return "HEADER"; - if (isTableDataRow(rowCells)) return "DATA"; + getRowType(rowCells) { + if (isTableHeaderRow(rowCells)) return HEADER_ROW; + if (isTableDataRow(rowCells)) return BODY_ROW; throw new Error("Invalid row type", rowCells); } @@ -113,7 +154,9 @@ class TableValidator { function traverseCellAndAppendTextContent(cell) { if (cell.value) { - text += cell.value; + //Strip out HTML tags due to unescaped characters in certain content + //E.g. cookies -> analytical cookies + text += cell.value.replace(/(<([^>]+)>)/gi, ""); } if (cell.content) { diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/README.md b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/README.md deleted file mode 100644 index 1607529d8..000000000 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# User Journey Info Scripts - -## Overview - -Javascript code that takes data that has been exported from Contentful, and for each section provides information about: - -1. The total possible number of user journeys, grouped by maturity result -2. The minimum number of paths required to display every possible question -3. The shortest possible user journey possible to get each possible recommendation page (for each maturity) for each section -4. Optional: every single possible user journey. - -This information is saved in JSON format, in the `output` sub-directory. A file is created per section, with the section name as the file name (e.g. for `Broadband connection` the result is `./output/Broadband connection.json`). - -It will also check for potential data issues. Currently it validates: -1. That all questions for a section can be navigated to in at least one user journey. -2. All maturities for each section can be reached from at least one user journey - -This information is output in the console. - -## Running - -1. [Export the Contentful data via their CLI](https://www.contentful.com/developers/docs/tutorials/cli/import-and-export/) -2. Ensure the exported file JSON is in this directory (i.e. [/contentful/user-journey-info-scripts](/contentful/user-journey-info-scripts/)) -3. Navigate to the directory above in a terminal -4. Run the script by running `node test-suit-generator.mjs '{CONTENTFUL_EXPORT_JSON_PATH}'` in the terminal. Where `{CONTENTFUL_EXPORT_JSON_PATH}` is the file path for your exported Contentful data. E.g. `node test-suit-generator.mjs 'contentfulexport.json'`, if your Contentful data is exported as `contentfulexport.json`. - -By default, the script will _not_ export all possible user journeys per section. If you wish to do this, simply add another `true` as another argument to the run command on step 4. I.e. `node test-suit-generator.mjs '{CONTENTFUL_EXPORT_JSON_PATH}' true` - diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/content-type.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/content-type.js deleted file mode 100644 index 2b8a45e94..000000000 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/content-type.js +++ /dev/null @@ -1,39 +0,0 @@ -const ARRAY_TYPE = "Array"; -const LINK_TYPE = "Link"; - -class ContentType { - id; - fields; - - constructor({ sys, fields }) { - this.id = sys.id; - this.fields = {}; - - for (const field of fields) { - this.fields[field.id] = field; - } - } - - getReferencedTypesForField(field) { - const matching = this.fields[field]; - - if (!matching) throw `Couldn't find ${field} in ${this.fields}`; - - if (matching.type !== ARRAY_TYPE && matching.type !== LINK_TYPE) return; - - const validations = - matching.type == ARRAY_TYPE - ? matching.items.validations - : matching.validations; - - if (!validations) return false; - - const linkContentType = validations - .filter((validation) => validation.linkContentType) - .map((validation) => validation.linkContentType)[0]; - - return linkContentType.length == 0 ? undefined : linkContentType; - } -} - -export default ContentType; diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/functions.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/functions.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/recommendation.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/recommendation.js deleted file mode 100644 index e0d2c200a..000000000 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/recommendation.js +++ /dev/null @@ -1,16 +0,0 @@ -export class Recommendation { - slug; - maturity; - displayName; - id; - page; - - constructor({ fields, sys }) { - this.slug = fields.slug; - this.maturity = fields.maturity; - this.displayName = fields.displayName; - this.id = sys.id; - this.page = fields.page; - this.slug = fields.page?.fields.slug; - } -} diff --git a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/test-suit-generator.js b/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/test-suit-generator.js deleted file mode 100644 index b8fdf620d..000000000 --- a/tests/Dfe.PlanTech.Web.E2ETests/cypress/helpers/contentful-helpers/test-suit-generator.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from "fs"; -import DataMapper from "./data-mapper"; - -const { file, writeAllPossiblePaths } = getArguments(); - -const dataMapper = new DataMapper(file); - -for (const section of dataMapper.mappedSections) { - const output = dataMapper.convertToMinimalSectionInfo( - section, - writeAllPossiblePaths - ); - const json = JSON.stringify(output); - - fs.writeFileSync(`./output/${section.name}.json`, json); -} - -/** - * Retrieves the command line arguments and returns an object containing the file path - * and a boolean indicating whether to write all possible paths. - * - * @return {Object} Object with file path and boolean for writing all possible paths - */ -function getArguments() { - const processArgs = process.argv.slice(2); - - if (processArgs.length == 0) { - console.error("Please provide a file path as the first argument"); - process.exit(1); - } - - return { - file: processArgs[0], - writeAllPossiblePaths: processArgs.length > 1 && processArgs[1] == "true", - }; -}