From 668265ceb014aefd9d04c225a53f64bf1bed2d63 Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Sat, 3 Oct 2026 21:03:07 -0500 Subject: [PATCH 1/3] Use ClassRelationshipDTO in package cycles Use ClassRelationshipDTO in package cycles to indicate what class relationships need to be removed --- assets/refactor-first-report.mustache | 2 +- lib/renderer.js | 50 +- lib/table-operations.js | 4 +- public/assets/refactor-first-report.mustache | 2 +- tests/fixtures/junit4-report.json | 980 ++++++++++++++++++- tests/unit/renderer.test.js | 126 +++ tests/unit/table-operations.test.js | 59 ++ 7 files changed, 1194 insertions(+), 29 deletions(-) diff --git a/assets/refactor-first-report.mustache b/assets/refactor-first-report.mustache index 0b9c568..f0c0c39 100644 --- a/assets/refactor-first-report.mustache +++ b/assets/refactor-first-report.mustache @@ -584,7 +584,7 @@ {{cycleCount}} {{effortRank}} {{#classRelationshipsToBreakPackage}} - {{{.}}}
{{/classRelationshipsToBreakPackage}} + {{{safeRenderedLabel}}}
{{/classRelationshipsToBreakPackage}} {{/packageRelationshipsToRemove.relationships}} diff --git a/lib/renderer.js b/lib/renderer.js index 5227ff7..8c46527 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -51,23 +51,57 @@ function registerLinkSafetyHook() { } /** - * Copies a relationship with label markup limited to source links and markers. + * Sanitizes a relationship label down to links and removal markers. * The report-wide sanitizer permits template styles, so labels need their own * allow-list before interpolation. Never trust a precomputed value from JSON. * + * @param {string} label - Raw label markup from the report JSON. + * @returns {string} Label restricted to anchor and marker markup. + */ +function sanitizeRelationshipLabel(label) { + return DOMPurify.sanitize(label ?? '', { + ALLOWED_TAGS: ['a', 'strong'], + ALLOWED_ATTR: ['href', 'target', 'rel'], + ALLOW_DATA_ATTR: false, + ALLOW_ARIA_ATTR: false + }); +} + +/** + * Normalizes one "class relationship to break a package cycle" entry. + * Reports serialize ClassRelationshipDTO objects; entries of any other + * shape (including pre-DTO rendered-label strings) are dropped. + * + * @param {*} entry - Raw class-break entry. + * @returns {object|null} Entry extended with a sanitized label, or null when + * the entry is not a valid ClassRelationshipDTO object. + */ +function prepareClassBreakEntry(entry) { + if (!entry || typeof entry !== 'object') return null; + return { + ...entry, + safeRenderedLabel: sanitizeRelationshipLabel(entry.renderedLabel) + }; +} + +/** + * Copies a relationship with label markup limited to source links and markers, + * and normalizes its class-break entries the same way. Never trust a + * precomputed value from JSON. + * * @param {object} relationship - Raw relationship data. - * @returns {object} Relationship with a restricted HTML label. + * @returns {object} Relationship with restricted HTML labels. */ function prepareRelationship(relationship) { registerLinkSafetyHook(); return { ...relationship, - safeRenderedLabel: DOMPurify.sanitize(relationship.renderedLabel ?? '', { - ALLOWED_TAGS: ['a', 'strong'], - ALLOWED_ATTR: ['href', 'target', 'rel'], - ALLOW_DATA_ATTR: false, - ALLOW_ARIA_ATTR: false - }) + safeRenderedLabel: sanitizeRelationshipLabel(relationship.renderedLabel), + classRelationshipsToBreakPackage: Array.isArray(relationship.classRelationshipsToBreakPackage) + ? relationship.classRelationshipsToBreakPackage + .map(prepareClassBreakEntry) + .filter(entry => entry !== null) + : [] }; } diff --git a/lib/table-operations.js b/lib/table-operations.js index b038709..805b90d 100644 --- a/lib/table-operations.js +++ b/lib/table-operations.js @@ -384,7 +384,9 @@ export const REPORT_TABLES = { { key: 'classRelationshipsToBreakPackage', label: 'Class Relationships to Remove To Break Package Relationship', - value: row => (row.classRelationshipsToBreakPackage ?? []).map(stripHtml).join('; ') + value: row => (row.classRelationshipsToBreakPackage ?? []) + .map(entry => stripHtml(entry?.renderedLabel ?? '')) + .join('; ') } ] }, diff --git a/public/assets/refactor-first-report.mustache b/public/assets/refactor-first-report.mustache index 0b9c568..f0c0c39 100644 --- a/public/assets/refactor-first-report.mustache +++ b/public/assets/refactor-first-report.mustache @@ -584,7 +584,7 @@ {{cycleCount}} {{effortRank}} {{#classRelationshipsToBreakPackage}} - {{{.}}}
{{/classRelationshipsToBreakPackage}} + {{{safeRenderedLabel}}}
{{/classRelationshipsToBreakPackage}} {{/packageRelationshipsToRemove.relationships}} diff --git a/tests/fixtures/junit4-report.json b/tests/fixtures/junit4-report.json index cd1d9aa..c48ef1c 100644 --- a/tests/fixtures/junit4-report.json +++ b/tests/fixtures/junit4-report.json @@ -703,7 +703,19 @@ "priority" : 1, "cycleCount" : 4, "effortRank" : 1, - "classRelationshipsToBreakPackage" : [ "MethodSorter → MethodSorters : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.internal.MethodSorter", + "targetClass" : "org.junit.runners.MethodSorters", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "MethodSorter → MethodSorters : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runners", "targetPackage" : "org.junit.internal.builders", @@ -714,7 +726,19 @@ "priority" : 2, "cycleCount" : 4, "effortRank" : 1, - "classRelationshipsToBreakPackage" : [ "Suite → AllDefaultPossibilitiesBuilder : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runners.Suite", + "targetClass" : "org.junit.internal.builders.AllDefaultPossibilitiesBuilder", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Suite → AllDefaultPossibilitiesBuilder : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runner", "targetPackage" : "org.junit.internal.builders", @@ -725,7 +749,19 @@ "priority" : 3, "cycleCount" : 4, "effortRank" : 2, - "classRelationshipsToBreakPackage" : [ "Request* → AllDefaultPossibilitiesBuilder : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runner.Request", + "targetClass" : "org.junit.internal.builders.AllDefaultPossibilitiesBuilder", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "Request* → AllDefaultPossibilitiesBuilder : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runner", "targetPackage" : "org.junit.internal.requests", @@ -736,7 +772,58 @@ "priority" : 4, "cycleCount" : 4, "effortRank" : 5, - "classRelationshipsToBreakPackage" : [ "Request* → SortingRequest : 1", "Request* → ClassRequest : 2", "Request* → FilterRequest : 1", "Request* → OrderingRequest : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runner.Request", + "targetClass" : "org.junit.internal.requests.SortingRequest", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Request* → SortingRequest : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.Request", + "targetClass" : "org.junit.internal.requests.ClassRequest", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "Request* → ClassRequest : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.Request", + "targetClass" : "org.junit.internal.requests.FilterRequest", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Request* → FilterRequest : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.Request", + "targetClass" : "org.junit.internal.requests.OrderingRequest", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Request* → OrderingRequest : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.internal", "targetPackage" : "org.junit.runner", @@ -747,7 +834,32 @@ "priority" : 5, "cycleCount" : 4, "effortRank" : 12, - "classRelationshipsToBreakPackage" : [ "TextListener → Description : 2", "TextListener → Result : 10" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.internal.TextListener", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "TextListener → Description : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.TextListener", + "targetClass" : "org.junit.runner.Result", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 10, + "renderedLabel" : "TextListener → Result : 10", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit", "targetPackage" : "org.junit.runners", @@ -758,7 +870,19 @@ "priority" : 6, "cycleCount" : 3, "effortRank" : 1, - "classRelationshipsToBreakPackage" : [ "FixMethodOrder → MethodSorters : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.FixMethodOrder", + "targetClass" : "org.junit.runners.MethodSorters", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "FixMethodOrder → MethodSorters : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runner.notification", "targetPackage" : "org.junit.internal", @@ -769,7 +893,19 @@ "priority" : 7, "cycleCount" : 3, "effortRank" : 2, - "classRelationshipsToBreakPackage" : [ "Failure* → Throwables : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runner.notification.Failure", + "targetClass" : "org.junit.internal.Throwables", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "Failure* → Throwables : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runners.model", "targetPackage" : "org.junit.internal.runners.model", @@ -780,7 +916,32 @@ "priority" : 8, "cycleCount" : 3, "effortRank" : 2, - "classRelationshipsToBreakPackage" : [ "FrameworkMethod* → ReflectiveCallable : 1", "MultipleFailureException → MultipleFailureException : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runners.model.FrameworkMethod", + "targetClass" : "org.junit.internal.runners.model.ReflectiveCallable", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "FrameworkMethod* → ReflectiveCallable : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runners.model.MultipleFailureException", + "targetClass" : "org.junit.internal.runners.model.MultipleFailureException", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "MultipleFailureException → MultipleFailureException : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.internal.runners", "targetPackage" : "org.junit.runners.model", @@ -791,7 +952,45 @@ "priority" : 9, "cycleCount" : 3, "effortRank" : 5, - "classRelationshipsToBreakPackage" : [ "ErrorReportingRunner → InvalidTestClassError : 1", "ErrorReportingRunner → InitializationError : 3", "MethodRoadie → TestTimedOutException : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.internal.runners.ErrorReportingRunner", + "targetClass" : "org.junit.runners.model.InvalidTestClassError", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "ErrorReportingRunner → InvalidTestClassError : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.ErrorReportingRunner", + "targetClass" : "org.junit.runners.model.InitializationError", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 3, + "renderedLabel" : "ErrorReportingRunner → InitializationError : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.MethodRoadie", + "targetClass" : "org.junit.runners.model.TestTimedOutException", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "MethodRoadie → TestTimedOutException : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.internal", "targetPackage" : "org.junit", @@ -802,7 +1001,58 @@ "priority" : 10, "cycleCount" : 3, "effortRank" : 8, - "classRelationshipsToBreakPackage" : [ "InexactComparisonCriteria → Assert : 2", "ExactComparisonCriteria → Assert : 1", "ComparisonCriteria → Assert : 3", "MethodSorter → FixMethodOrder : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.internal.InexactComparisonCriteria", + "targetClass" : "org.junit.Assert", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "InexactComparisonCriteria → Assert : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.ExactComparisonCriteria", + "targetClass" : "org.junit.Assert", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "ExactComparisonCriteria → Assert : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.ComparisonCriteria", + "targetClass" : "org.junit.Assert", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 3, + "renderedLabel" : "ComparisonCriteria → Assert : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.MethodSorter", + "targetClass" : "org.junit.FixMethodOrder", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "MethodSorter → FixMethodOrder : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runners.model", "targetPackage" : "org.junit.runner", @@ -813,7 +1063,45 @@ "priority" : 11, "cycleCount" : 3, "effortRank" : 15, - "classRelationshipsToBreakPackage" : [ "RunnerBuilder* → Description : 2", "RunnerBuilder* → Runner : 11", "RunnerBuilder* → OrderWith : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runners.model.RunnerBuilder", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "RunnerBuilder* → Description : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runners.model.RunnerBuilder", + "targetClass" : "org.junit.runner.Runner", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 11, + "renderedLabel" : "RunnerBuilder* → Runner : 11", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runners.model.RunnerBuilder", + "targetClass" : "org.junit.runner.OrderWith", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "RunnerBuilder* → OrderWith : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runners.parameterized", "targetPackage" : "org.junit.runners", @@ -824,7 +1112,45 @@ "priority" : 12, "cycleCount" : 3, "effortRank" : 16, - "classRelationshipsToBreakPackage" : [ "BlockJUnit4ClassRunnerWithParameters → Parameterized$Parameter : 3", "BlockJUnit4ClassRunnerWithParameters → BlockJUnit4ClassRunner* : 5", "BlockJUnit4ClassRunnerWithParameters → ParentRunner : 8" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters", + "targetClass" : "org.junit.runners.Parameterized$Parameter", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 3, + "renderedLabel" : "BlockJUnit4ClassRunnerWithParameters → Parameterized$Parameter : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters", + "targetClass" : "org.junit.runners.BlockJUnit4ClassRunner", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 5, + "renderedLabel" : "BlockJUnit4ClassRunnerWithParameters → BlockJUnit4ClassRunner* : 5", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters", + "targetClass" : "org.junit.runners.ParentRunner", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 8, + "renderedLabel" : "BlockJUnit4ClassRunnerWithParameters → ParentRunner : 8", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runner.notification", "targetPackage" : "org.junit.runner", @@ -835,7 +1161,97 @@ "priority" : 13, "cycleCount" : 3, "effortRank" : 25, - "classRelationshipsToBreakPackage" : [ "RunListener → Result : 1", "RunListener → Description : 6", "SynchronizedRunListener → Description : 6", "RunNotifier → Result : 1", "Failure* → Description : 4", "RunNotifier → Description : 6", "SynchronizedRunListener → Result : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runner.notification.RunListener", + "targetClass" : "org.junit.runner.Result", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "RunListener → Result : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.RunListener", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 6, + "renderedLabel" : "RunListener → Description : 6", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.SynchronizedRunListener", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 6, + "renderedLabel" : "SynchronizedRunListener → Description : 6", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.RunNotifier", + "targetClass" : "org.junit.runner.Result", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "RunNotifier → Result : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.Failure", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 4, + "renderedLabel" : "Failure* → Description : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.RunNotifier", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 6, + "renderedLabel" : "RunNotifier → Description : 6", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.notification.SynchronizedRunListener", + "targetClass" : "org.junit.runner.Result", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "SynchronizedRunListener → Result : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "junit.framework", "targetPackage" : "org.junit.runner", @@ -846,7 +1262,97 @@ "priority" : 14, "cycleCount" : 3, "effortRank" : 39, - "classRelationshipsToBreakPackage" : [ "JUnit4TestAdapter* → Description : 13", "JUnit4TestAdapterCache → Description : 14", "JUnit4TestCaseFacade → Description : 4", "JUnit4TestCaseFacade → Describable : 1", "JUnit4TestAdapter* → Request* : 2", "JUnit4TestAdapter* → Runner : 4", "JUnit4TestAdapter* → Describable : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "junit.framework.JUnit4TestAdapter", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 13, + "renderedLabel" : "JUnit4TestAdapter* → Description : 13", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestAdapterCache", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 14, + "renderedLabel" : "JUnit4TestAdapterCache → Description : 14", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestCaseFacade", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 4, + "renderedLabel" : "JUnit4TestCaseFacade → Description : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestCaseFacade", + "targetClass" : "org.junit.runner.Describable", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "JUnit4TestCaseFacade → Describable : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestAdapter", + "targetClass" : "org.junit.runner.Request", + "sourceMarked" : true, + "targetMarked" : true, + "weight" : 2, + "renderedLabel" : "JUnit4TestAdapter* → Request* : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestAdapter", + "targetClass" : "org.junit.runner.Runner", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 4, + "renderedLabel" : "JUnit4TestAdapter* → Runner : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "junit.framework.JUnit4TestAdapter", + "targetClass" : "org.junit.runner.Describable", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "JUnit4TestAdapter* → Describable : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.internal.runners", "targetPackage" : "org.junit.runner", @@ -857,7 +1363,149 @@ "priority" : 15, "cycleCount" : 3, "effortRank" : 41, - "classRelationshipsToBreakPackage" : [ "JUnit38ClassRunner$OldTestClassAdaptingListener → Describable : 4", "ErrorReportingRunner → Runner : 1", "ErrorReportingRunner → Description : 7", "JUnit38ClassRunner* → Describable : 4", "JUnit38ClassRunner* → Runner : 2", "JUnit4ClassRunner → Runner : 1", "JUnit38ClassRunner* → Description : 8", "JUnit38ClassRunner$OldTestClassAdaptingListener → Description : 2", "ClassRoadie → Description : 2", "JUnit4ClassRunner → Description : 8", "MethodRoadie → Description : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.internal.runners.JUnit38ClassRunner$OldTestClassAdaptingListener", + "targetClass" : "org.junit.runner.Describable", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 4, + "renderedLabel" : "JUnit38ClassRunner$OldTestClassAdaptingListener → Describable : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.ErrorReportingRunner", + "targetClass" : "org.junit.runner.Runner", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "ErrorReportingRunner → Runner : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.ErrorReportingRunner", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 7, + "renderedLabel" : "ErrorReportingRunner → Description : 7", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit38ClassRunner", + "targetClass" : "org.junit.runner.Describable", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 4, + "renderedLabel" : "JUnit38ClassRunner* → Describable : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit38ClassRunner", + "targetClass" : "org.junit.runner.Runner", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "JUnit38ClassRunner* → Runner : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit4ClassRunner", + "targetClass" : "org.junit.runner.Runner", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "JUnit4ClassRunner → Runner : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit38ClassRunner", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 8, + "renderedLabel" : "JUnit38ClassRunner* → Description : 8", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit38ClassRunner$OldTestClassAdaptingListener", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "JUnit38ClassRunner$OldTestClassAdaptingListener → Description : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.ClassRoadie", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "ClassRoadie → Description : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.JUnit4ClassRunner", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 8, + "renderedLabel" : "JUnit4ClassRunner → Description : 8", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.internal.runners.MethodRoadie", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 2, + "renderedLabel" : "MethodRoadie → Description : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.validator", "targetPackage" : "org.junit.runners.model", @@ -868,7 +1516,175 @@ "priority" : 16, "cycleCount" : 2, "effortRank" : 31, - "classRelationshipsToBreakPackage" : [ "AnnotationsValidator$FieldValidator → TestClass* : 2", "AnnotationValidator → FrameworkMethod* : 1", "AnnotationsValidator$MethodValidator → TestClass* : 2", "AnnotationsValidator → TestClass* : 1", "AnnotationsValidator$MethodValidator → FrameworkMethod* : 3", "AnnotationsValidator$AnnotatableValidator → Annotatable : 7", "AnnotationValidator → FrameworkField : 1", "TestClassValidator → TestClass* : 1", "AnnotationsValidator$FieldValidator → FrameworkField : 3", "AnnotationsValidator$ClassValidator → TestClass* : 4", "AnnotationValidator → TestClass* : 1", "PublicClassValidator → TestClass* : 3", "AnnotationsValidator$AnnotatableValidator → TestClass* : 2" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.validator.AnnotationsValidator$FieldValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 2, + "renderedLabel" : "AnnotationsValidator$FieldValidator → TestClass* : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationValidator", + "targetClass" : "org.junit.runners.model.FrameworkMethod", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 1, + "renderedLabel" : "AnnotationValidator → FrameworkMethod* : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$MethodValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 2, + "renderedLabel" : "AnnotationsValidator$MethodValidator → TestClass* : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 1, + "renderedLabel" : "AnnotationsValidator → TestClass* : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$MethodValidator", + "targetClass" : "org.junit.runners.model.FrameworkMethod", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 3, + "renderedLabel" : "AnnotationsValidator$MethodValidator → FrameworkMethod* : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$AnnotatableValidator", + "targetClass" : "org.junit.runners.model.Annotatable", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 7, + "renderedLabel" : "AnnotationsValidator$AnnotatableValidator → Annotatable : 7", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationValidator", + "targetClass" : "org.junit.runners.model.FrameworkField", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "AnnotationValidator → FrameworkField : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.TestClassValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 1, + "renderedLabel" : "TestClassValidator → TestClass* : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$FieldValidator", + "targetClass" : "org.junit.runners.model.FrameworkField", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 3, + "renderedLabel" : "AnnotationsValidator$FieldValidator → FrameworkField : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$ClassValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 4, + "renderedLabel" : "AnnotationsValidator$ClassValidator → TestClass* : 4", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 1, + "renderedLabel" : "AnnotationValidator → TestClass* : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.PublicClassValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 3, + "renderedLabel" : "PublicClassValidator → TestClass* : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.validator.AnnotationsValidator$AnnotatableValidator", + "targetClass" : "org.junit.runners.model.TestClass", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 2, + "renderedLabel" : "AnnotationsValidator$AnnotatableValidator → TestClass* : 2", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.experimental.theories", "targetPackage" : "org.junit.experimental.theories.internal", @@ -879,7 +1695,45 @@ "priority" : 17, "cycleCount" : 1, "effortRank" : 13, - "classRelationshipsToBreakPackage" : [ "FromDataPoints → SpecificDataPointsSupplier* : 1", "Theories$TheoryAnchor → Assignments : 11", "Theories$TheoryAnchor → ParameterizedAssertionError : 1" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.experimental.theories.FromDataPoints", + "targetClass" : "org.junit.experimental.theories.internal.SpecificDataPointsSupplier", + "sourceMarked" : false, + "targetMarked" : true, + "weight" : 1, + "renderedLabel" : "FromDataPoints → SpecificDataPointsSupplier* : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.experimental.theories.Theories$TheoryAnchor", + "targetClass" : "org.junit.experimental.theories.internal.Assignments", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 11, + "renderedLabel" : "Theories$TheoryAnchor → Assignments : 11", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.experimental.theories.Theories$TheoryAnchor", + "targetClass" : "org.junit.experimental.theories.internal.ParameterizedAssertionError", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Theories$TheoryAnchor → ParameterizedAssertionError : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] }, { "sourcePackage" : "org.junit.runner.manipulation", "targetPackage" : "org.junit.runner", @@ -890,7 +1744,97 @@ "priority" : 18, "cycleCount" : 1, "effortRank" : 44, - "classRelationshipsToBreakPackage" : [ "Ordering$1 → Description : 1", "Ordering → Description : 7", "Sorter* → Description : 11", "Filter* → Description : 10", "Orderer* → Description : 7", "Alphanumeric → Description : 5", "Ordering$Context → Description : 3" ] + "classRelationshipsToBreakPackage" : [ { + "sourceClass" : "org.junit.runner.manipulation.Ordering$1", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 1, + "renderedLabel" : "Ordering$1 → Description : 1", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Ordering", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 7, + "renderedLabel" : "Ordering → Description : 7", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Sorter", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 11, + "renderedLabel" : "Sorter* → Description : 11", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Filter", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 10, + "renderedLabel" : "Filter* → Description : 10", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Orderer", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : true, + "targetMarked" : false, + "weight" : 7, + "renderedLabel" : "Orderer* → Description : 7", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Alphanumeric", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 5, + "renderedLabel" : "Alphanumeric → Description : 5", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + }, +{ + "sourceClass" : "org.junit.runner.manipulation.Ordering$Context", + "targetClass" : "org.junit.runner.Description", + "sourceMarked" : false, + "targetMarked" : false, + "weight" : 3, + "renderedLabel" : "Ordering$Context → Description : 3", + "priority" : 0, + "cycleCount" : 0, + "effortRank" : 0, + "alsoRemovesPackageRelationship" : false, + "packageCycleCount" : 0 + } ] } ] }, "hasDisharmonies" : true, diff --git a/tests/unit/renderer.test.js b/tests/unit/renderer.test.js index eccccf5..96f8423 100644 --- a/tests/unit/renderer.test.js +++ b/tests/unit/renderer.test.js @@ -602,3 +602,129 @@ describe('prepareReportData', () => { expect(disharmonyThs[1].querySelector('.rf-sort-indicator').textContent).toBe('▲'); }); }); + +// --------------------------------------------------------------------------- +// Class relationships to break a package cycle: RefactorFirst serializes +// structured ClassRelationshipDTO objects, which must render through the +// package relationships table cell, sanitized to links and markers only. +// Pre-DTO string entries are no longer part of the schema and are dropped. +// --------------------------------------------------------------------------- + +describe('class relationships to break a package cycle cell rendering', () => { + const template = readFileSync( + path.join(import.meta.dir, '../../assets/refactor-first-report.mustache'), 'utf8' + ); + + const structuredEntry = { + sourceClass: 'org.junit.runner.Request', + targetClass: 'org.junit.internal.requests.SortingRequest', + sourceMarked: true, + targetMarked: false, + weight: 2, + renderedLabel: + 'Request* → ' + + 'SortingRequest : 2', + cycleCount: 1 + }; + const secondEntry = { + sourceClass: 'org.junit.internal.MethodSorter', + targetClass: 'org.junit.runners.MethodSorters', + sourceMarked: false, + targetMarked: false, + weight: 1, + renderedLabel: + 'MethodSorter → ' + + 'MethodSorters : 1', + cycleCount: 1 + }; + + function reportWithBreakEntries(entries) { + return { + project: { name: 'Demo', version: '1.0' }, + packageRelationshipsToRemove: { + hasRelationships: true, + relationships: [ + { + renderedLabel: 'org.junit.runner → org.junit.internal : 2', + priority: 1, + cycleCount: 1, + effortRank: 1, + classRelationshipsToBreakPackage: entries + } + ] + } + }; + } + + function breakCellOf(html) { + const doc = new JSDOM(html).window.document; + const cell = doc.querySelector( + 'table[data-rf-table="package-relationships"] tbody td:nth-of-type(5)' + ); + expect(cell).not.toBeNull(); + return cell; + } + + it('renders structured ClassRelationshipDTO entries from current reports', () => { + const html = renderTemplate(template, prepareReportData(reportWithBreakEntries([structuredEntry]))); + const cell = breakCellOf(html); + expect(cell.textContent).toContain('Request* → SortingRequest : 2'); + expect(cell.querySelector('a')?.getAttribute('href')).toBe('https://example.com/Request.java'); + expect(cell.querySelector('a')?.getAttribute('rel')).toBe('noopener noreferrer'); + }); + + it('drops pre-DTO string entries from older reports', () => { + const html = renderTemplate( + template, + prepareReportData(reportWithBreakEntries([ + structuredEntry, + 'MethodSorter → ' + + 'MethodSorters : 1' + ])) + ); + const cell = breakCellOf(html); + expect(cell.textContent).toContain('Request* → SortingRequest : 2'); + expect(cell.textContent).not.toContain('MethodSorter'); + expect(cell.querySelectorAll('br').length).toBe(1); + }); + + it('renders multiple entries on separate lines', () => { + const html = renderTemplate( + template, + prepareReportData(reportWithBreakEntries([structuredEntry, secondEntry])) + ); + const cell = breakCellOf(html); + expect(cell.querySelectorAll('br').length).toBe(2); + expect(cell.textContent).toContain('Request* → SortingRequest : 2'); + expect(cell.textContent).toContain('MethodSorter → MethodSorters : 1'); + }); + + it('sanitizes class-break labels to links and markers only', () => { + const hostile = { + ...structuredEntry, + renderedLabel: + '' + + 'X' + + '' + + 'Unsafe' + }; + const html = renderTemplate(template, prepareReportData(reportWithBreakEntries([hostile]))); + const cell = breakCellOf(html); + expect(cell.textContent).toContain('X'); + expect(cell.innerHTML).not.toContain('body {'); + expect(cell.innerHTML).not.toContain('onclick'); + expect(cell.innerHTML).not.toContain('onerror'); + expect(cell.innerHTML).not.toContain('javascript:'); + expect(cell.querySelector('img')).toBeNull(); + for (const element of cell.querySelectorAll('*')) { + expect(['A', 'BR']).toContain(element.tagName); + } + }); + + it('does not mutate the report data while normalizing entries', () => { + const data = reportWithBreakEntries([structuredEntry, secondEntry]); + const original = structuredClone(data); + prepareReportData(data); + expect(data).toEqual(original); + }); +}); diff --git a/tests/unit/table-operations.test.js b/tests/unit/table-operations.test.js index 7d16462..545c7f0 100644 --- a/tests/unit/table-operations.test.js +++ b/tests/unit/table-operations.test.js @@ -4,6 +4,8 @@ import { describe, it, expect, mock, afterEach } from 'bun:test'; import { TABLE_CONFIG, + REPORT_TABLES, + columnValue, paginateTableData, pageCount, sortTableData, @@ -413,3 +415,60 @@ describe('TABLE_CONFIG defaults', () => { expect(TABLE_CONFIG.stickyHeaders).toBe(true); }); }); + +describe('REPORT_TABLES package-relationships class-break column', () => { + const column = REPORT_TABLES['package-relationships'].columns.find( + col => col.key === 'classRelationshipsToBreakPackage' + ); + + it('exposes the class-break column', () => { + expect(column).toBeDefined(); + expect(column.label).toBe('Class Relationships to Remove To Break Package Relationship'); + }); + + it('reads the rendered label of structured ClassRelationshipDTO entries', () => { + const row = { + classRelationshipsToBreakPackage: [ + { + sourceClass: 'org.junit.runner.Request', + targetClass: 'org.junit.internal.requests.SortingRequest', + sourceMarked: true, + targetMarked: false, + weight: 2, + renderedLabel: + 'Request* → ' + + 'SortingRequest : 2' + }, + { + sourceClass: 'org.junit.runner.Request', + targetClass: 'org.junit.internal.requests.ClassRequest', + renderedLabel: + 'Request → ' + + 'ClassRequest : 2' + } + ] + }; + expect(columnValue(column, row)).toBe('Request * → SortingRequest : 2; Request → ClassRequest : 2'); + }); + + it('ignores pre-DTO string entries from older reports', () => { + const row = { + classRelationshipsToBreakPackage: [ + 'MethodSorter → ' + + 'MethodSorters : 1' + ] + }; + expect(columnValue(column, row)).toBe(''); + }); + + it('tolerates missing or malformed entries', () => { + expect(columnValue(column, {})).toBe(''); + expect(columnValue(column, { classRelationshipsToBreakPackage: null })).toBe(''); + expect(columnValue(column, { classRelationshipsToBreakPackage: [] })).toBe(''); + expect( + columnValue(column, { + classRelationshipsToBreakPackage: [{ sourceClass: 'a.B' }, null, { renderedLabel: 'Plain label' }] + }) + ).toBe('; ; Plain label'); + }); +}); From 0bb8724e71698f725df48e790416c7c822179d05 Mon Sep 17 00:00:00 2001 From: Jim Bethancourt Date: Sat, 3 Oct 2026 21:48:52 -0500 Subject: [PATCH 2/3] Applying Coderabbit suggestions --- assets/refactor-first-report.mustache | 2 +- lib/renderer.js | 17 ++++++++--------- public/assets/refactor-first-report.mustache | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/assets/refactor-first-report.mustache b/assets/refactor-first-report.mustache index f0c0c39..0b9c568 100644 --- a/assets/refactor-first-report.mustache +++ b/assets/refactor-first-report.mustache @@ -584,7 +584,7 @@ {{cycleCount}} {{effortRank}} {{#classRelationshipsToBreakPackage}} - {{{safeRenderedLabel}}}
{{/classRelationshipsToBreakPackage}} + {{{.}}}
{{/classRelationshipsToBreakPackage}} {{/packageRelationshipsToRemove.relationships}} diff --git a/lib/renderer.js b/lib/renderer.js index 8c46527..16b3c96 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -68,20 +68,19 @@ function sanitizeRelationshipLabel(label) { } /** - * Normalizes one "class relationship to break a package cycle" entry. - * Reports serialize ClassRelationshipDTO objects; entries of any other - * shape (including pre-DTO rendered-label strings) are dropped. + * Adapts one "class relationship to break a package cycle" entry for the + * template. Reports serialize structured ClassRelationshipDTO objects, and + * the template renders each entry as a raw string, so the entry is replaced + * by its rendered label, sanitized to links and removal markers. Entries + * of any other shape (including pre-DTO rendered-label strings) are dropped. * * @param {*} entry - Raw class-break entry. - * @returns {object|null} Entry extended with a sanitized label, or null when - * the entry is not a valid ClassRelationshipDTO object. + * @returns {string|null} Sanitized label markup, or null when the entry is + * not a valid ClassRelationshipDTO object. */ function prepareClassBreakEntry(entry) { if (!entry || typeof entry !== 'object') return null; - return { - ...entry, - safeRenderedLabel: sanitizeRelationshipLabel(entry.renderedLabel) - }; + return sanitizeRelationshipLabel(entry.renderedLabel); } /** diff --git a/public/assets/refactor-first-report.mustache b/public/assets/refactor-first-report.mustache index f0c0c39..0b9c568 100644 --- a/public/assets/refactor-first-report.mustache +++ b/public/assets/refactor-first-report.mustache @@ -584,7 +584,7 @@ {{cycleCount}} {{effortRank}} {{#classRelationshipsToBreakPackage}} - {{{safeRenderedLabel}}}
{{/classRelationshipsToBreakPackage}} + {{{.}}}
{{/classRelationshipsToBreakPackage}} {{/packageRelationshipsToRemove.relationships}} From 503406e62cb02f5c7e7982cefdc28f4915690216 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:54:24 +0000 Subject: [PATCH 3/3] Document package cycle renderer test helpers with JSDoc --- tests/unit/renderer.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/renderer.test.js b/tests/unit/renderer.test.js index 96f8423..ff99e86 100644 --- a/tests/unit/renderer.test.js +++ b/tests/unit/renderer.test.js @@ -638,6 +638,11 @@ describe('class relationships to break a package cycle cell rendering', () => { cycleCount: 1 }; + /** + * Builds a minimal report with the supplied class-break entries in one package relationship. + * @param {Array} entries - Structured DTOs or legacy labels to render. + * @returns {object} Report fixture for the package relationship table. + */ function reportWithBreakEntries(entries) { return { project: { name: 'Demo', version: '1.0' }, @@ -656,6 +661,11 @@ describe('class relationships to break a package cycle cell rendering', () => { }; } + /** + * Parses a rendered report and asserts that its package class-break cell exists. + * @param {string} html - Rendered report markup. + * @returns {HTMLTableCellElement} The first package relationship's class-break cell. + */ function breakCellOf(html) { const doc = new JSDOM(html).window.document; const cell = doc.querySelector(