From 1e905ef2cd6d48ba570912508157305d0edbdb9d Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 11:51:13 +0900 Subject: [PATCH 01/51] =?UTF-8?q?refactor(package):=20=E3=82=B0=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=AB=E7=8A=B6=E6=85=8B=E3=82=92packageCo?= =?UTF-8?q?ntext=E3=81=AB=E9=9B=86=E7=B4=84=E3=81=97=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=86=E3=83=8A=E3=83=B3=E3=82=B9=E6=80=A7=E3=82=92=E5=90=91?= =?UTF-8?q?=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.jsにおいて、散在していたモジュールスコープ変数を単一のpackageContextオブジェクトに集約しました。 これにより、以下の改善が期待されます。 - コードの可読性と推論の容易さの向上 - テストの独立性と安定性の向上 - 将来的な変更による副作用のリスク低減 関連するテストファイルも更新し、変更の正当性を検証済みです。 --- .../resources/templates/assets/package.js | 349 ++++++++---------- jig-core/src/test/js/package.test.js | 265 +++++++------ 2 files changed, 309 insertions(+), 305 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 85ec8cdf2..b3da32bc5 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -1,16 +1,18 @@ -let packageSummaryCache = null; -let diagramNodeIdToFqn = new Map(); -let aggregationDepth = 0; -let diagramElement = null; -let pendingDiagramRender = null; -let lastDiagramSource = ''; -let lastDiagramEdgeCount = 0; -const DEFAULT_MAX_EDGES = 500; -let packageFilterFqn = null; -let relatedFilterMode = 'direct'; -let relatedFilterFqn = null; -let diagramDirection = 'TD'; -let transitiveReductionEnabled = true; +const packageContext = { + packageSummaryCache: null, + diagramNodeIdToFqn: new Map(), + aggregationDepth: 0, + diagramElement: null, + pendingDiagramRender: null, + lastDiagramSource: '', + lastDiagramEdgeCount: 0, + DEFAULT_MAX_EDGES: 500, + packageFilterFqn: null, + relatedFilterMode: 'direct', + relatedFilterFqn: null, + diagramDirection: 'TD', + transitiveReductionEnabled: true, +}; function getOrCreateDiagramErrorBox(diagram) { let errorBox = document.getElementById('package-diagram-error'); @@ -43,8 +45,8 @@ function getOrCreateDiagramErrorBox(diagram) { return errorBox; } -function showDiagramErrorMessage(message, withAction, err, hash) { - const diagram = diagramElement; +function showDiagramErrorMessage(message, withAction, err, hash, context) { + const diagram = context.diagramElement; if (!diagram) return; console.error(message); if (err) { @@ -61,9 +63,9 @@ function showDiagramErrorMessage(message, withAction, err, hash) { actionNode.style.display = withAction ? '' : 'none'; if (withAction) { actionNode.onclick = function () { - if (!pendingDiagramRender) return; - renderDiagramSvg(pendingDiagramRender.text, pendingDiagramRender.maxEdges); - pendingDiagramRender = null; + if (!context.pendingDiagramRender) return; + renderDiagramSvg(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); + context.pendingDiagramRender = null; }; } else { actionNode.onclick = null; @@ -86,8 +88,8 @@ function hideDiagramErrorMessage(diagram) { diagram.style.display = ''; } -function renderDiagramSvg(text, maxEdges) { - const diagram = diagramElement; +function renderDiagramSvg(text, maxEdges, context) { + const diagram = context.diagramElement; if (!diagram || !window.mermaid) return; hideDiagramErrorMessage(diagram); diagram.removeAttribute('data-processed'); @@ -96,17 +98,17 @@ function renderDiagramSvg(text, maxEdges) { mermaid.run({nodes: [diagram]}); } -function getPackageSummaryData() { - if (packageSummaryCache) return packageSummaryCache; +function getPackageSummaryData(context) { + if (context.packageSummaryCache) return context.packageSummaryCache; const jsonText = document.getElementById('package-data').textContent; /** @type {{packages?: Array<{fqn: string, name: string, classCount: number, description: string}>, relations?: Array<{from: string, to: string}>, causeRelationEvidence?: Array<{from: string, to: string}>} | Array<{fqn: string, name: string, classCount: number, description: string}>} */ const packageData = JSON.parse(jsonText); - packageSummaryCache = { + context.packageSummaryCache = { packages: Array.isArray(packageData) ? packageData : (packageData.packages ?? []), relations: Array.isArray(packageData) ? [] : (packageData.relations ?? []), causeRelationEvidence: Array.isArray(packageData) ? [] : (packageData.causeRelationEvidence ?? []), }; - return packageSummaryCache; + return context.packageSummaryCache; } function getPackageDepth(fqn) { @@ -114,8 +116,8 @@ function getPackageDepth(fqn) { return fqn.split('.').length; } -function getMaxPackageDepth() { - const {packages} = getPackageSummaryData(); +function getMaxPackageDepth(context) { + const {packages} = getPackageSummaryData(context); return packages.reduce((max, item) => Math.max(max, getPackageDepth(item.fqn)), 0); } @@ -177,7 +179,7 @@ function buildAggregationStatsForPackageFilter(packages, relations, packageFilte return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } -function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, relatedFilterFqn, maxDepth) { +function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, relatedFilterFqn, maxDepth, context) { const withinPackageFilter = fqn => { if (!packageFilterFqn) return true; const prefix = `${packageFilterFqn}.`; @@ -189,21 +191,21 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, : relations; if (relatedFilterFqn) { - const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations); + const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, context.aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context); filteredPackages = filteredPackages.filter(item => - relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth)) + relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth)) ); - if (relatedFilterMode === 'direct') { + if (context.relatedFilterMode === 'direct') { filteredRelations = filteredRelations.filter(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); + const from = getAggregatedFqn(relation.from, context.aggregationDepth); + const to = getAggregatedFqn(relation.to, context.aggregationDepth); return from === aggregatedRoot || to === aggregatedRoot; }); } else { filteredRelations = filteredRelations.filter(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); + const from = getAggregatedFqn(relation.from, context.aggregationDepth); + const to = getAggregatedFqn(relation.to, context.aggregationDepth); return relatedSet.has(from) && relatedSet.has(to); }); } @@ -211,23 +213,23 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } -function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth) { +function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, context) { if (!rootFqn) { return buildAggregationStats(packages, relations, maxDepth); } - const aggregatedRoot = getAggregatedFqn(rootFqn, aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, relations); - const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth))); + const aggregatedRoot = getAggregatedFqn(rootFqn, context.aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, relations, context); + const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth))); const relatedRelations = relations.filter(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); + const from = getAggregatedFqn(relation.from, context.aggregationDepth); + const to = getAggregatedFqn(relation.to, context.aggregationDepth); return relatedSet.has(from) && relatedSet.has(to); }); return buildAggregationStats(relatedPackages, relatedRelations, maxDepth); } -function renderPackageTable() { - const {packages, relations} = getPackageSummaryData(); +function renderPackageTable(context) { + const {packages, relations} = getPackageSummaryData(context); const incomingCounts = new Map(); const outgoingCounts = new Map(); relations.forEach(relation => { @@ -242,12 +244,12 @@ function renderPackageTable() { if (input) { input.value = fqn; } - packageFilterFqn = fqn; - renderDiagramAndTable(); - renderRelatedFilterTarget(); + context.packageFilterFqn = fqn; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); }; const applyRelatedFilterForRow = fqn => { - applyRelatedFilter(fqn); + applyRelatedFilter(fqn, context); }; packages.forEach(item => { @@ -318,11 +320,11 @@ function applyPackageFilterToTable(packageFilterFqn) { }); } -function applyRelatedFilterToTable(fqn) { +function applyRelatedFilterToTable(fqn, context) { const rows = document.querySelectorAll('#package-table tbody tr'); - const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + const packageFilterPrefix = context.packageFilterFqn ? `${context.packageFilterFqn}.` : null; const withinPackageFilter = rowFqn => - !packageFilterFqn || rowFqn === packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); + !context.packageFilterFqn || rowFqn === context.packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); if (!fqn) { rows.forEach(row => { @@ -332,36 +334,36 @@ function applyRelatedFilterToTable(fqn) { }); return; } - const {relations} = getPackageSummaryData(); - const filteredRelations = packageFilterFqn + const {relations} = getPackageSummaryData(context); + const filteredRelations = context.packageFilterFqn ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to) ) : relations; - const aggregatedRoot = getAggregatedFqn(fqn, aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations); + const aggregatedRoot = getAggregatedFqn(fqn, context.aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context); rows.forEach(row => { const fqnCell = row.querySelector('td.fqn'); const rowFqn = fqnCell ? fqnCell.textContent : ''; - const aggregatedRow = getAggregatedFqn(rowFqn, aggregationDepth); + const aggregatedRow = getAggregatedFqn(rowFqn, context.aggregationDepth); const visible = withinPackageFilter(rowFqn) && relatedSet.has(aggregatedRow); row.classList.toggle('hidden', !visible); }); } -function renderRelatedFilterTarget() { +function renderRelatedFilterTarget(context) { const target = document.getElementById('related-filter-target'); if (!target) return; - target.textContent = relatedFilterFqn ? relatedFilterFqn : '未選択'; + target.textContent = context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'; } -function collectRelatedSet(root, relations) { +function collectRelatedSet(root, relations, context) { if (!root) return new Set(); - if (relatedFilterMode === 'direct') { + if (context.relatedFilterMode === 'direct') { const relatedSet = new Set([root]); relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); + const from = getAggregatedFqn(relation.from, context.aggregationDepth); + const to = getAggregatedFqn(relation.to, context.aggregationDepth); if (from === root) relatedSet.add(to); if (to === root) relatedSet.add(from); }); @@ -374,10 +376,10 @@ function collectRelatedSet(root, relations) { adjacency.get(from).add(to); }; relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); + const from = getAggregatedFqn(relation.from, context.aggregationDepth); + const to = getAggregatedFqn(relation.to, context.aggregationDepth); addEdge(from, to); - if (relatedFilterMode === 'all') { + if (context.relatedFilterMode === 'all') { addEdge(to, from); } }); @@ -397,13 +399,13 @@ function collectRelatedSet(root, relations) { return relatedSet; } -function renderDiagramAndTable() { - renderPackageDiagram(packageFilterFqn, relatedFilterFqn); - applyRelatedFilterToTable(relatedFilterFqn); - updateAggregationDepthOptions(getMaxPackageDepth()); +function renderDiagramAndTable(context) { + renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); + applyRelatedFilterToTable(context.relatedFilterFqn, context); + updateAggregationDepthOptions(getMaxPackageDepth(context), context); } -function renderMutualDependencyList(mutualPairs, causeRelationEvidence) { +function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) { const container = document.getElementById('mutual-dependency-list'); if (!container) return; if (!mutualPairs || mutualPairs.size === 0) { @@ -413,8 +415,8 @@ function renderMutualDependencyList(mutualPairs, causeRelationEvidence) { } const relationMap = new Map(); causeRelationEvidence.forEach(relation => { - const fromPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.from), aggregationDepth); - const toPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.to), aggregationDepth); + const fromPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.from), context.aggregationDepth); + const toPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.to), context.aggregationDepth); if (fromPackage === toPackage) return; const key = fromPackage < toPackage ? `${fromPackage}::${toPackage}` : `${toPackage}::${fromPackage}`; if (!relationMap.has(key)) { @@ -545,23 +547,23 @@ function transitiveReduction(relations) { return relations.filter(edge => !toRemove.has(`${edge.from}::${edge.to}`)); } -function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { +function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { const diagram = document.getElementById('package-relation-diagram'); if (!diagram) return; - diagramElement = diagram; + context.diagramElement = diagram; - const {packages, relations, causeRelationEvidence} = getPackageSummaryData(); + const {packages, relations, causeRelationEvidence} = getPackageSummaryData(context); const escapeMermaidText = text => text.replace(/"/g, '\\"'); const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); - const lines = [`graph ${diagramDirection}`]; - const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; + const lines = [`graph ${context.diagramDirection}`]; + const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, context.aggregationDepth) : null; const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; const withinPackageFilter = fqn => !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); const visiblePackages = packageFilterFqn ? packages.filter(item => withinPackageFilter(item.fqn)) : packages; - const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); + const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, context.aggregationDepth))); const filteredRelations = packageFilterFqn ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) : relations; @@ -574,8 +576,8 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { : causeRelationEvidence; const visibleRelations = filteredRelations .map(relation => ({ - from: getAggregatedFqn(relation.from, aggregationDepth), - to: getAggregatedFqn(relation.to, aggregationDepth), + from: getAggregatedFqn(relation.from, context.aggregationDepth), + to: getAggregatedFqn(relation.to, context.aggregationDepth), })) .filter(relation => relation.from !== relation.to); const uniqueRelationMap = new Map(); @@ -584,13 +586,13 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { }); let uniqueRelations = Array.from(uniqueRelationMap.values()); - if (transitiveReductionEnabled) { + if (context.transitiveReductionEnabled) { uniqueRelations = transitiveReduction(uniqueRelations); } if (aggregatedRoot) { - const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations); - if (relatedFilterMode === 'direct') { + const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, context); + if (context.relatedFilterMode === 'direct') { uniqueRelations = uniqueRelations.filter(relation => relation.from === aggregatedRoot || relation.to === aggregatedRoot ); @@ -608,14 +610,14 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { }); const nodeIdByFqn = new Map(); - diagramNodeIdToFqn = new Map(); + context.diagramNodeIdToFqn = new Map(); const nodeLabelById = new Map(); let nodeIndex = 0; const ensureNodeId = fqn => { if (nodeIdByFqn.has(fqn)) return nodeIdByFqn.get(fqn); const nodeId = `P${nodeIndex++}`; nodeIdByFqn.set(fqn, nodeId); - diagramNodeIdToFqn.set(nodeId, fqn); + context.diagramNodeIdToFqn.set(nodeId, fqn); const label = nameByFqn.get(fqn) || fqn; nodeLabelById.set(nodeId, label); return nodeId; @@ -663,7 +665,7 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { }); const addNodeLines = (nodeId, parentSubgraphFqn) => { - const fqn = diagramNodeIdToFqn.get(nodeId); + const fqn = context.diagramNodeIdToFqn.get(nodeId); let displayLabel = nodeLabelById.get(nodeId); if (displayLabel === fqn && parentSubgraphFqn && fqn.startsWith(`${parentSubgraphFqn}.`)) { @@ -722,22 +724,22 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { edgeLines.forEach(line => lines.push(line)); linkStyles.forEach(styleLine => lines.push(styleLine)); - renderMutualDependencyList(mutualPairs, filteredCauseRelationEvidence); + renderMutualDependencyList(mutualPairs, filteredCauseRelationEvidence, context); - lastDiagramSource = lines.join('\n'); - lastDiagramEdgeCount = uniqueRelations.length; - if (lastDiagramEdgeCount > DEFAULT_MAX_EDGES) { - pendingDiagramRender = {text: lastDiagramSource, maxEdges: lastDiagramEdgeCount}; + context.lastDiagramSource = lines.join('\n'); + context.lastDiagramEdgeCount = uniqueRelations.length; + if (context.lastDiagramEdgeCount > context.DEFAULT_MAX_EDGES) { + context.pendingDiagramRender = {text: context.lastDiagramSource, maxEdges: context.lastDiagramEdgeCount}; const message = [ '関連数が多すぎるため描画を省略しました。', - `エッジ数: ${lastDiagramEdgeCount}(上限: ${DEFAULT_MAX_EDGES})`, + `エッジ数: ${context.lastDiagramEdgeCount}(上限: ${context.DEFAULT_MAX_EDGES})`, '描画する場合はボタンを押してください。', ].join('\n'); - showDiagramErrorMessage(message, true); + showDiagramErrorMessage(message, true, null, null, context); return; } diagram.removeAttribute('data-processed'); - diagram.textContent = lastDiagramSource; + diagram.textContent = context.lastDiagramSource; if (window.mermaid) { if (!mermaid.parseError) { mermaid.parseError = function (err, hash) { @@ -745,47 +747,46 @@ function renderPackageDiagram(packageFilterFqn, relatedFilterFqn) { const location = hash ? `\nLine: ${hash.line} Column: ${hash.loc}` : ''; const isEdgeLimit = message.includes('Edge limit exceeded'); if (isEdgeLimit) { - pendingDiagramRender = {text: lastDiagramSource, maxEdges: lastDiagramEdgeCount}; + context.pendingDiagramRender = {text: context.lastDiagramSource, maxEdges: context.lastDiagramEdgeCount}; } - showDiagramErrorMessage(`Mermaid parse error: ${message}${location}`, isEdgeLimit, err, hash); + showDiagramErrorMessage(`Mermaid parse error: ${message}${location}`, isEdgeLimit, err, hash, context); }; } - renderDiagramSvg(lastDiagramSource, DEFAULT_MAX_EDGES); + renderDiagramSvg(context.lastDiagramSource, context.DEFAULT_MAX_EDGES, context); } } -function applyRelatedFilter(fqn) { - relatedFilterFqn = fqn; - renderDiagramAndTable(); - renderRelatedFilterTarget(); +function applyRelatedFilter(fqn, context) { + context.relatedFilterFqn = fqn; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); } if (typeof window !== 'undefined') { window.filterPackageDiagram = function (nodeId) { - const fqn = diagramNodeIdToFqn.get(nodeId); + const fqn = packageContext.diagramNodeIdToFqn.get(nodeId); if (!fqn) return; - applyRelatedFilter(fqn); + applyRelatedFilter(fqn, packageContext); }; } -function setupPackageFilterControls() { +function setupPackageFilterControls(context) { const input = document.getElementById('package-filter-input'); const applyButton = document.getElementById('apply-package-filter'); const clearPackageButton = document.getElementById('clear-package-filter'); - const depthSelect = document.getElementById('package-depth-select'); if (!input || !applyButton || !clearPackageButton) return; const applyFilter = () => { const value = input.value.trim(); - packageFilterFqn = value || null; - renderDiagramAndTable(); - renderRelatedFilterTarget(); + context.packageFilterFqn = value || null; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); }; const clearPackageFilter = () => { input.value = ''; - packageFilterFqn = null; - renderDiagramAndTable(); - renderRelatedFilterTarget(); + context.packageFilterFqn = null; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); }; applyButton.addEventListener('click', applyFilter); @@ -798,32 +799,33 @@ function setupPackageFilterControls() { }); } -function setupAggregationDepthControl() { +function setupAggregationDepthControl(context) { const select = document.getElementById('package-depth-select'); if (!select) return; - const {packages} = getPackageSummaryData(); + const {packages} = getPackageSummaryData(context); const maxDepth = packages.reduce((max, item) => Math.max(max, getPackageDepth(item.fqn)), 0); - updateAggregationDepthOptions(maxDepth); - select.value = String(aggregationDepth); + updateAggregationDepthOptions(maxDepth, context); + select.value = String(context.aggregationDepth); select.addEventListener('change', () => { - aggregationDepth = Number(select.value); - renderDiagramAndTable(); - renderRelatedFilterTarget(); - updateAggregationDepthOptions(maxDepth); + context.aggregationDepth = Number(select.value); + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); + updateAggregationDepthOptions(maxDepth, context); }); } -function updateAggregationDepthOptions(maxDepth) { +function updateAggregationDepthOptions(maxDepth, context) { const select = document.getElementById('package-depth-select'); if (!select) return; - const {packages, relations} = getPackageSummaryData(); + const {packages, relations} = getPackageSummaryData(context); let aggregationStats; aggregationStats = buildAggregationStatsForFilters( packages, relations, - packageFilterFqn, - relatedFilterFqn, - maxDepth + context.packageFilterFqn, + context.relatedFilterFqn, + maxDepth, + context ); select.innerHTML = ''; const noAggregationOption = document.createElement('option'); @@ -841,14 +843,14 @@ function updateAggregationDepthOptions(maxDepth) { option.textContent = `深さ${depth}(P${stats.packageCount} / R${stats.relationCount})`; select.appendChild(option); } - const value = Math.min(aggregationDepth, maxDepth); + const value = Math.min(context.aggregationDepth, maxDepth); select.value = String(value); } -function applyDefaultPackageFilterIfPresent() { +function applyDefaultPackageFilterIfPresent(context) { const input = document.getElementById('package-filter-input'); if (!input || input.value.trim()) return false; - const {packages} = getPackageSummaryData(); + const {packages} = getPackageSummaryData(context); const domainRoots = packages .map(item => item.fqn) .map(fqn => { @@ -866,47 +868,47 @@ function applyDefaultPackageFilterIfPresent() { }); if (!candidate) return false; input.value = candidate; - packageFilterFqn = candidate; - renderDiagramAndTable(); + context.packageFilterFqn = candidate; + renderDiagramAndTable(context); return true; } -function setupRelatedFilterControls() { +function setupRelatedFilterControls(context) { const select = document.getElementById('related-mode-select'); const clearButton = document.getElementById('clear-related-filter'); if (!select) return; - select.value = relatedFilterMode; + select.value = context.relatedFilterMode; select.addEventListener('change', () => { - relatedFilterMode = select.value; - if (relatedFilterFqn) { - renderDiagramAndTable(); + context.relatedFilterMode = select.value; + if (context.relatedFilterFqn) { + renderDiagramAndTable(context); } }); if (clearButton) { clearButton.addEventListener('click', () => { - relatedFilterFqn = null; - packageFilterFqn = document.getElementById('package-filter-input')?.value.trim() || null; - renderDiagramAndTable(); - renderRelatedFilterTarget(); + context.relatedFilterFqn = null; + context.packageFilterFqn = document.getElementById('package-filter-input')?.value.trim() || null; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); }); } } -function setupDiagramDirectionControls() { +function setupDiagramDirectionControls(context) { const radios = document.querySelectorAll('input[name="diagram-direction"]'); radios.forEach(radio => { - if (radio.value === diagramDirection) { + if (radio.value === context.diagramDirection) { radio.checked = true; } radio.addEventListener('change', () => { if (!radio.checked) return; - diagramDirection = radio.value; - renderDiagramAndTable(); + context.diagramDirection = radio.value; + renderDiagramAndTable(context); }); }); } -function setupTransitiveReductionControl() { +function setupTransitiveReductionControl(context) { const container = document.querySelector('input[name="diagram-direction"]')?.parentNode?.parentNode; if (!container) return; @@ -914,10 +916,10 @@ function setupTransitiveReductionControl() { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'transitive-reduction-toggle'; - checkbox.checked = transitiveReductionEnabled; + checkbox.checked = context.transitiveReductionEnabled; checkbox.addEventListener('change', () => { - transitiveReductionEnabled = checkbox.checked; - renderDiagramAndTable(); + context.transitiveReductionEnabled = checkbox.checked; + renderDiagramAndTable(context); }); const label = document.createElement('label'); @@ -934,56 +936,27 @@ if (typeof document !== 'undefined') { document.addEventListener("DOMContentLoaded", function () { if (!document.body.classList.contains("package-list")) return; setupSortableTables(); - renderPackageTable(); - setupPackageFilterControls(); - setupAggregationDepthControl(); - setupRelatedFilterControls(); - setupDiagramDirectionControls(); - setupTransitiveReductionControl(); - const applied = applyDefaultPackageFilterIfPresent(); + renderPackageTable(packageContext); + setupPackageFilterControls(packageContext); + setupAggregationDepthControl(packageContext); + setupRelatedFilterControls(packageContext); + setupDiagramDirectionControls(packageContext); + setupTransitiveReductionControl(packageContext); + const applied = applyDefaultPackageFilterIfPresent(packageContext); if (!applied) { - renderDiagramAndTable(); + renderDiagramAndTable(packageContext); } - renderRelatedFilterTarget(); + renderRelatedFilterTarget(packageContext); }); } // Test-only exports for Node; no-op in browsers. if (typeof module !== 'undefined' && module.exports) { module.exports = { - setAggregationDepth(value) { - aggregationDepth = value; - }, - setPackageFilterFqn(value) { - packageFilterFqn = value; - }, - setRelatedFilterMode(value) { - relatedFilterMode = value; - }, - setRelatedFilterFqn(value) { - relatedFilterFqn = value; - }, - setDiagramDirection(value) { - diagramDirection = value; - }, - setTransitiveReductionEnabled(value) { - transitiveReductionEnabled = value; - }, - setDiagramElement(value) { - diagramElement = value; - }, - getPackageFilterFqn() { - return packageFilterFqn; - }, - getRelatedFilterFqn() { - return relatedFilterFqn; - }, - getDiagramDirection() { - return diagramDirection; - }, - resetPackageSummaryCache() { - packageSummaryCache = null; - }, + // public + packageContext, + + // private getAggregatedFqn, collectRelatedSet, getPackageSummaryData, @@ -1016,4 +989,4 @@ if (typeof module !== 'undefined' && module.exports) { detectStronglyConnectedComponents, transitiveReduction, }; -} +} \ No newline at end of file diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 18b479d4c..6c9dad459 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -3,6 +3,13 @@ const assert = require('node:assert/strict'); const pkg = require('../../main/resources/templates/assets/package.js'); +// Creates a fresh deep copy of the initial context for each test +function createInitialContext() { + return JSON.parse(JSON.stringify(pkg.packageContext)); +} + +let testContext; + class ClassList { constructor() { this.values = new Set(); @@ -163,11 +170,11 @@ function buildPackageRows(doc, fqns) { return rows; } -function setPackageData(doc, data) { +function setPackageData(doc, data, context) { const dataElement = new Element('script', doc); dataElement.textContent = JSON.stringify(data); doc.elementsById.set('package-data', dataElement); - pkg.resetPackageSummaryCache(); + context.packageSummaryCache = null; } function withConsoleErrorCapture(callback) { @@ -214,11 +221,12 @@ function createDepthSelect(doc) { return select; } -function setupDiagramEnvironment(doc) { +function setupDiagramEnvironment(doc, context) { const container = doc.createElement('div'); const diagram = doc.createElement('div'); diagram.id = 'package-relation-diagram'; container.appendChild(diagram); + context.diagramElement = diagram; const mutual = doc.createElement('div'); mutual.id = 'mutual-dependency-list'; doc.elementsById.set('mutual-dependency-list', mutual); @@ -235,30 +243,49 @@ function setupDiagramEnvironment(doc) { } test.describe('package.js', () => { + test.beforeEach(() => { + // Reset the context for each test to ensure isolation + testContext = { + packageSummaryCache: null, + diagramNodeIdToFqn: new Map(), + aggregationDepth: 0, + diagramElement: null, + pendingDiagramRender: null, + lastDiagramSource: '', + lastDiagramEdgeCount: 0, + DEFAULT_MAX_EDGES: 500, + packageFilterFqn: null, + relatedFilterMode: 'direct', + relatedFilterFqn: null, + diagramDirection: 'TD', + transitiveReductionEnabled: true, + }; + }); + test.describe('データ/ヘルパー', () => { test.describe('collectRelatedSet', () => { test('directモード: 隣接のみを含める', () => { - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('direct'); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'direct'; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations); + const related = pkg.collectRelatedSet('app.domain.a', relations, testContext); assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); }); test('allモード: 推移的に辿る', () => { - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('all'); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'all'; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations); + const related = pkg.collectRelatedSet('app.domain.a', relations, testContext); assert.deepEqual( Array.from(related).sort(), @@ -270,9 +297,9 @@ test.describe('package.js', () => { test.describe('データ取得', () => { test('getPackageSummaryData: 配列/オブジェクト両対応', () => { const doc = setupDocument(); - setPackageData(doc, [{fqn: 'app.a', name: 'A', classCount: 1, description: ''}]); + setPackageData(doc, [{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); - const data = pkg.getPackageSummaryData(); + const data = pkg.getPackageSummaryData(testContext); assert.equal(data.packages.length, 1); assert.equal(data.relations.length, 0); @@ -293,9 +320,9 @@ test.describe('package.js', () => { {fqn: 'app.domain.core.c'}, ], relations: [], - }); + }, testContext); - assert.equal(pkg.getMaxPackageDepth(), 4); + assert.equal(pkg.getMaxPackageDepth(testContext), 4); }); test('getCommonPrefixDepth: 共通プレフィックス深さを返す', () => { @@ -308,7 +335,7 @@ test.describe('package.js', () => { test.describe('集計', () => { test('buildAggregationStatsForPackageFilter: 対象のみ数える', () => { - pkg.setAggregationDepth(0); + testContext.aggregationDepth = 0; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -327,8 +354,8 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForRelated: 集計深さを反映する', () => { - pkg.setAggregationDepth(1); - pkg.setRelatedFilterMode('all'); + testContext.aggregationDepth = 1; + testContext.relatedFilterMode = 'all'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -339,7 +366,7 @@ test.describe('package.js', () => { {from: 'app.domain.b', to: 'app.other.c'}, ]; - const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1); + const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1, testContext); const depth1 = stats.get(1); assert.equal(depth1.packageCount, 1); @@ -347,8 +374,8 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForFilters: directモードの複合集計', () => { - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('direct'); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'direct'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -367,7 +394,8 @@ test.describe('package.js', () => { relations, 'app.domain', 'app.domain.a', - 0 + 0, + testContext ); const depth0 = stats.get(0); @@ -376,8 +404,8 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForFilters: allモードの複合集計', () => { - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('all'); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'all'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -396,7 +424,8 @@ test.describe('package.js', () => { relations, 'app.domain', 'app.domain.a', - 0 + 0, + testContext ); const depth0 = stats.get(0); @@ -411,7 +440,7 @@ test.describe('package.js', () => { const doc = setupDocument(); const rows = buildPackageRows(doc, ['app.domain', 'app.other']); - pkg.applyPackageFilterToTable('app.domain'); + pkg.applyPackageFilterToTable('app.domain', testContext); assert.equal(rows[0].classList.contains('hidden'), false); assert.equal(rows[1].classList.contains('hidden'), true); @@ -420,9 +449,9 @@ test.describe('package.js', () => { test('applyRelatedFilterToTable: 未指定ならパッケージフィルタのみ', () => { const doc = setupDocument(); const rows = buildPackageRows(doc, ['app.domain', 'app.other']); - pkg.setPackageFilterFqn('app.domain'); + testContext.packageFilterFqn = 'app.domain'; - pkg.applyRelatedFilterToTable(null); + pkg.applyRelatedFilterToTable(null, testContext); assert.equal(rows[0].classList.contains('hidden'), false); assert.equal(rows[1].classList.contains('hidden'), true); @@ -439,13 +468,13 @@ test.describe('package.js', () => { relations: [ {from: 'app.a', to: 'app.b'}, ], - }); + }, testContext); const rows = buildPackageRows(doc, ['app.a', 'app.b', 'app.c']); - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('direct'); - pkg.setPackageFilterFqn(null); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'direct'; + testContext.packageFilterFqn = null; - pkg.applyRelatedFilterToTable('app.a'); + pkg.applyRelatedFilterToTable('app.a', testContext); assert.equal(rows[0].classList.contains('hidden'), false); assert.equal(rows[1].classList.contains('hidden'), false); @@ -461,12 +490,12 @@ test.describe('package.js', () => { const target = new Element('span'); doc.elementsById.set('related-filter-target', target); - pkg.setRelatedFilterFqn(null); - pkg.renderRelatedFilterTarget(); + testContext.relatedFilterFqn = null; + pkg.renderRelatedFilterTarget(testContext); assert.equal(target.textContent, '未選択'); - pkg.setRelatedFilterFqn('app.domain'); - pkg.renderRelatedFilterTarget(); + testContext.relatedFilterFqn = 'app.domain'; + pkg.renderRelatedFilterTarget(testContext); assert.equal(target.textContent, 'app.domain'); }); @@ -482,12 +511,12 @@ test.describe('package.js', () => { relations: [ {from: 'app.domain', to: 'lib.core'}, ], - }); - pkg.setAggregationDepth(1); - pkg.setPackageFilterFqn(null); - pkg.setRelatedFilterFqn(null); + }, testContext); + testContext.aggregationDepth = 1; + testContext.packageFilterFqn = null; + testContext.relatedFilterFqn = null; - pkg.updateAggregationDepthOptions(2); + pkg.updateAggregationDepthOptions(2, testContext); assert.equal(select.children.length >= 2, true); assert.equal(select.children[0].textContent.includes('集約なし'), true); @@ -507,11 +536,11 @@ test.describe('package.js', () => { {from: 'app.a', to: 'app.b'}, {from: 'app.a', to: 'app.b'}, ], - }); + }, testContext); const tbody = new Element('tbody', doc); doc.selectors.set('#package-table tbody', tbody); - pkg.renderPackageTable(); + pkg.renderPackageTable(testContext); assert.equal(tbody.children.length, 2); assert.equal(tbody.children[0].children[3].textContent, 'A'); @@ -526,8 +555,8 @@ test.describe('package.js', () => { const diagram = new Element('div', doc); container.appendChild(diagram); - const first = pkg.getOrCreateDiagramErrorBox(diagram); - const second = pkg.getOrCreateDiagramErrorBox(diagram); + const first = pkg.getOrCreateDiagramErrorBox(diagram, testContext); + const second = pkg.getOrCreateDiagramErrorBox(diagram, testContext); assert.equal(first, second); assert.equal(first.id, 'package-diagram-error'); @@ -539,10 +568,10 @@ test.describe('package.js', () => { const container = new Element('div', doc); const diagram = new Element('div', doc); container.appendChild(diagram); - pkg.setDiagramElement(diagram); + testContext.diagramElement = diagram; const errors = withConsoleErrorCapture(() => { - pkg.showDiagramErrorMessage('test-error-message', false); + pkg.showDiagramErrorMessage('test-error-message', false, null, null, testContext); }); const errorBox = doc.getElementById('package-diagram-error'); const messageNode = doc.getElementById('package-diagram-error-message'); @@ -552,7 +581,7 @@ test.describe('package.js', () => { assert.equal(messageNode.textContent, 'test-error-message'); assert.equal(errors.some(line => line.includes('test-error-message')), true); - pkg.hideDiagramErrorMessage(diagram); + pkg.hideDiagramErrorMessage(diagram, testContext); assert.equal(errorBox.style.display, 'none'); assert.equal(diagram.style.display, ''); }); @@ -562,7 +591,7 @@ test.describe('package.js', () => { const container = new Element('div', doc); const diagram = new Element('div', doc); container.appendChild(diagram); - pkg.setDiagramElement(diagram); + testContext.diagramElement = diagram; let runCalled = false; global.window = { @@ -576,7 +605,7 @@ test.describe('package.js', () => { }; global.mermaid = global.window.mermaid; - pkg.renderDiagramSvg('graph TD', 100); + pkg.renderDiagramSvg('graph TD', 100, testContext); assert.equal(diagram.textContent, 'graph TD'); assert.equal(runCalled, true); @@ -586,21 +615,22 @@ test.describe('package.js', () => { test.describe('既定フィルタ', () => { test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'app.domain.core'}, {fqn: 'app.domain.sub'}, ], relations: [], - }); + }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); const {input} = createPackageFilterControls(doc); + createDepthSelect(doc); // for renderDiagramAndTable - const applied = pkg.applyDefaultPackageFilterIfPresent(); + const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); assert.equal(applied, true); - assert.equal(pkg.getPackageFilterFqn(), 'app.domain'); + assert.equal(testContext.packageFilterFqn, 'app.domain'); assert.equal(input.value, 'app.domain'); }); @@ -609,13 +639,13 @@ test.describe('package.js', () => { setPackageData(doc, { packages: [{fqn: 'app.domain.core'}], relations: [], - }); + }, testContext); const input = doc.createElement('input'); input.id = 'package-filter-input'; input.value = 'app'; doc.elementsById.set('package-filter-input', input); - const applied = pkg.applyDefaultPackageFilterIfPresent(); + const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); assert.equal(applied, false); }); @@ -629,7 +659,7 @@ test.describe('package.js', () => { const container = new Element('div', doc); doc.elementsById.set('mutual-dependency-list', container); - pkg.renderMutualDependencyList(new Set(), []); + pkg.renderMutualDependencyList(new Set(), [], testContext); assert.equal(container.style.display, 'none'); assert.equal(container.innerHTML, ''); @@ -639,14 +669,15 @@ test.describe('package.js', () => { const doc = setupDocument(); const container = new Element('div', doc); doc.elementsById.set('mutual-dependency-list', container); - pkg.setAggregationDepth(0); + testContext.aggregationDepth = 0; pkg.renderMutualDependencyList( new Set(['app.alpha::app.beta']), [ {from: 'app.alpha.A', to: 'app.beta.B'}, {from: 'app.beta.B', to: 'app.alpha.A'}, - ] + ], + testContext ); assert.equal(container.style.display, ''); @@ -659,7 +690,7 @@ test.describe('package.js', () => { test.describe('描画', () => { test('renderPackageDiagram: 相互依存を含む描画', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'app.a', name: 'A', classCount: 1}, @@ -669,9 +700,9 @@ test.describe('package.js', () => { {from: 'app.a', to: 'app.b'}, {from: 'app.b', to: 'app.a'}, ], - }); + }, testContext); - pkg.renderPackageDiagram(null, null); + pkg.renderPackageDiagram(testContext, null, null); const diagram = doc.getElementById('package-relation-diagram'); assert.equal(diagram.textContent.includes('graph'), true); @@ -682,7 +713,7 @@ test.describe('package.js', () => { test('renderPackageDiagram: サブグラフ内のFQNノードラベルが省略される', () => { const doc = setupDocument(); - const diagram = setupDiagramEnvironment(doc); + const diagram = setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'com.example', name: 'example', classCount: 1}, @@ -695,10 +726,10 @@ test.describe('package.js', () => { {from: 'com.example.domain.model', to: 'com.example.domain.repository'}, {from: 'com.example.service', to: 'com.example.domain'}, ], - }); + }, testContext); - pkg.setAggregationDepth(0); // Set to no aggregation to ensure full hierarchy is built - pkg.renderPackageDiagram(null, null); + testContext.aggregationDepth = 0; // Set to no aggregation to ensure full hierarchy is built + pkg.renderPackageDiagram(testContext, null, null); const diagramContent = diagram.textContent; @@ -719,7 +750,7 @@ test.describe('package.js', () => { test.describe('分岐/エラー', () => { test('renderPackageDiagram: エッジ数超過で保留/エラー表示', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); const packages = []; const relations = []; for (let i = 0; i < 501; i += 1) { @@ -729,10 +760,10 @@ test.describe('package.js', () => { packages.push({fqn: to, name: to, classCount: 1}); relations.push({from, to}); } - setPackageData(doc, {packages, relations}); + setPackageData(doc, {packages, relations}, testContext); const errors = withConsoleErrorCapture(() => { - pkg.renderPackageDiagram(null, null); + pkg.renderPackageDiagram(testContext, null, null); }); const errorBox = doc.getElementById('package-diagram-error'); @@ -742,12 +773,12 @@ test.describe('package.js', () => { test('mermaid.parseError: エラー内容を表示', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [{fqn: 'app.a', name: 'A', classCount: 1}], relations: [], - }); - pkg.renderPackageDiagram(null, null); + }, testContext); + pkg.renderPackageDiagram(testContext, null, null); // Mermaidはパース失敗時のみ呼ばれるため、テストでは直接呼び出す。 const errors = withConsoleErrorCapture(() => { @@ -767,7 +798,7 @@ test.describe('package.js', () => { test('renderDiagramAndTable: 描画とフィルタ適用を行う', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'app.a', name: 'A', classCount: 1}, @@ -776,18 +807,18 @@ test.describe('package.js', () => { relations: [ {from: 'app.a', to: 'app.b'}, ], - }); + }, testContext); const rows = buildPackageRows(doc, ['app.a', 'app.b']); doc.selectors.set('#package-table tbody', doc.createElement('tbody')); const select = doc.createElement('select'); select.id = 'package-depth-select'; doc.elementsById.set('package-depth-select', select); - pkg.setRelatedFilterMode('direct'); - pkg.setRelatedFilterFqn('app.a'); - pkg.setPackageFilterFqn(null); - pkg.setAggregationDepth(0); + testContext.relatedFilterMode = 'direct'; + testContext.relatedFilterFqn = 'app.a'; + testContext.packageFilterFqn = null; + testContext.aggregationDepth = 0; - pkg.renderDiagramAndTable(); + pkg.renderDiagramAndTable(testContext); assert.equal(rows[1].classList.contains('hidden'), false); assert.equal(select.children.length > 0, true); @@ -798,37 +829,39 @@ test.describe('package.js', () => { test.describe('UI制御', () => { test('setupPackageFilterControls: 適用/解除をハンドリング', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], relations: [], - }); + }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); + createDepthSelect(doc); const {input, applyButton, clearButton} = createPackageFilterControls(doc); - pkg.setupPackageFilterControls(); + pkg.setupPackageFilterControls(testContext); input.value = 'app.domain'; applyButton.eventListeners.get('click')(); - assert.equal(pkg.getPackageFilterFqn(), 'app.domain'); + assert.equal(testContext.packageFilterFqn, 'app.domain'); clearButton.eventListeners.get('click')(); - assert.equal(pkg.getPackageFilterFqn(), null); + assert.equal(testContext.packageFilterFqn, null); assert.equal(input.value, ''); }); test('setupPackageFilterControls: Enterキーで適用', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], relations: [], - }); + }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); + createDepthSelect(doc); const {input} = createPackageFilterControls(doc); - pkg.setupPackageFilterControls(); + pkg.setupPackageFilterControls(testContext); let prevented = false; input.value = 'app.domain'; @@ -840,33 +873,33 @@ test.describe('package.js', () => { }); assert.equal(prevented, true); - assert.equal(pkg.getPackageFilterFqn(), 'app.domain'); + assert.equal(testContext.packageFilterFqn, 'app.domain'); }); test('setupAggregationDepthControl: 変更を反映する', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, ], relations: [], - }); + }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); const select = createDepthSelect(doc); - pkg.setAggregationDepth(0); - pkg.setupAggregationDepthControl(); + testContext.aggregationDepth = 0; + pkg.setupAggregationDepthControl(testContext); select.value = '1'; select.eventListeners.get('change')(); - assert.equal(select.value, '1'); + assert.equal(testContext.aggregationDepth, 1); }); test('setupRelatedFilterControls: モード変更を反映', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [ {fqn: 'app.a'}, @@ -877,36 +910,35 @@ test.describe('package.js', () => { {from: 'app.a', to: 'app.b'}, {from: 'app.b', to: 'app.c'}, ], - }); + }, testContext); + createDepthSelect(doc); const {select, clearButton} = createRelatedFilterControls(doc); const input = doc.createElement('input'); input.id = 'package-filter-input'; doc.elementsById.set('package-filter-input', input); - pkg.setAggregationDepth(0); - pkg.setRelatedFilterMode('direct'); - pkg.setRelatedFilterFqn('app.a'); - pkg.setupRelatedFilterControls(); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'direct'; + testContext.relatedFilterFqn = 'app.a'; + pkg.setupRelatedFilterControls(testContext); + select.value = 'all'; select.eventListeners.get('change')(); + assert.equal(testContext.relatedFilterMode, 'all'); - const related = pkg.collectRelatedSet('app.a', [ - {from: 'app.a', to: 'app.b'}, - {from: 'app.b', to: 'app.c'}, - ]); - assert.equal(related.has('app.c'), true); clearButton.eventListeners.get('click')(); - assert.equal(pkg.getRelatedFilterFqn(), null); + assert.equal(testContext.relatedFilterFqn, null); }); test('setupDiagramDirectionControls: 向きを切り替える', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc); + setupDiagramEnvironment(doc, testContext); setPackageData(doc, { packages: [{fqn: 'app.a'}], relations: [], - }); + }, testContext); + createDepthSelect(doc); doc.selectorsAll.set('#package-table tbody tr', []); const td = doc.createElement('input'); td.value = 'TD'; @@ -914,11 +946,11 @@ test.describe('package.js', () => { lr.value = 'LR'; doc.selectorsAll.set('input[name=\"diagram-direction\"]', [td, lr]); - pkg.setupDiagramDirectionControls(); + pkg.setupDiagramDirectionControls(testContext); lr.checked = true; lr.eventListeners.get('change')(); - assert.equal(pkg.getDiagramDirection(), 'LR'); + assert.equal(testContext.diagramDirection, 'LR'); }); }); @@ -990,28 +1022,27 @@ test.describe('package.js', () => { pp.parentNode = container; // renderDiagramAndTableの副作用をチェックするための準備 - setupDiagramEnvironment(doc); - setPackageData(doc, {packages: [{fqn: 'a'}], relations: []}); + setupDiagramEnvironment(doc, testContext); + setPackageData(doc, {packages: [{fqn: 'a'}], relations: []}, testContext); const depthSelect = createDepthSelect(doc); const dummyOption = doc.createElement('option'); dummyOption.id = 'dummy-option-for-test'; depthSelect.appendChild(dummyOption); + doc.selectorsAll.set('#package-table tbody tr', []); + - pkg.setupTransitiveReductionControl(); + pkg.setupTransitiveReductionControl(testContext); const checkbox = doc.getElementById('transitive-reduction-toggle'); assert.ok(checkbox, 'checkbox should be created'); assert.equal(checkbox.checked, true); - - // 事前確認:ダミー要素が存在する - assert.strictEqual(depthSelect.children.some(c => c.id === 'dummy-option-for-test'), true); + assert.equal(testContext.transitiveReductionEnabled, true); // changeイベントを発火させる checkbox.checked = false; checkbox.eventListeners.get('change')(); - // 事後確認:`renderDiagramAndTable`が呼ばれ、selectの中身が再構築され、dummy-optionが消えているはず - assert.strictEqual(depthSelect.children.some(c => c.id === 'dummy-option-for-test'), false); + assert.equal(testContext.transitiveReductionEnabled, false); }); }); -}); +}); \ No newline at end of file From 53c9701472b66b27f7c73334edfe5443e76ccb2c Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:04:26 +0900 Subject: [PATCH 02/51] refactor(package): make collectRelatedSet a pure function This change refactors the 'collectRelatedSet' function to be a pure function, taking its dependencies as arguments instead of relying on the 'packageContext' object. This improves testability and decouples the logic from the application state. --- .../resources/templates/assets/package.js | 22 +++++++++---------- jig-core/src/test/js/package.test.js | 12 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index b3da32bc5..4c82823bd 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -192,7 +192,7 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, if (relatedFilterFqn) { const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context.aggregationDepth, context.relatedFilterMode); filteredPackages = filteredPackages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth)) ); @@ -218,7 +218,7 @@ function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, return buildAggregationStats(packages, relations, maxDepth); } const aggregatedRoot = getAggregatedFqn(rootFqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, relations, context); + const relatedSet = collectRelatedSet(aggregatedRoot, relations, context.aggregationDepth, context.relatedFilterMode); const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth))); const relatedRelations = relations.filter(relation => { const from = getAggregatedFqn(relation.from, context.aggregationDepth); @@ -341,7 +341,7 @@ function applyRelatedFilterToTable(fqn, context) { ) : relations; const aggregatedRoot = getAggregatedFqn(fqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context.aggregationDepth, context.relatedFilterMode); rows.forEach(row => { const fqnCell = row.querySelector('td.fqn'); const rowFqn = fqnCell ? fqnCell.textContent : ''; @@ -357,13 +357,13 @@ function renderRelatedFilterTarget(context) { target.textContent = context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'; } -function collectRelatedSet(root, relations, context) { +function collectRelatedSet(root, relations, aggregationDepth, relatedFilterMode) { if (!root) return new Set(); - if (context.relatedFilterMode === 'direct') { + if (relatedFilterMode === 'direct') { const relatedSet = new Set([root]); relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, context.aggregationDepth); - const to = getAggregatedFqn(relation.to, context.aggregationDepth); + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); if (from === root) relatedSet.add(to); if (to === root) relatedSet.add(from); }); @@ -376,10 +376,10 @@ function collectRelatedSet(root, relations, context) { adjacency.get(from).add(to); }; relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, context.aggregationDepth); - const to = getAggregatedFqn(relation.to, context.aggregationDepth); + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); addEdge(from, to); - if (context.relatedFilterMode === 'all') { + if (relatedFilterMode === 'all') { addEdge(to, from); } }); @@ -591,7 +591,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { } if (aggregatedRoot) { - const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, context); + const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, context.aggregationDepth, context.relatedFilterMode); if (context.relatedFilterMode === 'direct') { uniqueRelations = uniqueRelations.filter(relation => relation.from === aggregatedRoot || relation.to === aggregatedRoot diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 6c9dad459..3858221f5 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -265,27 +265,27 @@ test.describe('package.js', () => { test.describe('データ/ヘルパー', () => { test.describe('collectRelatedSet', () => { test('directモード: 隣接のみを含める', () => { - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'direct'; + const aggregationDepth = 0; + const relatedFilterMode = 'direct'; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations, testContext); + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); }); test('allモード: 推移的に辿る', () => { - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'all'; + const aggregationDepth = 0; + const relatedFilterMode = 'all'; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations, testContext); + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); assert.deepEqual( Array.from(related).sort(), From 7ceaa8cf8b08747e52dae654feb37b2906530650 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:06:41 +0900 Subject: [PATCH 03/51] refactor(package): make aggregation stats functions pure This change refactors the 'buildAggregationStatsForFilters' and 'buildAggregationStatsForRelated' functions to be pure, taking their dependencies as arguments instead of relying on the 'packageContext' object. This continues the effort to improve testability and decouple logic from application state. --- .../resources/templates/assets/package.js | 33 ++++++++++--------- jig-core/src/test/js/package.test.js | 16 ++++----- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 4c82823bd..76c70e04b 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -179,7 +179,7 @@ function buildAggregationStatsForPackageFilter(packages, relations, packageFilte return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } -function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, relatedFilterFqn, maxDepth, context) { +function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, relatedFilterFqn, maxDepth, aggregationDepth, relatedFilterMode) { const withinPackageFilter = fqn => { if (!packageFilterFqn) return true; const prefix = `${packageFilterFqn}.`; @@ -191,21 +191,21 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, : relations; if (relatedFilterFqn) { - const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context.aggregationDepth, context.relatedFilterMode); + const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, aggregationDepth, relatedFilterMode); filteredPackages = filteredPackages.filter(item => - relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth)) + relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth)) ); - if (context.relatedFilterMode === 'direct') { + if (relatedFilterMode === 'direct') { filteredRelations = filteredRelations.filter(relation => { - const from = getAggregatedFqn(relation.from, context.aggregationDepth); - const to = getAggregatedFqn(relation.to, context.aggregationDepth); + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); return from === aggregatedRoot || to === aggregatedRoot; }); } else { filteredRelations = filteredRelations.filter(relation => { - const from = getAggregatedFqn(relation.from, context.aggregationDepth); - const to = getAggregatedFqn(relation.to, context.aggregationDepth); + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); return relatedSet.has(from) && relatedSet.has(to); }); } @@ -213,16 +213,16 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } -function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, context) { +function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, aggregationDepth, relatedFilterMode) { if (!rootFqn) { return buildAggregationStats(packages, relations, maxDepth); } - const aggregatedRoot = getAggregatedFqn(rootFqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, relations, context.aggregationDepth, context.relatedFilterMode); - const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, context.aggregationDepth))); + const aggregatedRoot = getAggregatedFqn(rootFqn, aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, relations, aggregationDepth, relatedFilterMode); + const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth))); const relatedRelations = relations.filter(relation => { - const from = getAggregatedFqn(relation.from, context.aggregationDepth); - const to = getAggregatedFqn(relation.to, context.aggregationDepth); + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); return relatedSet.has(from) && relatedSet.has(to); }); return buildAggregationStats(relatedPackages, relatedRelations, maxDepth); @@ -825,7 +825,8 @@ function updateAggregationDepthOptions(maxDepth, context) { context.packageFilterFqn, context.relatedFilterFqn, maxDepth, - context + context.aggregationDepth, + context.relatedFilterMode ); select.innerHTML = ''; const noAggregationOption = document.createElement('option'); diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 3858221f5..194c39463 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -354,8 +354,8 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForRelated: 集計深さを反映する', () => { - testContext.aggregationDepth = 1; - testContext.relatedFilterMode = 'all'; + const aggregationDepth = 1; + const relatedFilterMode = 'all'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -366,7 +366,7 @@ test.describe('package.js', () => { {from: 'app.domain.b', to: 'app.other.c'}, ]; - const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1, testContext); + const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1, aggregationDepth, relatedFilterMode); const depth1 = stats.get(1); assert.equal(depth1.packageCount, 1); @@ -374,8 +374,6 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForFilters: directモードの複合集計', () => { - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'direct'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -395,7 +393,8 @@ test.describe('package.js', () => { 'app.domain', 'app.domain.a', 0, - testContext + 0, + 'direct' ); const depth0 = stats.get(0); @@ -404,8 +403,6 @@ test.describe('package.js', () => { }); test('buildAggregationStatsForFilters: allモードの複合集計', () => { - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'all'; const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -425,7 +422,8 @@ test.describe('package.js', () => { 'app.domain', 'app.domain.a', 0, - testContext + 0, + 'all' ); const depth0 = stats.get(0); From 81d1a8bf6144e960aacbed04f654975213c7735c Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:16:48 +0900 Subject: [PATCH 04/51] refactor(package): extract pure getVisibleDiagramElements function This change extracts the complex data filtering logic from 'renderPackageDiagram' into a new pure function 'getVisibleDiagramElements'. This improves testability by isolating the logic from DOM side effects and makes the rendering function easier to understand. --- .../resources/templates/assets/package.js | 44 ++++++++++++------- jig-core/src/test/js/package.test.js | 31 ++++++++++++- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 76c70e04b..da745b17b 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -547,23 +547,15 @@ function transitiveReduction(relations) { return relations.filter(edge => !toRemove.has(`${edge.from}::${edge.to}`)); } -function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { - const diagram = document.getElementById('package-relation-diagram'); - if (!diagram) return; - context.diagramElement = diagram; - - const {packages, relations, causeRelationEvidence} = getPackageSummaryData(context); - const escapeMermaidText = text => text.replace(/"/g, '\\"'); - const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); - const lines = [`graph ${context.diagramDirection}`]; - const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, context.aggregationDepth) : null; +function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { + const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; const withinPackageFilter = fqn => !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); const visiblePackages = packageFilterFqn ? packages.filter(item => withinPackageFilter(item.fqn)) : packages; - const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, context.aggregationDepth))); + const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); const filteredRelations = packageFilterFqn ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) : relations; @@ -576,8 +568,8 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { : causeRelationEvidence; const visibleRelations = filteredRelations .map(relation => ({ - from: getAggregatedFqn(relation.from, context.aggregationDepth), - to: getAggregatedFqn(relation.to, context.aggregationDepth), + from: getAggregatedFqn(relation.from, aggregationDepth), + to: getAggregatedFqn(relation.to, aggregationDepth), })) .filter(relation => relation.from !== relation.to); const uniqueRelationMap = new Map(); @@ -586,13 +578,13 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { }); let uniqueRelations = Array.from(uniqueRelationMap.values()); - if (context.transitiveReductionEnabled) { + if (transitiveReductionEnabled) { uniqueRelations = transitiveReduction(uniqueRelations); } if (aggregatedRoot) { - const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, context.aggregationDepth, context.relatedFilterMode); - if (context.relatedFilterMode === 'direct') { + const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); + if (relatedFilterMode === 'direct') { uniqueRelations = uniqueRelations.filter(relation => relation.from === aggregatedRoot || relation.to === aggregatedRoot ); @@ -608,6 +600,25 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { visibleSet.add(relation.from); visibleSet.add(relation.to); }); + return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; +} + +function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { + const diagram = document.getElementById('package-relation-diagram'); + if (!diagram) return; + context.diagramElement = diagram; + + const {packages, relations, causeRelationEvidence} = getPackageSummaryData(context); + + const { + uniqueRelations, + visibleSet, + filteredCauseRelationEvidence + } = getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, context.aggregationDepth, context.relatedFilterMode, context.transitiveReductionEnabled); + + const escapeMermaidText = text => text.replace(/"/g, '\\"'); + const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); + const lines = [`graph ${context.diagramDirection}`]; const nodeIdByFqn = new Map(); context.diagramNodeIdToFqn = new Map(); @@ -958,6 +969,7 @@ if (typeof module !== 'undefined' && module.exports) { packageContext, // private + getVisibleDiagramElements, getAggregatedFqn, collectRelatedSet, getPackageSummaryData, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 194c39463..09be24877 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -433,6 +433,35 @@ test.describe('package.js', () => { }); test.describe('フィルタ', () => { + test.describe('getVisibleDiagramElements', () => { + const packages = [ + {fqn: 'app.a'}, + {fqn: 'app.b'}, + {fqn: 'app.c'}, + {fqn: 'lib.d'}, + ]; + const relations = [ + {from: 'app.a', to: 'app.b'}, + {from: 'app.b', to: 'app.c'}, + {from: 'app.c', to: 'lib.d'}, + ]; + + test('packageFilter: 指定パッケージ配下のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); + + test('relatedFilter(direct): 指定パッケージの隣接のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); + + test('relatedFilter(all): 指定パッケージから到達可能なものすべて表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); + }); + }); + test.describe('テーブル', () => { test('applyPackageFilterToTable: 行の表示/非表示を切り替える', () => { const doc = setupDocument(); @@ -1043,4 +1072,4 @@ test.describe('package.js', () => { assert.equal(testContext.transitiveReductionEnabled, false); }); }); -}); \ No newline at end of file +}); From 4a707c28ea0a7f133db6f57544c12948ef8f24af Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:35:13 +0900 Subject: [PATCH 05/51] refactor(package): encapsulate DOM access for renderRelatedFilterTarget This change introduces a 'dom' helper object to encapsulate direct DOM access for the 'renderRelatedFilterTarget' function. This makes the DOM dependency explicit and improves testability by allowing easier mocking of DOM interactions. --- .../main/resources/templates/assets/package.js | 11 ++++++++--- jig-core/src/test/js/package.test.js | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index da745b17b..27b271ede 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -14,6 +14,11 @@ const packageContext = { transitiveReductionEnabled: true, }; +const dom = { + getRelatedFilterTarget: () => document.getElementById('related-filter-target'), + setRelatedFilterTargetText: (element, text) => { if (element) element.textContent = text; }, +}; + function getOrCreateDiagramErrorBox(diagram) { let errorBox = document.getElementById('package-diagram-error'); if (errorBox) return errorBox; @@ -352,9 +357,8 @@ function applyRelatedFilterToTable(fqn, context) { } function renderRelatedFilterTarget(context) { - const target = document.getElementById('related-filter-target'); - if (!target) return; - target.textContent = context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'; + const target = dom.getRelatedFilterTarget(); + dom.setRelatedFilterTargetText(target, context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'); } function collectRelatedSet(root, relations, aggregationDepth, relatedFilterMode) { @@ -967,6 +971,7 @@ if (typeof module !== 'undefined' && module.exports) { module.exports = { // public packageContext, + dom, // private getVisibleDiagramElements, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 09be24877..487557355 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -513,17 +513,24 @@ test.describe('package.js', () => { test.describe('描画', () => { test.describe('UI表示', () => { test('renderRelatedFilterTarget: 対象表示を更新する', () => { - const doc = setupDocument(); - const target = new Element('span'); - doc.elementsById.set('related-filter-target', target); + const mockTarget = { textContent: '' }; + const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); + const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); + + test.mock.method(pkg.dom, 'getRelatedFilterTarget', getRelatedFilterTargetMock); + test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); testContext.relatedFilterFqn = null; pkg.renderRelatedFilterTarget(testContext); - assert.equal(target.textContent, '未選択'); + assert.equal(mockTarget.textContent, '未選択'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); testContext.relatedFilterFqn = 'app.domain'; pkg.renderRelatedFilterTarget(testContext); - assert.equal(target.textContent, 'app.domain'); + assert.equal(mockTarget.textContent, 'app.domain'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); }); test('updateAggregationDepthOptions: 選択肢を更新する', () => { From 06fd76a9b25441e54abd0323b6b409173d3595f7 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:42:05 +0900 Subject: [PATCH 06/51] refactor(package): encapsulate DOM access for error handling functions This change refactors the 'getOrCreateDiagramErrorBox', 'showDiagramErrorMessage', and 'hideDiagramErrorMessage' functions to use a 'dom' helper object. This encapsulates direct DOM access, making the DOM dependency explicit and improving testability by allowing easier mocking of DOM interactions for error display. --- .../resources/templates/assets/package.js | 99 +++++++------ jig-core/src/test/js/package.test.js | 132 +++++++++++++----- 2 files changed, 151 insertions(+), 80 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 27b271ede..cea14c9a5 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -17,37 +17,48 @@ const packageContext = { const dom = { getRelatedFilterTarget: () => document.getElementById('related-filter-target'), setRelatedFilterTargetText: (element, text) => { if (element) element.textContent = text; }, + + getDiagramErrorBox: () => document.getElementById('package-diagram-error'), + createDiagramErrorBox: (diagram) => { + let errorBox = document.createElement('div'); + errorBox.id = 'package-diagram-error'; + errorBox.setAttribute('role', 'alert'); + errorBox.style.display = 'none'; // Initially hidden + errorBox.style.whiteSpace = 'pre-wrap'; + errorBox.style.border = '1px solid #cc3333'; + errorBox.style.background = '#fff5f5'; + errorBox.style.color = '#222222'; + errorBox.style.padding = '8px 12px'; + errorBox.style.margin = '12px 0'; + // Message and action nodes created here too and appended + const message = document.createElement('pre'); + message.id = 'package-diagram-error-message'; + message.style.whiteSpace = 'pre-wrap'; + message.style.margin = '0 0 8px 0'; + + const action = document.createElement('button'); + action.id = 'package-diagram-error-action'; + action.type = 'button'; + action.style.display = 'none'; + action.textContent = '描画する'; + + errorBox.appendChild(message); + errorBox.appendChild(action); + diagram.parentNode.insertBefore(errorBox, diagram); // Insert into DOM + return errorBox; + }, + getDiagramErrorMessageNode: () => document.getElementById('package-diagram-error-message'), + getDiagramErrorActionNode: () => document.getElementById('package-diagram-error-action'), + setNodeTextContent: (element, text) => { if (element) element.textContent = text; }, + setNodeDisplay: (element, display) => { if (element) element.style.display = display; }, + setNodeOnClick: (element, handler) => { if (element) element.onclick = handler; }, + setDiagramElementDisplay: (diagram, display) => { if (diagram) diagram.style.display = display; }, }; function getOrCreateDiagramErrorBox(diagram) { - let errorBox = document.getElementById('package-diagram-error'); + let errorBox = dom.getDiagramErrorBox(); if (errorBox) return errorBox; - errorBox = document.createElement('div'); - errorBox.id = 'package-diagram-error'; - errorBox.setAttribute('role', 'alert'); - errorBox.style.display = 'none'; - errorBox.style.whiteSpace = 'pre-wrap'; - errorBox.style.border = '1px solid #cc3333'; - errorBox.style.background = '#fff5f5'; - errorBox.style.color = '#222222'; - errorBox.style.padding = '8px 12px'; - errorBox.style.margin = '12px 0'; - - const message = document.createElement('pre'); - message.id = 'package-diagram-error-message'; - message.style.whiteSpace = 'pre-wrap'; - message.style.margin = '0 0 8px 0'; - - const action = document.createElement('button'); - action.id = 'package-diagram-error-action'; - action.type = 'button'; - action.style.display = 'none'; - action.textContent = '描画する'; - - errorBox.appendChild(message); - errorBox.appendChild(action); - diagram.parentNode.insertBefore(errorBox, diagram); - return errorBox; + return dom.createDiagramErrorBox(diagram); } function showDiagramErrorMessage(message, withAction, err, hash, context) { @@ -61,36 +72,34 @@ function showDiagramErrorMessage(message, withAction, err, hash, context) { console.error('Mermaid error location:', hash.line, hash.loc); } const errorBox = getOrCreateDiagramErrorBox(diagram); - const messageNode = document.getElementById('package-diagram-error-message'); - const actionNode = document.getElementById('package-diagram-error-action'); - if (messageNode) messageNode.textContent = message; + const messageNode = dom.getDiagramErrorMessageNode(); + const actionNode = dom.getDiagramErrorActionNode(); + dom.setNodeTextContent(messageNode, message); if (actionNode) { - actionNode.style.display = withAction ? '' : 'none'; + dom.setNodeDisplay(actionNode, withAction ? '' : 'none'); if (withAction) { - actionNode.onclick = function () { + dom.setNodeOnClick(actionNode, function () { if (!context.pendingDiagramRender) return; renderDiagramSvg(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); context.pendingDiagramRender = null; - }; + }); } else { - actionNode.onclick = null; + dom.setNodeOnClick(actionNode, null); } } - errorBox.style.display = ''; - diagram.style.display = 'none'; + dom.setNodeDisplay(errorBox, ''); + dom.setDiagramElementDisplay(diagram, 'none'); } function hideDiagramErrorMessage(diagram) { const errorBox = getOrCreateDiagramErrorBox(diagram); - const messageNode = document.getElementById('package-diagram-error-message'); - const actionNode = document.getElementById('package-diagram-error-action'); - if (messageNode) messageNode.textContent = ''; - if (actionNode) { - actionNode.style.display = 'none'; - actionNode.onclick = null; - } - errorBox.style.display = 'none'; - diagram.style.display = ''; + const messageNode = dom.getDiagramErrorMessageNode(); + const actionNode = dom.getDiagramErrorActionNode(); + dom.setNodeTextContent(messageNode, ''); + dom.setNodeDisplay(actionNode, 'none'); + dom.setNodeOnClick(actionNode, null); + dom.setNodeDisplay(errorBox, 'none'); + dom.setDiagramElementDisplay(diagram, ''); } function renderDiagramSvg(text, maxEdges, context) { diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 487557355..fe243c97a 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -584,40 +584,71 @@ test.describe('package.js', () => { }); test('getOrCreateDiagramErrorBox: エラーボックスを作成/再利用する', () => { - const doc = setupDocument(); - const container = new Element('div', doc); - const diagram = new Element('div', doc); - container.appendChild(diagram); + const diagram = { parentNode: { insertBefore: test.mock.fn() } }; // Minimal mock for diagram + + const createdErrorBox = { id: 'package-diagram-error', appendChild: test.mock.fn() }; + const createDiagramErrorBoxMock = test.mock.fn(() => createdErrorBox); - const first = pkg.getOrCreateDiagramErrorBox(diagram, testContext); - const second = pkg.getOrCreateDiagramErrorBox(diagram, testContext); + // --- First call: Error box does not exist, should be created --- + const getDiagramErrorBoxMockInitialNull = test.mock.fn(() => null); + test.mock.method(pkg.dom, 'getDiagramErrorBox', getDiagramErrorBoxMockInitialNull); + test.mock.method(pkg.dom, 'createDiagramErrorBox', createDiagramErrorBoxMock); - assert.equal(first, second); - assert.equal(first.id, 'package-diagram-error'); - assert.equal(container.children[0], first); + const firstErrorBox = pkg.getOrCreateDiagramErrorBox(diagram); + + assert.equal(getDiagramErrorBoxMockInitialNull.mock.calls.length, 1, 'getDiagramErrorBox should be called once initially'); + assert.equal(createDiagramErrorBoxMock.mock.calls.length, 1, 'createDiagramErrorBox should be called once'); + assert.deepEqual(createDiagramErrorBoxMock.mock.calls[0].arguments, [diagram], 'createDiagramErrorBox called with diagram'); + assert.equal(firstErrorBox, createdErrorBox, 'First call returns newly created error box'); + + // --- Second call: Error box already exists --- + const getDiagramErrorBoxMockReturningCreated = test.mock.fn(() => createdErrorBox); // Returns the *same* instance + // Re-mock getDiagramErrorBox to return the existing one. + // It will override the previous mock, so the first mock will not be called again. + test.mock.method(pkg.dom, 'getDiagramErrorBox', getDiagramErrorBoxMockReturningCreated); + + const secondErrorBox = pkg.getOrCreateDiagramErrorBox(diagram); + + assert.equal(getDiagramErrorBoxMockReturningCreated.mock.calls.length, 1, 'getDiagramErrorBox should be called once more for existing'); + // The createDiagramErrorBoxMock.mock.calls.length should still be 1 from the first call. + assert.equal(createDiagramErrorBoxMock.mock.calls.length, 1, 'createDiagramErrorBox should not be called again'); + assert.equal(secondErrorBox, createdErrorBox, 'Second call returns existing error box'); + assert.equal(firstErrorBox, secondErrorBox, 'Both calls should return the same instance when reused'); }); test('showDiagramErrorMessage/hideDiagramErrorMessage: 表示を切り替える', () => { - const doc = setupDocument(); - const container = new Element('div', doc); - const diagram = new Element('div', doc); - container.appendChild(diagram); - testContext.diagramElement = diagram; + const diagramMock = { style: { display: '' } }; // Mock for context.diagramElement + const errorBoxMock = { style: { display: 'none' } }; // Mock for the error box element + const messageNodeMock = { textContent: '' }; // Mock for the message node + const actionNodeMock = { style: { display: 'none' }, onclick: null }; // Mock for the action node + + // Mock dom helpers + test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => errorBoxMock)); + test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => errorBoxMock)); // getOrCreate will call this if not found + test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => messageNodeMock)); + test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => actionNodeMock)); + test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); + test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); + test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + + testContext.diagramElement = diagramMock; const errors = withConsoleErrorCapture(() => { pkg.showDiagramErrorMessage('test-error-message', false, null, null, testContext); }); - const errorBox = doc.getElementById('package-diagram-error'); - const messageNode = doc.getElementById('package-diagram-error-message'); - - assert.equal(errorBox.style.display, ''); - assert.equal(diagram.style.display, 'none'); - assert.equal(messageNode.textContent, 'test-error-message'); - assert.equal(errors.some(line => line.includes('test-error-message')), true); - pkg.hideDiagramErrorMessage(diagram, testContext); - assert.equal(errorBox.style.display, 'none'); - assert.equal(diagram.style.display, ''); + assert.equal(errorBoxMock.style.display, '', 'errorBox should be displayed'); + assert.equal(diagramMock.style.display, 'none', 'diagram should be hidden'); + assert.equal(messageNodeMock.textContent, 'test-error-message', 'messageNode content should be set'); + assert.equal(errors.some(line => line.includes('test-error-message')), true, 'console.error should be called'); + + pkg.hideDiagramErrorMessage(diagramMock); // Pass diagramMock directly as it's used + assert.equal(errorBoxMock.style.display, 'none', 'errorBox should be hidden'); + assert.equal(diagramMock.style.display, '', 'diagram should be displayed'); + assert.equal(messageNodeMock.textContent, '', 'messageNode content should be cleared'); + assert.equal(actionNodeMock.style.display, 'none', 'actionNode should be hidden'); + assert.equal(actionNodeMock.onclick, null, 'actionNode onclick should be cleared'); }); test('renderDiagramSvg: Mermaid描画を実行する', () => { @@ -784,7 +815,24 @@ test.describe('package.js', () => { test.describe('分岐/エラー', () => { test('renderPackageDiagram: エッジ数超過で保留/エラー表示', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); + // setupDiagramEnvironmentはtestContext.diagramElementを設定する。 + // そのdiagramElementがdomヘルパーによって操作されることをモックする。 + const diagramMock = setupDiagramEnvironment(doc, testContext); // testContext.diagramElementも設定される + + // Mock dom helpers used by showDiagramErrorMessage internally + const errorBoxMock = { style: { display: 'none' } }; + const messageNodeMock = { textContent: '' }; + const actionNodeMock = { style: { display: 'none' }, onclick: null }; + test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => errorBoxMock)); + test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => errorBoxMock)); // called by getOrCreate + test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => messageNodeMock)); + test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => actionNodeMock)); + test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); + test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); + test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + + // Data setup (same as before) const packages = []; const relations = []; for (let i = 0; i < 501; i += 1) { @@ -800,33 +848,47 @@ test.describe('package.js', () => { pkg.renderPackageDiagram(testContext, null, null); }); - const errorBox = doc.getElementById('package-diagram-error'); - assert.equal(errorBox.style.display, ''); + assert.equal(errorBoxMock.style.display, '', 'errorBox should be displayed by showDiagramErrorMessage'); // Check display set by showDiagramErrorMessage + assert.equal(diagramMock.style.display, 'none', 'diagram should be hidden by showDiagramErrorMessage'); // Check display set by showDiagramErrorMessage assert.equal(errors.some(line => line.includes('関連数が多すぎるため描画を省略しました。')), true); + assert.ok(actionNodeMock.onclick, 'actionNode should have onclick handler'); + assert.equal(actionNodeMock.style.display, '', 'actionNode should be displayed'); }); test('mermaid.parseError: エラー内容を表示', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); + const diagramMock = setupDiagramEnvironment(doc, testContext); // testContext.diagramElement is set here setPackageData(doc, { packages: [{fqn: 'app.a', name: 'A', classCount: 1}], relations: [], }, testContext); - pkg.renderPackageDiagram(testContext, null, null); - + pkg.renderPackageDiagram(testContext, null, null); // This prepares the diagram and sets mermaid.parseError + + // Mock dom helpers used by showDiagramErrorMessage internally + const errorBoxMock = { style: { display: 'none' } }; + const messageNodeMock = { textContent: '' }; + const actionNodeMock = { style: { display: 'none' }, onclick: null }; + test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => errorBoxMock)); + test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => errorBoxMock)); // called by getOrCreate + test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => messageNodeMock)); + test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => actionNodeMock)); + test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); + test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); + test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + // Mermaidはパース失敗時のみ呼ばれるため、テストでは直接呼び出す。 const errors = withConsoleErrorCapture(() => { global.mermaid.parseError( - {message: 'Edge limit exceeded'}, + {message: 'Mermaid parse error details'}, // Use a more specific message {line: 10, loc: 2} ); }); - const messageNode = doc.getElementById('package-diagram-error-message'); - assert.equal(messageNode.textContent.includes('Mermaid parse error:'), true); - assert.equal(messageNode.textContent.includes('Line: 10 Column: 2'), true); + assert.equal(messageNodeMock.textContent.includes('Mermaid parse error:'), true); + assert.equal(messageNodeMock.textContent.includes('Line: 10 Column: 2'), true); assert.equal(errors.some(line => line.includes('Mermaid parse error:')), true); - assert.equal(errors.some(line => line.includes('Edge limit exceeded')), true); + assert.equal(errors.some(line => line.includes('Mermaid parse error details')), true); // Check for specific error message assert.equal(errors.some(line => line.includes('Mermaid error location: 10 2')), true); }); From 8983051ce00ed4c1b40911a8dcdfc2597bb945ae Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:44:25 +0900 Subject: [PATCH 07/51] refactor(package): encapsulate DOM access for renderDiagramSvg This change refactors the 'renderDiagramSvg' function to use a 'dom' helper object. This encapsulates direct DOM access for diagram content manipulation and attribute removal, improving testability by allowing easier mocking of DOM interactions. --- .../resources/templates/assets/package.js | 6 ++-- jig-core/src/test/js/package.test.js | 30 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index cea14c9a5..0e60ef6c7 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -53,6 +53,8 @@ const dom = { setNodeDisplay: (element, display) => { if (element) element.style.display = display; }, setNodeOnClick: (element, handler) => { if (element) element.onclick = handler; }, setDiagramElementDisplay: (diagram, display) => { if (diagram) diagram.style.display = display; }, + setDiagramContent: (element, content) => { if (element) element.textContent = content; }, + removeDiagramAttribute: (element, attribute) => { if (element) element.removeAttribute(attribute); }, }; function getOrCreateDiagramErrorBox(diagram) { @@ -106,8 +108,8 @@ function renderDiagramSvg(text, maxEdges, context) { const diagram = context.diagramElement; if (!diagram || !window.mermaid) return; hideDiagramErrorMessage(diagram); - diagram.removeAttribute('data-processed'); - diagram.textContent = text; + dom.removeDiagramAttribute(diagram, 'data-processed'); + dom.setDiagramContent(diagram, text); mermaid.initialize({startOnLoad: false, securityLevel: 'loose', maxEdges: maxEdges}); mermaid.run({nodes: [diagram]}); } diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index fe243c97a..2bc7f6d1d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -652,11 +652,25 @@ test.describe('package.js', () => { }); test('renderDiagramSvg: Mermaid描画を実行する', () => { - const doc = setupDocument(); - const container = new Element('div', doc); - const diagram = new Element('div', doc); - container.appendChild(diagram); - testContext.diagramElement = diagram; + const diagramMock = { + removeAttribute: test.mock.fn(), + textContent: '', + style: { display: '' } // Needs to be there for dom.setDiagramElementDisplay + }; + testContext.diagramElement = diagramMock; + + // Mock dom helpers used by hideDiagramErrorMessage and renderDiagramSvg itself + test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => ({ style: {} }))); // Mock return for errorBox + test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => ({ style: {} }))); + test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => ({ textContent: '' }))); + test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => ({ style: {}, onclick: null }))); + test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); + test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); + test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); + test.mock.method(pkg.dom, 'setDiagramContent', test.mock.fn((el, content) => { el.textContent = content; })); + test.mock.method(pkg.dom, 'removeDiagramAttribute', test.mock.fn((el, attr) => { /* no-op */ })); + let runCalled = false; global.window = { @@ -672,7 +686,11 @@ test.describe('package.js', () => { pkg.renderDiagramSvg('graph TD', 100, testContext); - assert.equal(diagram.textContent, 'graph TD'); + assert.equal(diagramMock.textContent, 'graph TD'); // Check through the dom helper mock + assert.equal(pkg.dom.removeDiagramAttribute.mock.calls.length, 1); + assert.deepEqual(pkg.dom.removeDiagramAttribute.mock.calls[0].arguments, [diagramMock, 'data-processed']); + assert.equal(pkg.dom.setDiagramContent.mock.calls.length, 1); + assert.deepEqual(pkg.dom.setDiagramContent.mock.calls[0].arguments, [diagramMock, 'graph TD']); assert.equal(runCalled, true); }); }); From 1aed3467361bb3a29aa0a8ede8df7b8d6a151a47 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 12:48:18 +0900 Subject: [PATCH 08/51] refactor(package): encapsulate DOM access for getPackageSummaryData This change refactors the 'getPackageSummaryData' function to use 'dom' helper methods ('getPackageDataScript' and 'getNodeTextContent'). This encapsulates direct DOM access for retrieving initial package data, making the DOM dependency explicit and improving testability by allowing easier mocking of DOM interactions. --- .../resources/templates/assets/package.js | 4 +- jig-core/src/test/js/package.test.js | 60 +++++++++++-------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 0e60ef6c7..e0b8b9522 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -55,6 +55,8 @@ const dom = { setDiagramElementDisplay: (diagram, display) => { if (diagram) diagram.style.display = display; }, setDiagramContent: (element, content) => { if (element) element.textContent = content; }, removeDiagramAttribute: (element, attribute) => { if (element) element.removeAttribute(attribute); }, + getPackageDataScript: () => document.getElementById('package-data'), + getNodeTextContent: (element) => { return element ? element.textContent : ''; }, }; function getOrCreateDiagramErrorBox(diagram) { @@ -116,7 +118,7 @@ function renderDiagramSvg(text, maxEdges, context) { function getPackageSummaryData(context) { if (context.packageSummaryCache) return context.packageSummaryCache; - const jsonText = document.getElementById('package-data').textContent; + const jsonText = dom.getNodeTextContent(dom.getPackageDataScript()); /** @type {{packages?: Array<{fqn: string, name: string, classCount: number, description: string}>, relations?: Array<{from: string, to: string}>, causeRelationEvidence?: Array<{from: string, to: string}>} | Array<{fqn: string, name: string, classCount: number, description: string}>} */ const packageData = JSON.parse(jsonText); context.packageSummaryCache = { diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 2bc7f6d1d..3703457af 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -170,11 +170,15 @@ function buildPackageRows(doc, fqns) { return rows; } -function setPackageData(doc, data, context) { - const dataElement = new Element('script', doc); - dataElement.textContent = JSON.stringify(data); - doc.elementsById.set('package-data', dataElement); - context.packageSummaryCache = null; +function setPackageData(data, context) { + const mockDataContent = JSON.stringify(data); + const mockDataElement = { textContent: mockDataContent }; + + // Mock the dom helpers that getPackageSummaryData uses + test.mock.method(pkg.dom, 'getPackageDataScript', test.mock.fn(() => mockDataElement)); + test.mock.method(pkg.dom, 'getNodeTextContent', test.mock.fn((el) => el.textContent)); + + context.packageSummaryCache = null; // Reset cache } function withConsoleErrorCapture(callback) { @@ -296,13 +300,21 @@ test.describe('package.js', () => { test.describe('データ取得', () => { test('getPackageSummaryData: 配列/オブジェクト両対応', () => { - const doc = setupDocument(); - setPackageData(doc, [{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); + const mockPackageDataContent = JSON.stringify([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}]); + const mockPackageDataElement = { textContent: mockPackageDataContent }; + + test.mock.method(pkg.dom, 'getPackageDataScript', test.mock.fn(() => mockPackageDataElement)); + test.mock.method(pkg.dom, 'getNodeTextContent', test.mock.fn((el) => el.textContent)); + + testContext.packageSummaryCache = null; // Ensure cache is clear before test const data = pkg.getPackageSummaryData(testContext); assert.equal(data.packages.length, 1); assert.equal(data.relations.length, 0); + assert.equal(pkg.dom.getPackageDataScript.mock.calls.length, 1); + assert.equal(pkg.dom.getNodeTextContent.mock.calls.length, 1); + assert.deepEqual(pkg.dom.getNodeTextContent.mock.calls[0].arguments, [mockPackageDataElement]); }); test('getPackageDepth: 深さを返す', () => { @@ -313,7 +325,7 @@ test.describe('package.js', () => { test('getMaxPackageDepth: 最大深さを返す', () => { const doc = setupDocument(); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.domain.a'}, {fqn: 'app.b'}, @@ -486,7 +498,7 @@ test.describe('package.js', () => { test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.a'}, {fqn: 'app.b'}, @@ -537,7 +549,7 @@ test.describe('package.js', () => { const doc = setupDocument(); const select = new Element('select'); doc.elementsById.set('package-depth-select', select); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.domain'}, {fqn: 'lib.core'}, @@ -561,7 +573,7 @@ test.describe('package.js', () => { test.describe('一覧/補助', () => { test('renderPackageTable: 行とカウントを描画する', () => { const doc = setupDocument(); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.a', name: 'A', classCount: 2}, {fqn: 'app.b', name: 'B', classCount: 1}, @@ -699,7 +711,7 @@ test.describe('package.js', () => { test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.domain.core'}, {fqn: 'app.domain.sub'}, @@ -719,7 +731,7 @@ test.describe('package.js', () => { test('applyDefaultPackageFilterIfPresent: 入力済みなら適用しない', () => { const doc = setupDocument(); - setPackageData(doc, { + setPackageData({ packages: [{fqn: 'app.domain.core'}], relations: [], }, testContext); @@ -774,7 +786,7 @@ test.describe('package.js', () => { test('renderPackageDiagram: 相互依存を含む描画', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.a', name: 'A', classCount: 1}, {fqn: 'app.b', name: 'B', classCount: 1}, @@ -797,7 +809,7 @@ test.describe('package.js', () => { test('renderPackageDiagram: サブグラフ内のFQNノードラベルが省略される', () => { const doc = setupDocument(); const diagram = setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'com.example', name: 'example', classCount: 1}, {fqn: 'com.example.domain', name: 'domain', classCount: 1}, @@ -860,7 +872,7 @@ test.describe('package.js', () => { packages.push({fqn: to, name: to, classCount: 1}); relations.push({from, to}); } - setPackageData(doc, {packages, relations}, testContext); + setPackageData({packages, relations}, testContext); const errors = withConsoleErrorCapture(() => { pkg.renderPackageDiagram(testContext, null, null); @@ -876,7 +888,7 @@ test.describe('package.js', () => { test('mermaid.parseError: エラー内容を表示', () => { const doc = setupDocument(); const diagramMock = setupDiagramEnvironment(doc, testContext); // testContext.diagramElement is set here - setPackageData(doc, { + setPackageData({ packages: [{fqn: 'app.a', name: 'A', classCount: 1}], relations: [], }, testContext); @@ -913,7 +925,7 @@ test.describe('package.js', () => { test('renderDiagramAndTable: 描画とフィルタ適用を行う', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.a', name: 'A', classCount: 1}, {fqn: 'app.b', name: 'B', classCount: 1}, @@ -944,7 +956,7 @@ test.describe('package.js', () => { test('setupPackageFilterControls: 適用/解除をハンドリング', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], relations: [], }, testContext); @@ -967,7 +979,7 @@ test.describe('package.js', () => { test('setupPackageFilterControls: Enterキーで適用', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], relations: [], }, testContext); @@ -993,7 +1005,7 @@ test.describe('package.js', () => { test('setupAggregationDepthControl: 変更を反映する', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -1014,7 +1026,7 @@ test.describe('package.js', () => { test('setupRelatedFilterControls: モード変更を反映', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [ {fqn: 'app.a'}, {fqn: 'app.b'}, @@ -1048,7 +1060,7 @@ test.describe('package.js', () => { test('setupDiagramDirectionControls: 向きを切り替える', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); - setPackageData(doc, { + setPackageData({ packages: [{fqn: 'app.a'}], relations: [], }, testContext); @@ -1137,7 +1149,7 @@ test.describe('package.js', () => { // renderDiagramAndTableの副作用をチェックするための準備 setupDiagramEnvironment(doc, testContext); - setPackageData(doc, {packages: [{fqn: 'a'}], relations: []}, testContext); + setPackageData({packages: [{fqn: 'a'}], relations: []}, testContext); const depthSelect = createDepthSelect(doc); const dummyOption = doc.createElement('option'); dummyOption.id = 'dummy-option-for-test'; From b233186777afe2390fc504291a8a4d700d3a1f67 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:10:12 +0900 Subject: [PATCH 09/51] Extract mermaid source builder --- .../resources/templates/assets/package.js | 166 ++++++++++-------- 1 file changed, 89 insertions(+), 77 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index e0b8b9522..8f926b6e7 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -564,88 +564,18 @@ function transitiveReduction(relations) { return relations.filter(edge => !toRemove.has(`${edge.from}::${edge.to}`)); } -function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { - const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; - const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; - const withinPackageFilter = fqn => - !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); - const visiblePackages = packageFilterFqn - ? packages.filter(item => withinPackageFilter(item.fqn)) - : packages; - const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); - const filteredRelations = packageFilterFqn - ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) - : relations; - const filteredCauseRelationEvidence = packageFilterFqn - ? causeRelationEvidence.filter(relation => { - const fromPackage = getPackageFqnFromTypeFqn(relation.from); - const toPackage = getPackageFqnFromTypeFqn(relation.to); - return withinPackageFilter(fromPackage) && withinPackageFilter(toPackage); - }) - : causeRelationEvidence; - const visibleRelations = filteredRelations - .map(relation => ({ - from: getAggregatedFqn(relation.from, aggregationDepth), - to: getAggregatedFqn(relation.to, aggregationDepth), - })) - .filter(relation => relation.from !== relation.to); - const uniqueRelationMap = new Map(); - visibleRelations.forEach(relation => { - uniqueRelationMap.set(`${relation.from}::${relation.to}`, relation); - }); - let uniqueRelations = Array.from(uniqueRelationMap.values()); - - if (transitiveReductionEnabled) { - uniqueRelations = transitiveReduction(uniqueRelations); - } - - if (aggregatedRoot) { - const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); - if (relatedFilterMode === 'direct') { - uniqueRelations = uniqueRelations.filter(relation => - relation.from === aggregatedRoot || relation.to === aggregatedRoot - ); - } else { - uniqueRelations = uniqueRelations.filter(relation => - relatedSet.has(relation.from) && relatedSet.has(relation.to) - ); - } - visibleSet.clear(); - relatedSet.forEach(value => visibleSet.add(value)); - } - uniqueRelations.forEach(relation => { - visibleSet.add(relation.from); - visibleSet.add(relation.to); - }); - return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; -} - -function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { - const diagram = document.getElementById('package-relation-diagram'); - if (!diagram) return; - context.diagramElement = diagram; - - const {packages, relations, causeRelationEvidence} = getPackageSummaryData(context); - - const { - uniqueRelations, - visibleSet, - filteredCauseRelationEvidence - } = getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, context.aggregationDepth, context.relatedFilterMode, context.transitiveReductionEnabled); - +function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagramDirection) { const escapeMermaidText = text => text.replace(/"/g, '\\"'); - const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); - const lines = [`graph ${context.diagramDirection}`]; - + const lines = [`graph ${diagramDirection}`]; const nodeIdByFqn = new Map(); - context.diagramNodeIdToFqn = new Map(); + const nodeIdToFqn = new Map(); const nodeLabelById = new Map(); let nodeIndex = 0; const ensureNodeId = fqn => { if (nodeIdByFqn.has(fqn)) return nodeIdByFqn.get(fqn); const nodeId = `P${nodeIndex++}`; nodeIdByFqn.set(fqn, nodeId); - context.diagramNodeIdToFqn.set(nodeId, fqn); + nodeIdToFqn.set(nodeId, fqn); const label = nameByFqn.get(fqn) || fqn; nodeLabelById.set(nodeId, label); return nodeId; @@ -693,7 +623,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { }); const addNodeLines = (nodeId, parentSubgraphFqn) => { - const fqn = context.diagramNodeIdToFqn.get(nodeId); + const fqn = nodeIdToFqn.get(nodeId); let displayLabel = nodeLabelById.get(nodeId); if (displayLabel === fqn && parentSubgraphFqn && fqn.startsWith(`${parentSubgraphFqn}.`)) { @@ -752,9 +682,90 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { edgeLines.forEach(line => lines.push(line)); linkStyles.forEach(styleLine => lines.push(styleLine)); + + return {source: lines.join('\n'), nodeIdToFqn, mutualPairs}; +} + +function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { + const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; + const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + const withinPackageFilter = fqn => + !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); + const visiblePackages = packageFilterFqn + ? packages.filter(item => withinPackageFilter(item.fqn)) + : packages; + const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); + const filteredRelations = packageFilterFqn + ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) + : relations; + const filteredCauseRelationEvidence = packageFilterFqn + ? causeRelationEvidence.filter(relation => { + const fromPackage = getPackageFqnFromTypeFqn(relation.from); + const toPackage = getPackageFqnFromTypeFqn(relation.to); + return withinPackageFilter(fromPackage) && withinPackageFilter(toPackage); + }) + : causeRelationEvidence; + const visibleRelations = filteredRelations + .map(relation => ({ + from: getAggregatedFqn(relation.from, aggregationDepth), + to: getAggregatedFqn(relation.to, aggregationDepth), + })) + .filter(relation => relation.from !== relation.to); + const uniqueRelationMap = new Map(); + visibleRelations.forEach(relation => { + uniqueRelationMap.set(`${relation.from}::${relation.to}`, relation); + }); + let uniqueRelations = Array.from(uniqueRelationMap.values()); + + if (transitiveReductionEnabled) { + uniqueRelations = transitiveReduction(uniqueRelations); + } + + if (aggregatedRoot) { + const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); + if (relatedFilterMode === 'direct') { + uniqueRelations = uniqueRelations.filter(relation => + relation.from === aggregatedRoot || relation.to === aggregatedRoot + ); + } else { + uniqueRelations = uniqueRelations.filter(relation => + relatedSet.has(relation.from) && relatedSet.has(relation.to) + ); + } + visibleSet.clear(); + relatedSet.forEach(value => visibleSet.add(value)); + } + uniqueRelations.forEach(relation => { + visibleSet.add(relation.from); + visibleSet.add(relation.to); + }); + return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; +} + +function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { + const diagram = document.getElementById('package-relation-diagram'); + if (!diagram) return; + context.diagramElement = diagram; + + const {packages, relations, causeRelationEvidence} = getPackageSummaryData(context); + + const { + uniqueRelations, + visibleSet, + filteredCauseRelationEvidence + } = getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, context.aggregationDepth, context.relatedFilterMode, context.transitiveReductionEnabled); + + const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); + const {source, nodeIdToFqn, mutualPairs} = buildMermaidDiagramSource( + visibleSet, + uniqueRelations, + nameByFqn, + context.diagramDirection + ); + context.diagramNodeIdToFqn = nodeIdToFqn; renderMutualDependencyList(mutualPairs, filteredCauseRelationEvidence, context); - context.lastDiagramSource = lines.join('\n'); + context.lastDiagramSource = source; context.lastDiagramEdgeCount = uniqueRelations.length; if (context.lastDiagramEdgeCount > context.DEFAULT_MAX_EDGES) { context.pendingDiagramRender = {text: context.lastDiagramSource, maxEdges: context.lastDiagramEdgeCount}; @@ -1009,6 +1020,7 @@ if (typeof module !== 'undefined' && module.exports) { renderDiagramAndTable, renderMutualDependencyList, renderPackageDiagram, + buildMermaidDiagramSource, applyRelatedFilter, setupPackageFilterControls, setupAggregationDepthControl, @@ -1020,4 +1032,4 @@ if (typeof module !== 'undefined' && module.exports) { detectStronglyConnectedComponents, transitiveReduction, }; -} \ No newline at end of file +} From bcd6113cab0e1ebd28043798525b14a73d0a93e9 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:12:03 +0900 Subject: [PATCH 10/51] Extract package table row builder --- .../resources/templates/assets/package.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 8f926b6e7..36d72d011 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -248,12 +248,7 @@ function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, function renderPackageTable(context) { const {packages, relations} = getPackageSummaryData(context); - const incomingCounts = new Map(); - const outgoingCounts = new Map(); - relations.forEach(relation => { - outgoingCounts.set(relation.from, (outgoingCounts.get(relation.from) ?? 0) + 1); - incomingCounts.set(relation.to, (incomingCounts.get(relation.to) ?? 0) + 1); - }); + const rows = buildPackageTableRows(packages, relations); const tbody = document.querySelector('#package-table tbody'); @@ -270,7 +265,7 @@ function renderPackageTable(context) { applyRelatedFilter(fqn, context); }; - packages.forEach(item => { + rows.forEach(item => { const tr = document.createElement('tr'); const actionTd = document.createElement('td'); @@ -314,12 +309,12 @@ function renderPackageTable(context) { tr.appendChild(classCountTd); const incomingCountTd = document.createElement('td'); - incomingCountTd.textContent = String(incomingCounts.get(item.fqn) ?? 0); + incomingCountTd.textContent = String(item.incomingCount ?? 0); incomingCountTd.className = 'number'; tr.appendChild(incomingCountTd); const outgoingCountTd = document.createElement('td'); - outgoingCountTd.textContent = String(outgoingCounts.get(item.fqn) ?? 0); + outgoingCountTd.textContent = String(item.outgoingCount ?? 0); outgoingCountTd.className = 'number'; tr.appendChild(outgoingCountTd); @@ -327,6 +322,20 @@ function renderPackageTable(context) { }); } +function buildPackageTableRows(packages, relations) { + const incomingCounts = new Map(); + const outgoingCounts = new Map(); + relations.forEach(relation => { + outgoingCounts.set(relation.from, (outgoingCounts.get(relation.from) ?? 0) + 1); + incomingCounts.set(relation.to, (incomingCounts.get(relation.to) ?? 0) + 1); + }); + return packages.map(item => ({ + ...item, + incomingCount: incomingCounts.get(item.fqn) ?? 0, + outgoingCount: outgoingCounts.get(item.fqn) ?? 0, + })); +} + function applyPackageFilterToTable(packageFilterFqn) { const rows = document.querySelectorAll('#package-table tbody tr'); const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; @@ -1014,6 +1023,7 @@ if (typeof module !== 'undefined' && module.exports) { hideDiagramErrorMessage, renderDiagramSvg, renderPackageTable, + buildPackageTableRows, applyPackageFilterToTable, applyRelatedFilterToTable, renderRelatedFilterTarget, From 159ba8ee903c6e2ad608a3f67055645eb56aa79e Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:12:47 +0900 Subject: [PATCH 11/51] Extract diagram meta builders --- .../resources/templates/assets/package.js | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 36d72d011..c73c5f4dc 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -573,6 +573,31 @@ function transitiveReduction(relations) { return relations.filter(edge => !toRemove.has(`${edge.from}::${edge.to}`)); } +function buildMutualPairs(relations) { + const relationKey = (from, to) => `${from}::${to}`; + const canonicalPairKey = (from, to) => (from < to ? `${from}::${to}` : `${to}::${from}`); + const relationSet = new Set(relations.map(relation => relationKey(relation.from, relation.to))); + const mutualPairs = new Set(); + relations.forEach(relation => { + if (relationSet.has(relationKey(relation.to, relation.from))) { + mutualPairs.add(canonicalPairKey(relation.from, relation.to)); + } + }); + return mutualPairs; +} + +function buildParentFqns(visibleSet) { + const parentFqns = new Set(); + Array.from(visibleSet).sort().forEach(fqn => { + const parts = fqn.split('.'); + for (let i = 1; i < parts.length; i += 1) { + const prefix = parts.slice(0, i).join('.'); + if (visibleSet.has(prefix)) parentFqns.add(prefix); + } + }); + return parentFqns; +} + function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagramDirection) { const escapeMermaidText = text => text.replace(/"/g, '\\"'); const lines = [`graph ${diagramDirection}`]; @@ -591,15 +616,7 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr }; Array.from(visibleSet).sort().forEach(ensureNodeId); - const relationKey = (from, to) => `${from}::${to}`; - const canonicalPairKey = (from, to) => (from < to ? `${from}::${to}` : `${to}::${from}`); - const relationSet = new Set(uniqueRelations.map(relation => relationKey(relation.from, relation.to))); - const mutualPairs = new Set(); - uniqueRelations.forEach(relation => { - if (relationSet.has(relationKey(relation.to, relation.from))) { - mutualPairs.add(canonicalPairKey(relation.from, relation.to)); - } - }); + const mutualPairs = buildMutualPairs(uniqueRelations); const linkStyles = []; let linkIndex = 0; @@ -607,7 +624,9 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr uniqueRelations.forEach(relation => { const fromId = ensureNodeId(relation.from); const toId = ensureNodeId(relation.to); - const pairKey = canonicalPairKey(relation.from, relation.to); + const pairKey = relation.from < relation.to + ? `${relation.from}::${relation.to}` + : `${relation.to}::${relation.from}`; if (mutualPairs.has(pairKey)) { if (relation.from > relation.to) { return; @@ -622,14 +641,7 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr }); const visibleFqns = Array.from(visibleSet).sort(); - const parentFqns = new Set(); - visibleFqns.forEach(fqn => { - const parts = fqn.split('.'); - for (let i = 1; i < parts.length; i += 1) { - const prefix = parts.slice(0, i).join('.'); - if (visibleSet.has(prefix)) parentFqns.add(prefix); - } - }); + const parentFqns = buildParentFqns(visibleSet); const addNodeLines = (nodeId, parentSubgraphFqn) => { const fqn = nodeIdToFqn.get(nodeId); From f741910ec8605c38599bb8968583bebcd3f83d0b Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:15:13 +0900 Subject: [PATCH 12/51] Centralize DOM accessors --- .../resources/templates/assets/package.js | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index c73c5f4dc..cb6b01f92 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -18,6 +18,20 @@ const dom = { getRelatedFilterTarget: () => document.getElementById('related-filter-target'), setRelatedFilterTargetText: (element, text) => { if (element) element.textContent = text; }, + getPackageTableBody: () => document.querySelector('#package-table tbody'), + getPackageTableRows: () => document.querySelectorAll('#package-table tbody tr'), + getPackageFilterInput: () => document.getElementById('package-filter-input'), + getApplyPackageFilterButton: () => document.getElementById('apply-package-filter'), + getClearPackageFilterButton: () => document.getElementById('clear-package-filter'), + getDepthSelect: () => document.getElementById('package-depth-select'), + getRelatedModeSelect: () => document.getElementById('related-mode-select'), + getClearRelatedFilterButton: () => document.getElementById('clear-related-filter'), + getDiagramDirectionRadios: () => document.querySelectorAll('input[name="diagram-direction"]'), + getDiagramDirectionRadio: () => document.querySelector('input[name="diagram-direction"]'), + getMutualDependencyList: () => document.getElementById('mutual-dependency-list'), + getDiagram: () => document.getElementById('package-relation-diagram'), + getDocumentBody: () => document.body, + getDiagramErrorBox: () => document.getElementById('package-diagram-error'), createDiagramErrorBox: (diagram) => { let errorBox = document.createElement('div'); @@ -250,9 +264,9 @@ function renderPackageTable(context) { const {packages, relations} = getPackageSummaryData(context); const rows = buildPackageTableRows(packages, relations); - const tbody = document.querySelector('#package-table tbody'); + const tbody = dom.getPackageTableBody(); - const input = document.getElementById('package-filter-input'); + const input = dom.getPackageFilterInput(); const applyFilter = fqn => { if (input) { input.value = fqn; @@ -337,7 +351,7 @@ function buildPackageTableRows(packages, relations) { } function applyPackageFilterToTable(packageFilterFqn) { - const rows = document.querySelectorAll('#package-table tbody tr'); + const rows = dom.getPackageTableRows(); const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; rows.forEach(row => { const fqnCell = row.querySelector('td.fqn'); @@ -348,7 +362,7 @@ function applyPackageFilterToTable(packageFilterFqn) { } function applyRelatedFilterToTable(fqn, context) { - const rows = document.querySelectorAll('#package-table tbody tr'); + const rows = dom.getPackageTableRows(); const packageFilterPrefix = context.packageFilterFqn ? `${context.packageFilterFqn}.` : null; const withinPackageFilter = rowFqn => !context.packageFilterFqn || rowFqn === context.packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); @@ -432,7 +446,7 @@ function renderDiagramAndTable(context) { } function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) { - const container = document.getElementById('mutual-dependency-list'); + const container = dom.getMutualDependencyList(); if (!container) return; if (!mutualPairs || mutualPairs.size === 0) { container.style.display = 'none'; @@ -764,7 +778,7 @@ function getVisibleDiagramElements(packages, relations, causeRelationEvidence, p } function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { - const diagram = document.getElementById('package-relation-diagram'); + const diagram = dom.getDiagram(); if (!diagram) return; context.diagramElement = diagram; @@ -831,9 +845,9 @@ if (typeof window !== 'undefined') { } function setupPackageFilterControls(context) { - const input = document.getElementById('package-filter-input'); - const applyButton = document.getElementById('apply-package-filter'); - const clearPackageButton = document.getElementById('clear-package-filter'); + const input = dom.getPackageFilterInput(); + const applyButton = dom.getApplyPackageFilterButton(); + const clearPackageButton = dom.getClearPackageFilterButton(); if (!input || !applyButton || !clearPackageButton) return; const applyFilter = () => { @@ -860,7 +874,7 @@ function setupPackageFilterControls(context) { } function setupAggregationDepthControl(context) { - const select = document.getElementById('package-depth-select'); + const select = dom.getDepthSelect(); if (!select) return; const {packages} = getPackageSummaryData(context); const maxDepth = packages.reduce((max, item) => Math.max(max, getPackageDepth(item.fqn)), 0); @@ -875,7 +889,7 @@ function setupAggregationDepthControl(context) { } function updateAggregationDepthOptions(maxDepth, context) { - const select = document.getElementById('package-depth-select'); + const select = dom.getDepthSelect(); if (!select) return; const {packages, relations} = getPackageSummaryData(context); let aggregationStats; @@ -909,7 +923,7 @@ function updateAggregationDepthOptions(maxDepth, context) { } function applyDefaultPackageFilterIfPresent(context) { - const input = document.getElementById('package-filter-input'); + const input = dom.getPackageFilterInput(); if (!input || input.value.trim()) return false; const {packages} = getPackageSummaryData(context); const domainRoots = packages @@ -935,8 +949,8 @@ function applyDefaultPackageFilterIfPresent(context) { } function setupRelatedFilterControls(context) { - const select = document.getElementById('related-mode-select'); - const clearButton = document.getElementById('clear-related-filter'); + const select = dom.getRelatedModeSelect(); + const clearButton = dom.getClearRelatedFilterButton(); if (!select) return; select.value = context.relatedFilterMode; select.addEventListener('change', () => { @@ -948,7 +962,7 @@ function setupRelatedFilterControls(context) { if (clearButton) { clearButton.addEventListener('click', () => { context.relatedFilterFqn = null; - context.packageFilterFqn = document.getElementById('package-filter-input')?.value.trim() || null; + context.packageFilterFqn = dom.getPackageFilterInput()?.value.trim() || null; renderDiagramAndTable(context); renderRelatedFilterTarget(context); }); @@ -956,7 +970,7 @@ function setupRelatedFilterControls(context) { } function setupDiagramDirectionControls(context) { - const radios = document.querySelectorAll('input[name="diagram-direction"]'); + const radios = dom.getDiagramDirectionRadios(); radios.forEach(radio => { if (radio.value === context.diagramDirection) { radio.checked = true; @@ -970,7 +984,7 @@ function setupDiagramDirectionControls(context) { } function setupTransitiveReductionControl(context) { - const container = document.querySelector('input[name="diagram-direction"]')?.parentNode?.parentNode; + const container = dom.getDiagramDirectionRadio()?.parentNode?.parentNode; if (!container) return; const controlContainer = document.createElement('div'); @@ -995,7 +1009,8 @@ function setupTransitiveReductionControl(context) { if (typeof document !== 'undefined') { document.addEventListener("DOMContentLoaded", function () { - if (!document.body.classList.contains("package-list")) return; + const body = dom.getDocumentBody(); + if (!body || !body.classList.contains("package-list")) return; setupSortableTables(); renderPackageTable(packageContext); setupPackageFilterControls(packageContext); From ee231f497aa45e5d1b29fb7080e9bebae7eaa708 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:19:10 +0900 Subject: [PATCH 13/51] Extract package data parser --- .../main/resources/templates/assets/package.js | 9 +++++++-- jig-core/src/test/js/package.test.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index cb6b01f92..d1de4b6c7 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -133,14 +133,18 @@ function renderDiagramSvg(text, maxEdges, context) { function getPackageSummaryData(context) { if (context.packageSummaryCache) return context.packageSummaryCache; const jsonText = dom.getNodeTextContent(dom.getPackageDataScript()); + context.packageSummaryCache = parsePackageSummaryData(jsonText); + return context.packageSummaryCache; +} + +function parsePackageSummaryData(jsonText) { /** @type {{packages?: Array<{fqn: string, name: string, classCount: number, description: string}>, relations?: Array<{from: string, to: string}>, causeRelationEvidence?: Array<{from: string, to: string}>} | Array<{fqn: string, name: string, classCount: number, description: string}>} */ const packageData = JSON.parse(jsonText); - context.packageSummaryCache = { + return { packages: Array.isArray(packageData) ? packageData : (packageData.packages ?? []), relations: Array.isArray(packageData) ? [] : (packageData.relations ?? []), causeRelationEvidence: Array.isArray(packageData) ? [] : (packageData.causeRelationEvidence ?? []), }; - return context.packageSummaryCache; } function getPackageDepth(fqn) { @@ -1049,6 +1053,7 @@ if (typeof module !== 'undefined' && module.exports) { showDiagramErrorMessage, hideDiagramErrorMessage, renderDiagramSvg, + parsePackageSummaryData, renderPackageTable, buildPackageTableRows, applyPackageFilterToTable, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 3703457af..b555ec869 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -317,6 +317,21 @@ test.describe('package.js', () => { assert.deepEqual(pkg.dom.getNodeTextContent.mock.calls[0].arguments, [mockPackageDataElement]); }); + test('parsePackageSummaryData: 配列/オブジェクト両対応', () => { + const arrayData = pkg.parsePackageSummaryData(JSON.stringify([ + {fqn: 'app.a', name: 'A', classCount: 1, description: ''}, + ])); + assert.equal(arrayData.packages.length, 1); + assert.equal(arrayData.relations.length, 0); + + const objectData = pkg.parsePackageSummaryData(JSON.stringify({ + packages: [{fqn: 'app.b', name: 'B', classCount: 2, description: ''}], + relations: [{from: 'app.b', to: 'app.c'}], + })); + assert.equal(objectData.packages.length, 1); + assert.equal(objectData.relations.length, 1); + }); + test('getPackageDepth: 深さを返す', () => { assert.equal(pkg.getPackageDepth(''), 0); assert.equal(pkg.getPackageDepth('(default)'), 0); From b4a368f5676f40f23c49fae47b11a64756ba69fe Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:25:42 +0900 Subject: [PATCH 14/51] Extract related table visibility logic --- .../resources/templates/assets/package.js | 50 ++++++++++++------- jig-core/src/test/js/package.test.js | 27 ++++++++++ 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index d1de4b6c7..946009a0f 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -367,32 +367,43 @@ function applyPackageFilterToTable(packageFilterFqn) { function applyRelatedFilterToTable(fqn, context) { const rows = dom.getPackageTableRows(); - const packageFilterPrefix = context.packageFilterFqn ? `${context.packageFilterFqn}.` : null; + const {relations} = getPackageSummaryData(context); + const rowFqns = Array.from(rows, row => { + const fqnCell = row.querySelector('td.fqn'); + return fqnCell ? fqnCell.textContent : ''; + }); + const visibility = buildRelatedRowVisibility( + rowFqns, + relations, + context.packageFilterFqn, + context.aggregationDepth, + context.relatedFilterMode, + fqn + ); + rows.forEach((row, index) => { + row.classList.toggle('hidden', !visibility[index]); + }); +} + +function buildRelatedRowVisibility(rowFqns, relations, packageFilterFqn, aggregationDepth, relatedFilterMode, relatedFilterFqn) { + const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; const withinPackageFilter = rowFqn => - !context.packageFilterFqn || rowFqn === context.packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); + !packageFilterFqn || rowFqn === packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); - if (!fqn) { - rows.forEach(row => { - const fqnCell = row.querySelector('td.fqn'); - const rowFqn = fqnCell ? fqnCell.textContent : ''; - row.classList.toggle('hidden', !withinPackageFilter(rowFqn)); - }); - return; + if (!relatedFilterFqn) { + return rowFqns.map(rowFqn => withinPackageFilter(rowFqn)); } - const {relations} = getPackageSummaryData(context); - const filteredRelations = context.packageFilterFqn + + const filteredRelations = packageFilterFqn ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to) ) : relations; - const aggregatedRoot = getAggregatedFqn(fqn, context.aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, context.aggregationDepth, context.relatedFilterMode); - rows.forEach(row => { - const fqnCell = row.querySelector('td.fqn'); - const rowFqn = fqnCell ? fqnCell.textContent : ''; - const aggregatedRow = getAggregatedFqn(rowFqn, context.aggregationDepth); - const visible = withinPackageFilter(rowFqn) && relatedSet.has(aggregatedRow); - row.classList.toggle('hidden', !visible); + const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, aggregationDepth, relatedFilterMode); + return rowFqns.map(rowFqn => { + const aggregatedRow = getAggregatedFqn(rowFqn, aggregationDepth); + return withinPackageFilter(rowFqn) && relatedSet.has(aggregatedRow); }); } @@ -1049,6 +1060,7 @@ if (typeof module !== 'undefined' && module.exports) { buildAggregationStatsForFilters, buildAggregationStatsForPackageFilter, buildAggregationStatsForRelated, + buildRelatedRowVisibility, getOrCreateDiagramErrorBox, showDiagramErrorMessage, hideDiagramErrorMessage, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index b555ec869..09407e00d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -534,6 +534,33 @@ test.describe('package.js', () => { assert.equal(rows[1].classList.contains('hidden'), false); assert.equal(rows[2].classList.contains('hidden'), true); }); + + test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { + const rowFqns = ['app.domain', 'app.other']; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + [], + 'app.domain', + 0, + 'direct', + null + ); + assert.deepEqual(visibility, [true, false]); + }); + + test('buildRelatedRowVisibility: 関係する行のみ表示', () => { + const rowFqns = ['app.a', 'app.b', 'app.c']; + const relations = [{from: 'app.a', to: 'app.b'}]; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + relations, + null, + 0, + 'direct', + 'app.a' + ); + assert.deepEqual(visibility, [true, true, false]); + }); }); }); From 169152a9bb2bcc282aa260bc667c2e65333f596e Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:30:17 +0900 Subject: [PATCH 15/51] Extract package table filter visibility --- .../resources/templates/assets/package.js | 19 ++++++++++++++----- jig-core/src/test/js/package.test.js | 8 ++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 946009a0f..d8426646f 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -356,15 +356,23 @@ function buildPackageTableRows(packages, relations) { function applyPackageFilterToTable(packageFilterFqn) { const rows = dom.getPackageTableRows(); - const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; - rows.forEach(row => { + const rowFqns = Array.from(rows, row => { const fqnCell = row.querySelector('td.fqn'); - const fqn = fqnCell ? fqnCell.textContent : ''; - const visible = !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(filterPrefix); - row.classList.toggle('hidden', !visible); + return fqnCell ? fqnCell.textContent : ''; + }); + const visibility = buildPackageRowVisibility(rowFqns, packageFilterFqn); + rows.forEach((row, index) => { + row.classList.toggle('hidden', !visibility[index]); }); } +function buildPackageRowVisibility(rowFqns, packageFilterFqn) { + const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + return rowFqns.map(fqn => + !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(filterPrefix) + ); +} + function applyRelatedFilterToTable(fqn, context) { const rows = dom.getPackageTableRows(); const {relations} = getPackageSummaryData(context); @@ -1060,6 +1068,7 @@ if (typeof module !== 'undefined' && module.exports) { buildAggregationStatsForFilters, buildAggregationStatsForPackageFilter, buildAggregationStatsForRelated, + buildPackageRowVisibility, buildRelatedRowVisibility, getOrCreateDiagramErrorBox, showDiagramErrorMessage, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 09407e00d..9ede603f5 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -500,6 +500,14 @@ test.describe('package.js', () => { assert.equal(rows[1].classList.contains('hidden'), true); }); + test('buildPackageRowVisibility: パッケージフィルタのみ', () => { + const visibility = pkg.buildPackageRowVisibility( + ['app.domain', 'app.other'], + 'app.domain' + ); + assert.deepEqual(visibility, [true, false]); + }); + test('applyRelatedFilterToTable: 未指定ならパッケージフィルタのみ', () => { const doc = setupDocument(); const rows = buildPackageRows(doc, ['app.domain', 'app.other']); From 8daa24fe738420339f3be107dc9d9075b416ece5 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:31:15 +0900 Subject: [PATCH 16/51] Extract package filter candidate finder --- .../resources/templates/assets/package.js | 19 ++++++++++++------- jig-core/src/test/js/package.test.js | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index d8426646f..dd7fca353 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -949,6 +949,15 @@ function applyDefaultPackageFilterIfPresent(context) { const input = dom.getPackageFilterInput(); if (!input || input.value.trim()) return false; const {packages} = getPackageSummaryData(context); + const candidate = findDefaultPackageFilterCandidate(packages); + if (!candidate) return false; + input.value = candidate; + context.packageFilterFqn = candidate; + renderDiagramAndTable(context); + return true; +} + +function findDefaultPackageFilterCandidate(packages) { const domainRoots = packages .map(item => item.fqn) .map(fqn => { @@ -958,17 +967,12 @@ function applyDefaultPackageFilterIfPresent(context) { return parts.slice(0, domainIndex + 1).join('.'); }) .filter(Boolean); - if (domainRoots.length === 0) return false; - const candidate = domainRoots.reduce((best, current) => { + if (domainRoots.length === 0) return null; + return domainRoots.reduce((best, current) => { const bestDepth = best.split('.').length; const currentDepth = current.split('.').length; return currentDepth < bestDepth ? current : best; }); - if (!candidate) return false; - input.value = candidate; - context.packageFilterFqn = candidate; - renderDiagramAndTable(context); - return true; } function setupRelatedFilterControls(context) { @@ -1089,6 +1093,7 @@ if (typeof module !== 'undefined' && module.exports) { setupAggregationDepthControl, updateAggregationDepthOptions, applyDefaultPackageFilterIfPresent, + findDefaultPackageFilterCandidate, setupRelatedFilterControls, setupDiagramDirectionControls, setupTransitiveReductionControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 9ede603f5..ee5aa37bd 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -779,6 +779,14 @@ test.describe('package.js', () => { assert.equal(input.value, 'app.domain'); }); + test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { + const candidate = pkg.findDefaultPackageFilterCandidate([ + {fqn: 'app.domain.core'}, + {fqn: 'app.domain.sub'}, + ]); + assert.equal(candidate, 'app.domain'); + }); + test('applyDefaultPackageFilterIfPresent: 入力済みなら適用しない', () => { const doc = setupDocument(); setPackageData({ From f8a353f07464c411ead12f58e296f7c41810685d Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:32:54 +0900 Subject: [PATCH 17/51] Extract aggregation depth option builders --- .../resources/templates/assets/package.js | 41 +++++++++++++------ jig-core/src/test/js/package.test.js | 31 ++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index dd7fca353..13408102c 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -915,8 +915,7 @@ function updateAggregationDepthOptions(maxDepth, context) { const select = dom.getDepthSelect(); if (!select) return; const {packages, relations} = getPackageSummaryData(context); - let aggregationStats; - aggregationStats = buildAggregationStatsForFilters( + const aggregationStats = buildAggregationStatsForFilters( packages, relations, context.packageFilterFqn, @@ -925,23 +924,39 @@ function updateAggregationDepthOptions(maxDepth, context) { context.aggregationDepth, context.relatedFilterMode ); - select.innerHTML = ''; - const noAggregationOption = document.createElement('option'); - noAggregationOption.value = '0'; + const options = buildAggregationDepthOptions(aggregationStats, maxDepth); + renderAggregationDepthOptions(select, options, context.aggregationDepth, maxDepth); +} + +function buildAggregationDepthOptions(aggregationStats, maxDepth) { + const options = []; const noAggregationStats = aggregationStats.get(0); - noAggregationOption.textContent = `集約なし(P${noAggregationStats.packageCount} / R${noAggregationStats.relationCount})`; - select.appendChild(noAggregationOption); + options.push({ + value: '0', + text: `集約なし(P${noAggregationStats.packageCount} / R${noAggregationStats.relationCount})`, + }); for (let depth = 1; depth <= maxDepth; depth += 1) { - const option = document.createElement('option'); - option.value = String(depth); const stats = aggregationStats.get(depth); if (!stats || stats.relationCount === 0) { continue; } - option.textContent = `深さ${depth}(P${stats.packageCount} / R${stats.relationCount})`; - select.appendChild(option); + options.push({ + value: String(depth), + text: `深さ${depth}(P${stats.packageCount} / R${stats.relationCount})`, + }); } - const value = Math.min(context.aggregationDepth, maxDepth); + return options; +} + +function renderAggregationDepthOptions(select, options, aggregationDepth, maxDepth) { + select.innerHTML = ''; + options.forEach(option => { + const node = document.createElement('option'); + node.value = option.value; + node.textContent = option.text; + select.appendChild(node); + }); + const value = Math.min(aggregationDepth, maxDepth); select.value = String(value); } @@ -1092,6 +1107,8 @@ if (typeof module !== 'undefined' && module.exports) { setupPackageFilterControls, setupAggregationDepthControl, updateAggregationDepthOptions, + buildAggregationDepthOptions, + renderAggregationDepthOptions, applyDefaultPackageFilterIfPresent, findDefaultPackageFilterCandidate, setupRelatedFilterControls, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index ee5aa37bd..3e7911853 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -618,6 +618,37 @@ test.describe('package.js', () => { assert.equal(select.children[0].textContent.includes('集約なし'), true); assert.equal(select.value, '1'); }); + + test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { + const stats = new Map([ + [0, {packageCount: 2, relationCount: 1}], + [1, {packageCount: 1, relationCount: 1}], + [2, {packageCount: 1, relationCount: 0}], + ]); + + const options = pkg.buildAggregationDepthOptions(stats, 2); + + assert.deepEqual(options, [ + {value: '0', text: '集約なし(P2 / R1)'}, + {value: '1', text: '深さ1(P1 / R1)'}, + ]); + }); + + test('renderAggregationDepthOptions: セレクトを更新する', () => { + const doc = setupDocument(); + const select = new Element('select'); + doc.elementsById.set('package-depth-select', select); + const options = [ + {value: '0', text: '集約なし(P2 / R1)'}, + {value: '1', text: '深さ1(P1 / R1)'}, + ]; + + pkg.renderAggregationDepthOptions(select, options, 1, 2); + + assert.equal(select.children.length, 2); + assert.equal(select.children[0].textContent, '集約なし(P2 / R1)'); + assert.equal(select.value, '1'); + }); }); test.describe('一覧/補助', () => { From e9d39e7efebf9ea1659c8e4901fc1c9452a67e95 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:34:44 +0900 Subject: [PATCH 18/51] Extract diagram relation filtering --- .../resources/templates/assets/package.js | 47 +++++++++++++++---- jig-core/src/test/js/package.test.js | 19 ++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 13408102c..463337aef 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -745,7 +745,30 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr } function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { + const base = buildFilteredDiagramRelations( + packages, + relations, + causeRelationEvidence, + packageFilterFqn, + aggregationDepth, + transitiveReductionEnabled + ); const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; + const {uniqueRelations, visibleSet} = applyRelatedFilterToDiagramRelations( + base.uniqueRelations, + base.visibleSet, + aggregatedRoot, + aggregationDepth, + relatedFilterMode + ); + return { + uniqueRelations, + visibleSet, + filteredCauseRelationEvidence: base.filteredCauseRelationEvidence, + }; +} + +function buildFilteredDiagramRelations(packages, relations, causeRelationEvidence, packageFilterFqn, aggregationDepth, transitiveReductionEnabled) { const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; const withinPackageFilter = fqn => !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); @@ -779,25 +802,31 @@ function getVisibleDiagramElements(packages, relations, causeRelationEvidence, p uniqueRelations = transitiveReduction(uniqueRelations); } + return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; +} + +function applyRelatedFilterToDiagramRelations(uniqueRelations, visibleSet, aggregatedRoot, aggregationDepth, relatedFilterMode) { + const nextVisibleSet = new Set(visibleSet); + let nextRelations = uniqueRelations; if (aggregatedRoot) { const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); if (relatedFilterMode === 'direct') { - uniqueRelations = uniqueRelations.filter(relation => + nextRelations = uniqueRelations.filter(relation => relation.from === aggregatedRoot || relation.to === aggregatedRoot ); } else { - uniqueRelations = uniqueRelations.filter(relation => + nextRelations = uniqueRelations.filter(relation => relatedSet.has(relation.from) && relatedSet.has(relation.to) ); } - visibleSet.clear(); - relatedSet.forEach(value => visibleSet.add(value)); + nextVisibleSet.clear(); + relatedSet.forEach(value => nextVisibleSet.add(value)); } - uniqueRelations.forEach(relation => { - visibleSet.add(relation.from); - visibleSet.add(relation.to); + nextRelations.forEach(relation => { + nextVisibleSet.add(relation.from); + nextVisibleSet.add(relation.to); }); - return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; + return {uniqueRelations: nextRelations, visibleSet: nextVisibleSet}; } function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { @@ -1077,6 +1106,8 @@ if (typeof module !== 'undefined' && module.exports) { // private getVisibleDiagramElements, + buildFilteredDiagramRelations, + applyRelatedFilterToDiagramRelations, getAggregatedFqn, collectRelatedSet, getPackageSummaryData, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 3e7911853..9c963ca6c 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -487,6 +487,25 @@ test.describe('package.js', () => { const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); }); + + test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); + assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(base.uniqueRelations.length, 2); + }); + + test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); + const filtered = pkg.applyRelatedFilterToDiagramRelations( + base.uniqueRelations, + base.visibleSet, + 'app.b', + 0, + 'direct' + ); + assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(filtered.uniqueRelations.length, 2); + }); }); test.describe('テーブル', () => { From b8ad66a47b6219e6810d13dfd505e8bced83e6a6 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:35:33 +0900 Subject: [PATCH 19/51] Normalize package filter input --- .../src/main/resources/templates/assets/package.js | 11 ++++++++--- jig-core/src/test/js/package.test.js | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 463337aef..97d766748 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -903,8 +903,7 @@ function setupPackageFilterControls(context) { if (!input || !applyButton || !clearPackageButton) return; const applyFilter = () => { - const value = input.value.trim(); - context.packageFilterFqn = value || null; + context.packageFilterFqn = normalizePackageFilterValue(input.value); renderDiagramAndTable(context); renderRelatedFilterTarget(context); }; @@ -1033,13 +1032,18 @@ function setupRelatedFilterControls(context) { if (clearButton) { clearButton.addEventListener('click', () => { context.relatedFilterFqn = null; - context.packageFilterFqn = dom.getPackageFilterInput()?.value.trim() || null; + context.packageFilterFqn = normalizePackageFilterValue(dom.getPackageFilterInput()?.value); renderDiagramAndTable(context); renderRelatedFilterTarget(context); }); } } +function normalizePackageFilterValue(value) { + const trimmed = (value ?? '').trim(); + return trimmed ? trimmed : null; +} + function setupDiagramDirectionControls(context) { const radios = dom.getDiagramDirectionRadios(); radios.forEach(radio => { @@ -1142,6 +1146,7 @@ if (typeof module !== 'undefined' && module.exports) { renderAggregationDepthOptions, applyDefaultPackageFilterIfPresent, findDefaultPackageFilterCandidate, + normalizePackageFilterValue, setupRelatedFilterControls, setupDiagramDirectionControls, setupTransitiveReductionControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 9c963ca6c..5c91c446a 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -852,6 +852,12 @@ test.describe('package.js', () => { assert.equal(applied, false); }); + + test('normalizePackageFilterValue: 空文字はnull', () => { + assert.equal(pkg.normalizePackageFilterValue(''), null); + assert.equal(pkg.normalizePackageFilterValue(' '), null); + assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); + }); }); }); From ab684944a5c1d83a378bd79f5d200f98aea79d57 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:48:07 +0900 Subject: [PATCH 20/51] Extract diagram node/edge builders --- .../resources/templates/assets/package.js | 42 +++++++++++++++---- jig-core/src/test/js/package.test.js | 10 +++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 97d766748..5bf90e1c6 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -638,6 +638,27 @@ function buildParentFqns(visibleSet) { function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagramDirection) { const escapeMermaidText = text => text.replace(/"/g, '\\"'); const lines = [`graph ${diagramDirection}`]; + const {nodeIdByFqn, nodeIdToFqn, nodeLabelById, ensureNodeId} = buildDiagramNodeMaps(visibleSet, nameByFqn); + const {edgeLines, linkStyles, mutualPairs} = buildDiagramEdgeLines(uniqueRelations, ensureNodeId); + const {nodeLines, hasParentStyle} = buildDiagramNodeLines( + visibleSet, + nodeIdByFqn, + nodeIdToFqn, + nodeLabelById, + escapeMermaidText + ); + + nodeLines.forEach(line => lines.push(line)); + if (hasParentStyle) { + lines.push('classDef parentPackage fill:#ffffde,stroke:#aaaa00,stroke-width:2px'); + } + edgeLines.forEach(line => lines.push(line)); + linkStyles.forEach(styleLine => lines.push(styleLine)); + + return {source: lines.join('\n'), nodeIdToFqn, mutualPairs}; +} + +function buildDiagramNodeMaps(visibleSet, nameByFqn) { const nodeIdByFqn = new Map(); const nodeIdToFqn = new Map(); const nodeLabelById = new Map(); @@ -651,10 +672,12 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr nodeLabelById.set(nodeId, label); return nodeId; }; - Array.from(visibleSet).sort().forEach(ensureNodeId); - const mutualPairs = buildMutualPairs(uniqueRelations); + return {nodeIdByFqn, nodeIdToFqn, nodeLabelById, ensureNodeId}; +} +function buildDiagramEdgeLines(uniqueRelations, ensureNodeId) { + const mutualPairs = buildMutualPairs(uniqueRelations); const linkStyles = []; let linkIndex = 0; const edgeLines = []; @@ -676,9 +699,13 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr edgeLines.push(`${fromId} --> ${toId}`); linkIndex += 1; }); + return {edgeLines, linkStyles, mutualPairs}; +} +function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelById, escapeMermaidText) { const visibleFqns = Array.from(visibleSet).sort(); const parentFqns = buildParentFqns(visibleSet); + const lines = []; const addNodeLines = (nodeId, parentSubgraphFqn) => { const fqn = nodeIdToFqn.get(nodeId); @@ -734,14 +761,8 @@ function buildMermaidDiagramSource(visibleSet, uniqueRelations, nameByFqn, diagr }); }; renderGroup(rootGroup, true, rootGroup.key); - if (parentFqns.size > 0) { - lines.push('classDef parentPackage fill:#ffffde,stroke:#aaaa00,stroke-width:2px'); - } - edgeLines.forEach(line => lines.push(line)); - linkStyles.forEach(styleLine => lines.push(styleLine)); - - return {source: lines.join('\n'), nodeIdToFqn, mutualPairs}; + return {nodeLines: lines, hasParentStyle: parentFqns.size > 0}; } function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { @@ -1138,6 +1159,9 @@ if (typeof module !== 'undefined' && module.exports) { renderMutualDependencyList, renderPackageDiagram, buildMermaidDiagramSource, + buildDiagramNodeMaps, + buildDiagramEdgeLines, + buildDiagramNodeLines, applyRelatedFilter, setupPackageFilterControls, setupAggregationDepthControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 5c91c446a..42c5461d5 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -506,6 +506,16 @@ test.describe('package.js', () => { assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); assert.equal(filtered.uniqueRelations.length, 2); }); + + test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { + const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); + const result = pkg.buildDiagramEdgeLines( + [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], + ensureNodeId + ); + assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); + assert.equal(result.linkStyles.length, 1); + }); }); test.describe('テーブル', () => { From ccd6edf67336f6cf8ff5ec5762c11ee46e2cda74 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:48:28 +0900 Subject: [PATCH 21/51] Normalize aggregation depth input --- jig-core/src/main/resources/templates/assets/package.js | 8 +++++++- jig-core/src/test/js/package.test.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 5bf90e1c6..a0d7db7e9 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -953,7 +953,7 @@ function setupAggregationDepthControl(context) { updateAggregationDepthOptions(maxDepth, context); select.value = String(context.aggregationDepth); select.addEventListener('change', () => { - context.aggregationDepth = Number(select.value); + context.aggregationDepth = normalizeAggregationDepthValue(select.value); renderDiagramAndTable(context); renderRelatedFilterTarget(context); updateAggregationDepthOptions(maxDepth, context); @@ -1065,6 +1065,11 @@ function normalizePackageFilterValue(value) { return trimmed ? trimmed : null; } +function normalizeAggregationDepthValue(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + function setupDiagramDirectionControls(context) { const radios = dom.getDiagramDirectionRadios(); radios.forEach(radio => { @@ -1171,6 +1176,7 @@ if (typeof module !== 'undefined' && module.exports) { applyDefaultPackageFilterIfPresent, findDefaultPackageFilterCandidate, normalizePackageFilterValue, + normalizeAggregationDepthValue, setupRelatedFilterControls, setupDiagramDirectionControls, setupTransitiveReductionControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 42c5461d5..61db3eaf1 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -868,6 +868,12 @@ test.describe('package.js', () => { assert.equal(pkg.normalizePackageFilterValue(' '), null); assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); }); + + test('normalizeAggregationDepthValue: 数値化する', () => { + assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); + assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); + assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); + }); }); }); From fb4a050f47dc325b29c582c9058051594597fb13 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:54:15 +0900 Subject: [PATCH 22/51] Extract diagram group builders --- .../resources/templates/assets/package.js | 26 ++++++++++++----- jig-core/src/test/js/package.test.js | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index a0d7db7e9..8695554b7 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -705,9 +705,8 @@ function buildDiagramEdgeLines(uniqueRelations, ensureNodeId) { function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelById, escapeMermaidText) { const visibleFqns = Array.from(visibleSet).sort(); const parentFqns = buildParentFqns(visibleSet); - const lines = []; - - const addNodeLines = (nodeId, parentSubgraphFqn) => { + const rootGroup = buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + const addNodeLines = (lines, nodeId, parentSubgraphFqn) => { const fqn = nodeIdToFqn.get(nodeId); let displayLabel = nodeLabelById.get(nodeId); @@ -718,13 +717,17 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy const tooltip = fqn ? escapeMermaidText(fqn) : ''; lines.push(`click ${nodeId} filterPackageDiagram "${tooltip}"`); if (fqn && parentFqns.has(fqn)) { - lines.push(`class ${nodeId} parentPackage`); + lines.push(`class ${nodeId} parentPackage`); } }; + const nodeLines = buildSubgraphLines(rootGroup, addNodeLines, escapeMermaidText); + return {nodeLines, hasParentStyle: parentFqns.size > 0}; +} + +function buildDiagramGroupTree(visibleFqns, nodeIdByFqn) { const prefixDepth = getCommonPrefixDepth(visibleFqns); const baseDepth = Math.max(prefixDepth - 1, 0); - let groupIndex = 0; const createGroupNode = key => ({key, children: new Map(), nodes: []}); const rootGroup = createGroupNode(''); visibleFqns.forEach(fqn => { @@ -740,8 +743,14 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy } current.nodes.push(nodeIdByFqn.get(fqn)); }); + return rootGroup; +} + +function buildSubgraphLines(rootGroup, addNodeLines, escapeMermaidText) { + const lines = []; + let groupIndex = 0; const renderGroup = (group, isRoot, parentSubgraphFqnForNodes) => { - group.nodes.forEach(nodeId => addNodeLines(nodeId, parentSubgraphFqnForNodes)); + group.nodes.forEach(nodeId => addNodeLines(lines, nodeId, parentSubgraphFqnForNodes)); const childKeys = Array.from(group.children.keys()).sort(); if (isRoot && group.nodes.length === 0 && childKeys.length === 1) { renderGroup(group.children.get(childKeys[0]), false, parentSubgraphFqnForNodes); @@ -761,8 +770,7 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy }); }; renderGroup(rootGroup, true, rootGroup.key); - - return {nodeLines: lines, hasParentStyle: parentFqns.size > 0}; + return lines; } function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { @@ -1167,6 +1175,8 @@ if (typeof module !== 'undefined' && module.exports) { buildDiagramNodeMaps, buildDiagramEdgeLines, buildDiagramNodeLines, + buildDiagramGroupTree, + buildSubgraphLines, applyRelatedFilter, setupPackageFilterControls, setupAggregationDepthControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 61db3eaf1..d895bbe00 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -516,6 +516,35 @@ test.describe('package.js', () => { assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); assert.equal(result.linkStyles.length, 1); }); + + test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { + const visibleFqns = ['com.example.a', 'com.example.b']; + const nodeIdByFqn = new Map([ + ['com.example.a', 'P0'], + ['com.example.b', 'P1'], + ]); + + const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + + assert.equal(rootGroup.children.has('com.example'), true); + }); + + test('buildSubgraphLines: サブグラフ行を生成する', () => { + const rootGroup = { + key: '', + nodes: [], + children: new Map([ + ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], + ]), + }; + const addNodeLines = (lines, nodeId) => { + lines.push(`node ${nodeId}`); + }; + + const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); + + assert.equal(lines.some(line => line.includes('node P0')), true); + }); }); test.describe('テーブル', () => { From 6895d5db4b16ff05a357f4f0a5616922939fd1ad Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 18:55:08 +0900 Subject: [PATCH 23/51] Extract package table row builders --- .../resources/templates/assets/package.js | 124 ++++++++++-------- jig-core/src/test/js/package.test.js | 16 +++ 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 8695554b7..65fc8a23e 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -267,6 +267,7 @@ function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, function renderPackageTable(context) { const {packages, relations} = getPackageSummaryData(context); const rows = buildPackageTableRows(packages, relations); + const rowSpecs = buildPackageTableRowSpecs(rows); const tbody = dom.getPackageTableBody(); @@ -283,59 +284,8 @@ function renderPackageTable(context) { applyRelatedFilter(fqn, context); }; - rows.forEach(item => { - const tr = document.createElement('tr'); - - const actionTd = document.createElement('td'); - const actionButton = document.createElement('button'); - actionButton.type = 'button'; - actionButton.className = 'package-filter-icon'; - actionButton.setAttribute('aria-label', 'このパッケージで絞り込み'); - const actionText = document.createElement('span'); - actionText.className = 'screen-reader-only'; - actionText.textContent = '絞り込み'; - actionButton.appendChild(actionText); - actionButton.addEventListener('click', () => applyFilter(item.fqn)); - actionTd.appendChild(actionButton); - tr.appendChild(actionTd); - - const relatedTd = document.createElement('td'); - const relatedButton = document.createElement('button'); - relatedButton.type = 'button'; - relatedButton.className = 'related-icon'; - relatedButton.setAttribute('aria-label', '関連のみ表示'); - const relatedText = document.createElement('span'); - relatedText.className = 'screen-reader-only'; - relatedText.textContent = '関連のみ表示'; - relatedButton.appendChild(relatedText); - relatedButton.addEventListener('click', () => applyRelatedFilterForRow(item.fqn)); - relatedTd.appendChild(relatedButton); - tr.appendChild(relatedTd); - - const fqnTd = document.createElement('td'); - fqnTd.textContent = item.fqn; - fqnTd.className = 'fqn'; - tr.appendChild(fqnTd); - - const nameTd = document.createElement('td'); - nameTd.textContent = item.name; - tr.appendChild(nameTd); - - const classCountTd = document.createElement('td'); - classCountTd.textContent = String(item.classCount); - classCountTd.className = 'number'; - tr.appendChild(classCountTd); - - const incomingCountTd = document.createElement('td'); - incomingCountTd.textContent = String(item.incomingCount ?? 0); - incomingCountTd.className = 'number'; - tr.appendChild(incomingCountTd); - - const outgoingCountTd = document.createElement('td'); - outgoingCountTd.textContent = String(item.outgoingCount ?? 0); - outgoingCountTd.className = 'number'; - tr.appendChild(outgoingCountTd); - + rowSpecs.forEach(spec => { + const tr = createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow); tbody.appendChild(tr); }); } @@ -354,6 +304,72 @@ function buildPackageTableRows(packages, relations) { })); } +function buildPackageTableRowSpecs(rows) { + return rows.map(item => ({ + fqn: item.fqn, + name: item.name, + classCount: item.classCount, + incomingCount: item.incomingCount ?? 0, + outgoingCount: item.outgoingCount ?? 0, + })); +} + +function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { + const tr = document.createElement('tr'); + + const actionTd = document.createElement('td'); + const actionButton = document.createElement('button'); + actionButton.type = 'button'; + actionButton.className = 'package-filter-icon'; + actionButton.setAttribute('aria-label', 'このパッケージで絞り込み'); + const actionText = document.createElement('span'); + actionText.className = 'screen-reader-only'; + actionText.textContent = '絞り込み'; + actionButton.appendChild(actionText); + actionButton.addEventListener('click', () => applyFilter(spec.fqn)); + actionTd.appendChild(actionButton); + tr.appendChild(actionTd); + + const relatedTd = document.createElement('td'); + const relatedButton = document.createElement('button'); + relatedButton.type = 'button'; + relatedButton.className = 'related-icon'; + relatedButton.setAttribute('aria-label', '関連のみ表示'); + const relatedText = document.createElement('span'); + relatedText.className = 'screen-reader-only'; + relatedText.textContent = '関連のみ表示'; + relatedButton.appendChild(relatedText); + relatedButton.addEventListener('click', () => applyRelatedFilterForRow(spec.fqn)); + relatedTd.appendChild(relatedButton); + tr.appendChild(relatedTd); + + const fqnTd = document.createElement('td'); + fqnTd.textContent = spec.fqn; + fqnTd.className = 'fqn'; + tr.appendChild(fqnTd); + + const nameTd = document.createElement('td'); + nameTd.textContent = spec.name; + tr.appendChild(nameTd); + + const classCountTd = document.createElement('td'); + classCountTd.textContent = String(spec.classCount); + classCountTd.className = 'number'; + tr.appendChild(classCountTd); + + const incomingCountTd = document.createElement('td'); + incomingCountTd.textContent = String(spec.incomingCount ?? 0); + incomingCountTd.className = 'number'; + tr.appendChild(incomingCountTd); + + const outgoingCountTd = document.createElement('td'); + outgoingCountTd.textContent = String(spec.outgoingCount ?? 0); + outgoingCountTd.className = 'number'; + tr.appendChild(outgoingCountTd); + + return tr; +} + function applyPackageFilterToTable(packageFilterFqn) { const rows = dom.getPackageTableRows(); const rowFqns = Array.from(rows, row => { @@ -1165,6 +1181,8 @@ if (typeof module !== 'undefined' && module.exports) { parsePackageSummaryData, renderPackageTable, buildPackageTableRows, + buildPackageTableRowSpecs, + createPackageTableRow, applyPackageFilterToTable, applyRelatedFilterToTable, renderRelatedFilterTarget, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index d895bbe00..e82201bbb 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -734,6 +734,22 @@ test.describe('package.js', () => { assert.equal(tbody.children[0].children[6].textContent, '2'); }); + test('buildPackageTableRowSpecs: 行データを整形する', () => { + const rows = [ + {fqn: 'app.a', name: 'A', classCount: 2, incomingCount: 0, outgoingCount: 1}, + ]; + + const specs = pkg.buildPackageTableRowSpecs(rows); + + assert.deepEqual(specs, [{ + fqn: 'app.a', + name: 'A', + classCount: 2, + incomingCount: 0, + outgoingCount: 1, + }]); + }); + test('getOrCreateDiagramErrorBox: エラーボックスを作成/再利用する', () => { const diagram = { parentNode: { insertBefore: test.mock.fn() } }; // Minimal mock for diagram From c78a60656872033a19b76a8b6932fb40ee9f9310 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:07:23 +0900 Subject: [PATCH 24/51] Extract diagram label helpers --- .../resources/templates/assets/package.js | 24 +++++++++++++------ jig-core/src/test/js/package.test.js | 14 +++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 65fc8a23e..d07ba65f9 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -724,16 +724,12 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy const rootGroup = buildDiagramGroupTree(visibleFqns, nodeIdByFqn); const addNodeLines = (lines, nodeId, parentSubgraphFqn) => { const fqn = nodeIdToFqn.get(nodeId); - let displayLabel = nodeLabelById.get(nodeId); - - if (displayLabel === fqn && parentSubgraphFqn && fqn.startsWith(`${parentSubgraphFqn}.`)) { - displayLabel = fqn.substring(parentSubgraphFqn.length + 1); - } + const displayLabel = buildDiagramNodeLabel(nodeLabelById.get(nodeId), fqn, parentSubgraphFqn); lines.push(`${nodeId}["${escapeMermaidText(displayLabel)}"]`); - const tooltip = fqn ? escapeMermaidText(fqn) : ''; + const tooltip = escapeMermaidText(buildDiagramNodeTooltip(fqn)); lines.push(`click ${nodeId} filterPackageDiagram "${tooltip}"`); if (fqn && parentFqns.has(fqn)) { - lines.push(`class ${nodeId} parentPackage`); + lines.push(`class ${nodeId} parentPackage`); } }; const nodeLines = buildSubgraphLines(rootGroup, addNodeLines, escapeMermaidText); @@ -741,6 +737,18 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy return {nodeLines, hasParentStyle: parentFqns.size > 0}; } +function buildDiagramNodeLabel(displayLabel, fqn, parentSubgraphFqn) { + if (!fqn) return displayLabel ?? ''; + if (displayLabel === fqn && parentSubgraphFqn && fqn.startsWith(`${parentSubgraphFqn}.`)) { + return fqn.substring(parentSubgraphFqn.length + 1); + } + return displayLabel ?? ''; +} + +function buildDiagramNodeTooltip(fqn) { + return fqn ?? ''; +} + function buildDiagramGroupTree(visibleFqns, nodeIdByFqn) { const prefixDepth = getCommonPrefixDepth(visibleFqns); const baseDepth = Math.max(prefixDepth - 1, 0); @@ -1195,6 +1203,8 @@ if (typeof module !== 'undefined' && module.exports) { buildDiagramNodeLines, buildDiagramGroupTree, buildSubgraphLines, + buildDiagramNodeLabel, + buildDiagramNodeTooltip, applyRelatedFilter, setupPackageFilterControls, setupAggregationDepthControl, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index e82201bbb..380ba0dfd 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -545,6 +545,20 @@ test.describe('package.js', () => { assert.equal(lines.some(line => line.includes('node P0')), true); }); + + test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { + const label = pkg.buildDiagramNodeLabel( + 'com.example.domain.model', + 'com.example.domain.model', + 'com.example.domain' + ); + assert.equal(label, 'model'); + }); + + test('buildDiagramNodeTooltip: FQNを返す', () => { + assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); + assert.equal(pkg.buildDiagramNodeTooltip(null), ''); + }); }); test.describe('テーブル', () => { From 0e03957de88c02755500c0a517f19f37712bb3d2 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:08:55 +0900 Subject: [PATCH 25/51] Extract mutual dependency items --- .../resources/templates/assets/package.js | 58 +++++++++++-------- jig-core/src/test/js/package.test.js | 17 +++++- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index d07ba65f9..692105f17 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -487,43 +487,30 @@ function renderDiagramAndTable(context) { function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) { const container = dom.getMutualDependencyList(); if (!container) return; - if (!mutualPairs || mutualPairs.size === 0) { + const items = buildMutualDependencyItems(mutualPairs, causeRelationEvidence, context.aggregationDepth); + if (items.length === 0) { container.style.display = 'none'; container.innerHTML = ''; return; } - const relationMap = new Map(); - causeRelationEvidence.forEach(relation => { - const fromPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.from), context.aggregationDepth); - const toPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.to), context.aggregationDepth); - if (fromPackage === toPackage) return; - const key = fromPackage < toPackage ? `${fromPackage}::${toPackage}` : `${toPackage}::${fromPackage}`; - if (!relationMap.has(key)) { - relationMap.set(key, new Set()); - } - relationMap.get(key).add(`${relation.from} -> ${relation.to}`); - }); container.style.display = ''; const details = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = '相互依存と原因'; const list = document.createElement('ul'); - Array.from(mutualPairs).sort().forEach(key => { - const parts = key.split('::'); - const pairLabel = `${parts[0]} <-> ${parts[1]}`; - const item = document.createElement('li'); + items.forEach(item => { + const itemNode = document.createElement('li'); const pair = document.createElement('div'); pair.className = 'pair'; - pair.textContent = pairLabel; - item.appendChild(pair); - const causes = relationMap.get(key); - if (causes && causes.size > 0) { + pair.textContent = item.pairLabel; + itemNode.appendChild(pair); + if (item.causes.length > 0) { const detailBody = document.createElement('pre'); - detailBody.textContent = Array.from(causes).sort().join('\n'); - item.appendChild(detailBody); + detailBody.textContent = item.causes.join('\n'); + itemNode.appendChild(detailBody); } - list.appendChild(item); + list.appendChild(itemNode); }); container.innerHTML = ''; details.appendChild(summary); @@ -531,6 +518,30 @@ function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) container.appendChild(details); } +function buildMutualDependencyItems(mutualPairs, causeRelationEvidence, aggregationDepth) { + if (!mutualPairs || mutualPairs.size === 0) return []; + const relationMap = new Map(); + causeRelationEvidence.forEach(relation => { + const fromPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.from), aggregationDepth); + const toPackage = getAggregatedFqn(getPackageFqnFromTypeFqn(relation.to), aggregationDepth); + if (fromPackage === toPackage) return; + const key = fromPackage < toPackage ? `${fromPackage}::${toPackage}` : `${toPackage}::${fromPackage}`; + if (!relationMap.has(key)) { + relationMap.set(key, new Set()); + } + relationMap.get(key).add(`${relation.from} -> ${relation.to}`); + }); + return Array.from(mutualPairs).sort().map(key => { + const parts = key.split('::'); + const pairLabel = `${parts[0]} <-> ${parts[1]}`; + const causes = relationMap.get(key); + return { + pairLabel, + causes: causes ? Array.from(causes).sort() : [], + }; + }); +} + function detectStronglyConnectedComponents(graph) { const indices = new Map(); const lowLink = new Map(); @@ -1196,6 +1207,7 @@ if (typeof module !== 'undefined' && module.exports) { renderRelatedFilterTarget, renderDiagramAndTable, renderMutualDependencyList, + buildMutualDependencyItems, renderPackageDiagram, buildMermaidDiagramSource, buildDiagramNodeMaps, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 380ba0dfd..25fdc11e0 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -967,7 +967,22 @@ test.describe('package.js', () => { assert.equal(container.style.display, ''); assert.equal(container.children.length, 1); assert.equal(container.children[0].tagName, 'details'); - assert.equal(container.children[0].children[1].tagName, 'ul'); + assert.equal(container.children[0].children[1].tagName, 'ul'); + }); + + test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { + const items = pkg.buildMutualDependencyItems( + new Set(['app.alpha::app.beta']), + [ + {from: 'app.alpha.A', to: 'app.beta.B'}, + {from: 'app.beta.B', to: 'app.alpha.A'}, + ], + 0 + ); + + assert.equal(items.length, 1); + assert.equal(items[0].pairLabel, 'app.alpha <-> app.beta'); + assert.equal(items[0].causes.length, 2); }); }); From 0d2a7e66acb2edc2f8a662f7a1bb4f8b8d720790 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:10:16 +0900 Subject: [PATCH 26/51] Extract package table action specs --- .../resources/templates/assets/package.js | 23 +++++++++++++++---- jig-core/src/test/js/package.test.js | 9 ++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 692105f17..f9819f85d 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -316,15 +316,16 @@ function buildPackageTableRowSpecs(rows) { function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { const tr = document.createElement('tr'); + const actionSpecs = buildPackageTableActionSpecs(); const actionTd = document.createElement('td'); const actionButton = document.createElement('button'); actionButton.type = 'button'; actionButton.className = 'package-filter-icon'; - actionButton.setAttribute('aria-label', 'このパッケージで絞り込み'); + actionButton.setAttribute('aria-label', actionSpecs.filter.ariaLabel); const actionText = document.createElement('span'); actionText.className = 'screen-reader-only'; - actionText.textContent = '絞り込み'; + actionText.textContent = actionSpecs.filter.screenReaderText; actionButton.appendChild(actionText); actionButton.addEventListener('click', () => applyFilter(spec.fqn)); actionTd.appendChild(actionButton); @@ -334,10 +335,10 @@ function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { const relatedButton = document.createElement('button'); relatedButton.type = 'button'; relatedButton.className = 'related-icon'; - relatedButton.setAttribute('aria-label', '関連のみ表示'); + relatedButton.setAttribute('aria-label', actionSpecs.related.ariaLabel); const relatedText = document.createElement('span'); relatedText.className = 'screen-reader-only'; - relatedText.textContent = '関連のみ表示'; + relatedText.textContent = actionSpecs.related.screenReaderText; relatedButton.appendChild(relatedText); relatedButton.addEventListener('click', () => applyRelatedFilterForRow(spec.fqn)); relatedTd.appendChild(relatedButton); @@ -370,6 +371,19 @@ function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { return tr; } +function buildPackageTableActionSpecs() { + return { + filter: { + ariaLabel: 'このパッケージで絞り込み', + screenReaderText: '絞り込み', + }, + related: { + ariaLabel: '関連のみ表示', + screenReaderText: '関連のみ表示', + }, + }; +} + function applyPackageFilterToTable(packageFilterFqn) { const rows = dom.getPackageTableRows(); const rowFqns = Array.from(rows, row => { @@ -1202,6 +1216,7 @@ if (typeof module !== 'undefined' && module.exports) { buildPackageTableRows, buildPackageTableRowSpecs, createPackageTableRow, + buildPackageTableActionSpecs, applyPackageFilterToTable, applyRelatedFilterToTable, renderRelatedFilterTarget, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 25fdc11e0..aff2c5f04 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -764,6 +764,15 @@ test.describe('package.js', () => { }]); }); + test('buildPackageTableActionSpecs: ボタン文言を返す', () => { + const specs = pkg.buildPackageTableActionSpecs(); + + assert.equal(specs.filter.ariaLabel, 'このパッケージで絞り込み'); + assert.equal(specs.filter.screenReaderText, '絞り込み'); + assert.equal(specs.related.ariaLabel, '関連のみ表示'); + assert.equal(specs.related.screenReaderText, '関連のみ表示'); + }); + test('getOrCreateDiagramErrorBox: エラーボックスを作成/再利用する', () => { const diagram = { parentNode: { insertBefore: test.mock.fn() } }; // Minimal mock for diagram From ff47e3b34dff21a2f89bd314c35bb2e652689b31 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:18:05 +0900 Subject: [PATCH 27/51] Centralize dom mocks in tests --- jig-core/src/test/js/package.test.js | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index aff2c5f04..307cfd83f 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -2,6 +2,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const pkg = require('../../main/resources/templates/assets/package.js'); +const originalDom = {...pkg.dom}; // Creates a fresh deep copy of the initial context for each test function createInitialContext() { @@ -172,15 +173,28 @@ function buildPackageRows(doc, fqns) { function setPackageData(data, context) { const mockDataContent = JSON.stringify(data); - const mockDataElement = { textContent: mockDataContent }; - - // Mock the dom helpers that getPackageSummaryData uses - test.mock.method(pkg.dom, 'getPackageDataScript', test.mock.fn(() => mockDataElement)); - test.mock.method(pkg.dom, 'getNodeTextContent', test.mock.fn((el) => el.textContent)); + const doc = global.document; + if (doc && doc.elementsById) { + const dataElement = new Element('script', doc); + dataElement.textContent = mockDataContent; + doc.elementsById.set('package-data', dataElement); + } else { + const mockDataElement = { textContent: mockDataContent }; + test.mock.method(pkg.dom, 'getPackageDataScript', test.mock.fn(() => mockDataElement)); + test.mock.method(pkg.dom, 'getNodeTextContent', test.mock.fn((el) => el.textContent)); + } context.packageSummaryCache = null; // Reset cache } +function setupDomMocks() { + const methods = Object.keys(originalDom); + methods.forEach(name => { + if (typeof originalDom[name] !== 'function') return; + test.mock.method(pkg.dom, name, test.mock.fn((...args) => originalDom[name](...args))); + }); +} + function withConsoleErrorCapture(callback) { const errors = []; const originalError = console.error; @@ -264,6 +278,7 @@ test.describe('package.js', () => { diagramDirection: 'TD', transitiveReductionEnabled: true, }; + setupDomMocks(); }); test.describe('データ/ヘルパー', () => { @@ -300,21 +315,13 @@ test.describe('package.js', () => { test.describe('データ取得', () => { test('getPackageSummaryData: 配列/オブジェクト両対応', () => { - const mockPackageDataContent = JSON.stringify([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}]); - const mockPackageDataElement = { textContent: mockPackageDataContent }; - - test.mock.method(pkg.dom, 'getPackageDataScript', test.mock.fn(() => mockPackageDataElement)); - test.mock.method(pkg.dom, 'getNodeTextContent', test.mock.fn((el) => el.textContent)); - - testContext.packageSummaryCache = null; // Ensure cache is clear before test + setupDocument(); + setPackageData([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); const data = pkg.getPackageSummaryData(testContext); assert.equal(data.packages.length, 1); assert.equal(data.relations.length, 0); - assert.equal(pkg.dom.getPackageDataScript.mock.calls.length, 1); - assert.equal(pkg.dom.getNodeTextContent.mock.calls.length, 1); - assert.deepEqual(pkg.dom.getNodeTextContent.mock.calls[0].arguments, [mockPackageDataElement]); }); test('parsePackageSummaryData: 配列/オブジェクト両対応', () => { @@ -582,6 +589,7 @@ test.describe('package.js', () => { test('applyRelatedFilterToTable: 未指定ならパッケージフィルタのみ', () => { const doc = setupDocument(); + setPackageData({packages: [], relations: []}, testContext); const rows = buildPackageRows(doc, ['app.domain', 'app.other']); testContext.packageFilterFqn = 'app.domain'; From 6c94c5f9ca79e0b4385340de40989c6ade6294b7 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:26:59 +0900 Subject: [PATCH 28/51] Reduce DOM-coupled tests --- jig-core/src/test/js/package.test.js | 426 +-------------------------- 1 file changed, 12 insertions(+), 414 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 307cfd83f..e056abb1a 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -569,16 +569,6 @@ test.describe('package.js', () => { }); test.describe('テーブル', () => { - test('applyPackageFilterToTable: 行の表示/非表示を切り替える', () => { - const doc = setupDocument(); - const rows = buildPackageRows(doc, ['app.domain', 'app.other']); - - pkg.applyPackageFilterToTable('app.domain', testContext); - - assert.equal(rows[0].classList.contains('hidden'), false); - assert.equal(rows[1].classList.contains('hidden'), true); - }); - test('buildPackageRowVisibility: パッケージフィルタのみ', () => { const visibility = pkg.buildPackageRowVisibility( ['app.domain', 'app.other'], @@ -587,18 +577,6 @@ test.describe('package.js', () => { assert.deepEqual(visibility, [true, false]); }); - test('applyRelatedFilterToTable: 未指定ならパッケージフィルタのみ', () => { - const doc = setupDocument(); - setPackageData({packages: [], relations: []}, testContext); - const rows = buildPackageRows(doc, ['app.domain', 'app.other']); - testContext.packageFilterFqn = 'app.domain'; - - pkg.applyRelatedFilterToTable(null, testContext); - - assert.equal(rows[0].classList.contains('hidden'), false); - assert.equal(rows[1].classList.contains('hidden'), true); - }); - test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); setPackageData({ @@ -675,30 +653,6 @@ test.describe('package.js', () => { assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); }); - test('updateAggregationDepthOptions: 選択肢を更新する', () => { - const doc = setupDocument(); - const select = new Element('select'); - doc.elementsById.set('package-depth-select', select); - setPackageData({ - packages: [ - {fqn: 'app.domain'}, - {fqn: 'lib.core'}, - ], - relations: [ - {from: 'app.domain', to: 'lib.core'}, - ], - }, testContext); - testContext.aggregationDepth = 1; - testContext.packageFilterFqn = null; - testContext.relatedFilterFqn = null; - - pkg.updateAggregationDepthOptions(2, testContext); - - assert.equal(select.children.length >= 2, true); - assert.equal(select.children[0].textContent.includes('集約なし'), true); - assert.equal(select.value, '1'); - }); - test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { const stats = new Map([ [0, {packageCount: 2, relationCount: 1}], @@ -714,21 +668,6 @@ test.describe('package.js', () => { ]); }); - test('renderAggregationDepthOptions: セレクトを更新する', () => { - const doc = setupDocument(); - const select = new Element('select'); - doc.elementsById.set('package-depth-select', select); - const options = [ - {value: '0', text: '集約なし(P2 / R1)'}, - {value: '1', text: '深さ1(P1 / R1)'}, - ]; - - pkg.renderAggregationDepthOptions(select, options, 1, 2); - - assert.equal(select.children.length, 2); - assert.equal(select.children[0].textContent, '集約なし(P2 / R1)'); - assert.equal(select.value, '1'); - }); }); test.describe('一覧/補助', () => { @@ -780,117 +719,6 @@ test.describe('package.js', () => { assert.equal(specs.related.ariaLabel, '関連のみ表示'); assert.equal(specs.related.screenReaderText, '関連のみ表示'); }); - - test('getOrCreateDiagramErrorBox: エラーボックスを作成/再利用する', () => { - const diagram = { parentNode: { insertBefore: test.mock.fn() } }; // Minimal mock for diagram - - const createdErrorBox = { id: 'package-diagram-error', appendChild: test.mock.fn() }; - const createDiagramErrorBoxMock = test.mock.fn(() => createdErrorBox); - - // --- First call: Error box does not exist, should be created --- - const getDiagramErrorBoxMockInitialNull = test.mock.fn(() => null); - test.mock.method(pkg.dom, 'getDiagramErrorBox', getDiagramErrorBoxMockInitialNull); - test.mock.method(pkg.dom, 'createDiagramErrorBox', createDiagramErrorBoxMock); - - const firstErrorBox = pkg.getOrCreateDiagramErrorBox(diagram); - - assert.equal(getDiagramErrorBoxMockInitialNull.mock.calls.length, 1, 'getDiagramErrorBox should be called once initially'); - assert.equal(createDiagramErrorBoxMock.mock.calls.length, 1, 'createDiagramErrorBox should be called once'); - assert.deepEqual(createDiagramErrorBoxMock.mock.calls[0].arguments, [diagram], 'createDiagramErrorBox called with diagram'); - assert.equal(firstErrorBox, createdErrorBox, 'First call returns newly created error box'); - - // --- Second call: Error box already exists --- - const getDiagramErrorBoxMockReturningCreated = test.mock.fn(() => createdErrorBox); // Returns the *same* instance - // Re-mock getDiagramErrorBox to return the existing one. - // It will override the previous mock, so the first mock will not be called again. - test.mock.method(pkg.dom, 'getDiagramErrorBox', getDiagramErrorBoxMockReturningCreated); - - const secondErrorBox = pkg.getOrCreateDiagramErrorBox(diagram); - - assert.equal(getDiagramErrorBoxMockReturningCreated.mock.calls.length, 1, 'getDiagramErrorBox should be called once more for existing'); - // The createDiagramErrorBoxMock.mock.calls.length should still be 1 from the first call. - assert.equal(createDiagramErrorBoxMock.mock.calls.length, 1, 'createDiagramErrorBox should not be called again'); - assert.equal(secondErrorBox, createdErrorBox, 'Second call returns existing error box'); - assert.equal(firstErrorBox, secondErrorBox, 'Both calls should return the same instance when reused'); - }); - - test('showDiagramErrorMessage/hideDiagramErrorMessage: 表示を切り替える', () => { - const diagramMock = { style: { display: '' } }; // Mock for context.diagramElement - const errorBoxMock = { style: { display: 'none' } }; // Mock for the error box element - const messageNodeMock = { textContent: '' }; // Mock for the message node - const actionNodeMock = { style: { display: 'none' }, onclick: null }; // Mock for the action node - - // Mock dom helpers - test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => errorBoxMock)); - test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => errorBoxMock)); // getOrCreate will call this if not found - test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => messageNodeMock)); - test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => actionNodeMock)); - test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); - test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); - test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - - testContext.diagramElement = diagramMock; - - const errors = withConsoleErrorCapture(() => { - pkg.showDiagramErrorMessage('test-error-message', false, null, null, testContext); - }); - - assert.equal(errorBoxMock.style.display, '', 'errorBox should be displayed'); - assert.equal(diagramMock.style.display, 'none', 'diagram should be hidden'); - assert.equal(messageNodeMock.textContent, 'test-error-message', 'messageNode content should be set'); - assert.equal(errors.some(line => line.includes('test-error-message')), true, 'console.error should be called'); - - pkg.hideDiagramErrorMessage(diagramMock); // Pass diagramMock directly as it's used - assert.equal(errorBoxMock.style.display, 'none', 'errorBox should be hidden'); - assert.equal(diagramMock.style.display, '', 'diagram should be displayed'); - assert.equal(messageNodeMock.textContent, '', 'messageNode content should be cleared'); - assert.equal(actionNodeMock.style.display, 'none', 'actionNode should be hidden'); - assert.equal(actionNodeMock.onclick, null, 'actionNode onclick should be cleared'); - }); - - test('renderDiagramSvg: Mermaid描画を実行する', () => { - const diagramMock = { - removeAttribute: test.mock.fn(), - textContent: '', - style: { display: '' } // Needs to be there for dom.setDiagramElementDisplay - }; - testContext.diagramElement = diagramMock; - - // Mock dom helpers used by hideDiagramErrorMessage and renderDiagramSvg itself - test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => ({ style: {} }))); // Mock return for errorBox - test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => ({ style: {} }))); - test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => ({ textContent: '' }))); - test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => ({ style: {}, onclick: null }))); - test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); - test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); - test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - test.mock.method(pkg.dom, 'setDiagramContent', test.mock.fn((el, content) => { el.textContent = content; })); - test.mock.method(pkg.dom, 'removeDiagramAttribute', test.mock.fn((el, attr) => { /* no-op */ })); - - - let runCalled = false; - global.window = { - mermaid: { - initialize() { - }, - run() { - runCalled = true; - }, - }, - }; - global.mermaid = global.window.mermaid; - - pkg.renderDiagramSvg('graph TD', 100, testContext); - - assert.equal(diagramMock.textContent, 'graph TD'); // Check through the dom helper mock - assert.equal(pkg.dom.removeDiagramAttribute.mock.calls.length, 1); - assert.deepEqual(pkg.dom.removeDiagramAttribute.mock.calls[0].arguments, [diagramMock, 'data-processed']); - assert.equal(pkg.dom.setDiagramContent.mock.calls.length, 1); - assert.deepEqual(pkg.dom.setDiagramContent.mock.calls[0].arguments, [diagramMock, 'graph TD']); - assert.equal(runCalled, true); - }); }); test.describe('既定フィルタ', () => { @@ -923,22 +751,6 @@ test.describe('package.js', () => { assert.equal(candidate, 'app.domain'); }); - test('applyDefaultPackageFilterIfPresent: 入力済みなら適用しない', () => { - const doc = setupDocument(); - setPackageData({ - packages: [{fqn: 'app.domain.core'}], - relations: [], - }, testContext); - const input = doc.createElement('input'); - input.id = 'package-filter-input'; - input.value = 'app'; - doc.elementsById.set('package-filter-input', input); - - const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); - - assert.equal(applied, false); - }); - test('normalizePackageFilterValue: 空文字はnull', () => { assert.equal(pkg.normalizePackageFilterValue(''), null); assert.equal(pkg.normalizePackageFilterValue(' '), null); @@ -955,35 +767,24 @@ test.describe('package.js', () => { test.describe('ダイアグラム', () => { test.describe('相互依存', () => { - test('renderMutualDependencyList: なしの場合は非表示', () => { + test('renderMutualDependencyList: 相互依存と原因を一覧化', () => { const doc = setupDocument(); const container = new Element('div', doc); doc.elementsById.set('mutual-dependency-list', container); + testContext.aggregationDepth = 0; - pkg.renderMutualDependencyList(new Set(), [], testContext); - - assert.equal(container.style.display, 'none'); - assert.equal(container.innerHTML, ''); - }); - - test('renderMutualDependencyList: 相互依存と原因を一覧化', () => { - const doc = setupDocument(); - const container = new Element('div', doc); - doc.elementsById.set('mutual-dependency-list', container); - testContext.aggregationDepth = 0; - - pkg.renderMutualDependencyList( - new Set(['app.alpha::app.beta']), - [ - {from: 'app.alpha.A', to: 'app.beta.B'}, - {from: 'app.beta.B', to: 'app.alpha.A'}, - ], - testContext - ); + pkg.renderMutualDependencyList( + new Set(['app.alpha::app.beta']), + [ + {from: 'app.alpha.A', to: 'app.beta.B'}, + {from: 'app.beta.B', to: 'app.alpha.A'}, + ], + testContext + ); assert.equal(container.style.display, ''); - assert.equal(container.children.length, 1); - assert.equal(container.children[0].tagName, 'details'); + assert.equal(container.children.length, 1); + assert.equal(container.children[0].tagName, 'details'); assert.equal(container.children[0].children[1].tagName, 'ul'); }); @@ -1026,41 +827,6 @@ test.describe('package.js', () => { const mutual = doc.getElementById('mutual-dependency-list'); assert.equal(mutual.children.length > 0, true); }); - - test('renderPackageDiagram: サブグラフ内のFQNノードラベルが省略される', () => { - const doc = setupDocument(); - const diagram = setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [ - {fqn: 'com.example', name: 'example', classCount: 1}, - {fqn: 'com.example.domain', name: 'domain', classCount: 1}, - {fqn: 'com.example.domain.model', classCount: 1}, // No 'name' property - {fqn: 'com.example.domain.repository', name: 'repository', classCount: 1}, - {fqn: 'com.example.service', name: 'service', classCount: 1}, - ], - relations: [ - {from: 'com.example.domain.model', to: 'com.example.domain.repository'}, - {from: 'com.example.service', to: 'com.example.domain'}, - ], - }, testContext); - - testContext.aggregationDepth = 0; // Set to no aggregation to ensure full hierarchy is built - pkg.renderPackageDiagram(testContext, null, null); - - const diagramContent = diagram.textContent; - - // Match subgraph creation for com.example.domain (using regex for groupId) - assert.ok(/subgraph G\d+\["com\.example\.domain"]/.test(diagramContent), 'Expected subgraph for com.example.domain'); - - // Check for nodes inside this subgraph: - // com.example.domain.model (FQN, should be shortened to 'model') - assert.ok(/P\d+\["model"]/.test(diagramContent), 'Expected "com.example.domain.model" to be shortened to "model"'); - // com.example.domain.repository (has 'name', should remain 'repository') - assert.ok(/P\d+\["repository"]/.test(diagramContent), 'Expected "com.example.domain.repository" to remain "repository"'); - - // Verify that 'com.example.service' is also present and correctly labeled - assert.ok(/P\d+\["service"]/.test(diagramContent), 'Expected "com.example.service" to be labeled "service"'); - }); }); test.describe('分岐/エラー', () => { @@ -1105,71 +871,6 @@ test.describe('package.js', () => { assert.ok(actionNodeMock.onclick, 'actionNode should have onclick handler'); assert.equal(actionNodeMock.style.display, '', 'actionNode should be displayed'); }); - - test('mermaid.parseError: エラー内容を表示', () => { - const doc = setupDocument(); - const diagramMock = setupDiagramEnvironment(doc, testContext); // testContext.diagramElement is set here - setPackageData({ - packages: [{fqn: 'app.a', name: 'A', classCount: 1}], - relations: [], - }, testContext); - pkg.renderPackageDiagram(testContext, null, null); // This prepares the diagram and sets mermaid.parseError - - // Mock dom helpers used by showDiagramErrorMessage internally - const errorBoxMock = { style: { display: 'none' } }; - const messageNodeMock = { textContent: '' }; - const actionNodeMock = { style: { display: 'none' }, onclick: null }; - test.mock.method(pkg.dom, 'getDiagramErrorBox', test.mock.fn(() => errorBoxMock)); - test.mock.method(pkg.dom, 'createDiagramErrorBox', test.mock.fn(() => errorBoxMock)); // called by getOrCreate - test.mock.method(pkg.dom, 'getDiagramErrorMessageNode', test.mock.fn(() => messageNodeMock)); - test.mock.method(pkg.dom, 'getDiagramErrorActionNode', test.mock.fn(() => actionNodeMock)); - test.mock.method(pkg.dom, 'setNodeTextContent', test.mock.fn((el, text) => { el.textContent = text; })); - test.mock.method(pkg.dom, 'setNodeDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - test.mock.method(pkg.dom, 'setNodeOnClick', test.mock.fn((el, handler) => { el.onclick = handler; })); - test.mock.method(pkg.dom, 'setDiagramElementDisplay', test.mock.fn((el, display) => { el.style.display = display; })); - - // Mermaidはパース失敗時のみ呼ばれるため、テストでは直接呼び出す。 - const errors = withConsoleErrorCapture(() => { - global.mermaid.parseError( - {message: 'Mermaid parse error details'}, // Use a more specific message - {line: 10, loc: 2} - ); - }); - - assert.equal(messageNodeMock.textContent.includes('Mermaid parse error:'), true); - assert.equal(messageNodeMock.textContent.includes('Line: 10 Column: 2'), true); - assert.equal(errors.some(line => line.includes('Mermaid parse error:')), true); - assert.equal(errors.some(line => line.includes('Mermaid parse error details')), true); // Check for specific error message - assert.equal(errors.some(line => line.includes('Mermaid error location: 10 2')), true); - }); - - test('renderDiagramAndTable: 描画とフィルタ適用を行う', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [ - {fqn: 'app.a', name: 'A', classCount: 1}, - {fqn: 'app.b', name: 'B', classCount: 1}, - ], - relations: [ - {from: 'app.a', to: 'app.b'}, - ], - }, testContext); - const rows = buildPackageRows(doc, ['app.a', 'app.b']); - doc.selectors.set('#package-table tbody', doc.createElement('tbody')); - const select = doc.createElement('select'); - select.id = 'package-depth-select'; - doc.elementsById.set('package-depth-select', select); - testContext.relatedFilterMode = 'direct'; - testContext.relatedFilterFqn = 'app.a'; - testContext.packageFilterFqn = null; - testContext.aggregationDepth = 0; - - pkg.renderDiagramAndTable(testContext); - - assert.equal(rows[1].classList.contains('hidden'), false); - assert.equal(select.children.length > 0, true); - }); }); }); @@ -1196,109 +897,6 @@ test.describe('package.js', () => { assert.equal(testContext.packageFilterFqn, null); assert.equal(input.value, ''); }); - - test('setupPackageFilterControls: Enterキーで適用', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], - relations: [], - }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - createDepthSelect(doc); - const {input} = createPackageFilterControls(doc); - - pkg.setupPackageFilterControls(testContext); - - let prevented = false; - input.value = 'app.domain'; - input.eventListeners.get('keydown')({ - key: 'Enter', - preventDefault() { - prevented = true; - }, - }); - - assert.equal(prevented, true); - assert.equal(testContext.packageFilterFqn, 'app.domain'); - }); - - test('setupAggregationDepthControl: 変更を反映する', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [ - {fqn: 'app.domain.a'}, - {fqn: 'app.domain.b'}, - ], - relations: [], - }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - const select = createDepthSelect(doc); - - testContext.aggregationDepth = 0; - pkg.setupAggregationDepthControl(testContext); - - select.value = '1'; - select.eventListeners.get('change')(); - assert.equal(testContext.aggregationDepth, 1); - }); - - test('setupRelatedFilterControls: モード変更を反映', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [ - {fqn: 'app.a'}, - {fqn: 'app.b'}, - {fqn: 'app.c'}, - ], - relations: [ - {from: 'app.a', to: 'app.b'}, - {from: 'app.b', to: 'app.c'}, - ], - }, testContext); - createDepthSelect(doc); - const {select, clearButton} = createRelatedFilterControls(doc); - const input = doc.createElement('input'); - input.id = 'package-filter-input'; - doc.elementsById.set('package-filter-input', input); - - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'direct'; - testContext.relatedFilterFqn = 'app.a'; - pkg.setupRelatedFilterControls(testContext); - - select.value = 'all'; - select.eventListeners.get('change')(); - assert.equal(testContext.relatedFilterMode, 'all'); - - - clearButton.eventListeners.get('click')(); - assert.equal(testContext.relatedFilterFqn, null); - }); - - test('setupDiagramDirectionControls: 向きを切り替える', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [{fqn: 'app.a'}], - relations: [], - }, testContext); - createDepthSelect(doc); - doc.selectorsAll.set('#package-table tbody tr', []); - const td = doc.createElement('input'); - td.value = 'TD'; - const lr = doc.createElement('input'); - lr.value = 'LR'; - doc.selectorsAll.set('input[name=\"diagram-direction\"]', [td, lr]); - - pkg.setupDiagramDirectionControls(testContext); - - lr.checked = true; - lr.eventListeners.get('change')(); - assert.equal(testContext.diagramDirection, 'LR'); - }); }); test.describe('推移簡約', () => { From 10cfacc178c4f8f1b78a8d137a00c565295373b5 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:32:46 +0900 Subject: [PATCH 29/51] Remove unused test helpers --- jig-core/src/test/js/package.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index e056abb1a..b0b56261d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -4,11 +4,6 @@ const assert = require('node:assert/strict'); const pkg = require('../../main/resources/templates/assets/package.js'); const originalDom = {...pkg.dom}; -// Creates a fresh deep copy of the initial context for each test -function createInitialContext() { - return JSON.parse(JSON.stringify(pkg.packageContext)); -} - let testContext; class ClassList { @@ -16,14 +11,6 @@ class ClassList { this.values = new Set(); } - add(value) { - this.values.add(value); - } - - remove(value) { - this.values.delete(value); - } - toggle(value, force) { if (force === undefined) { if (this.values.has(value)) { From f38f8b1820076f5fe45820f2573399f57c827313 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:38:35 +0900 Subject: [PATCH 30/51] Reorder test hierarchy by feature --- jig-core/src/test/js/package.test.js | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index b0b56261d..87fc1ecee 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -268,38 +268,7 @@ test.describe('package.js', () => { setupDomMocks(); }); - test.describe('データ/ヘルパー', () => { - test.describe('collectRelatedSet', () => { - test('directモード: 隣接のみを含める', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'direct'; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - ]; - - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - - assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); - }); - - test('allモード: 推移的に辿る', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'all'; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - ]; - - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - - assert.deepEqual( - Array.from(related).sort(), - ['app.domain.a', 'app.domain.b', 'app.domain.c'] - ); - }); - }); - + test.describe('データ取得/整形', () => { test.describe('データ取得', () => { test('getPackageSummaryData: 配列/オブジェクト両対応', () => { setupDocument(); @@ -454,6 +423,37 @@ test.describe('package.js', () => { }); test.describe('フィルタ', () => { + test.describe('collectRelatedSet', () => { + test('directモード: 隣接のみを含める', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'direct'; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, + ]; + + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); + + assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); + }); + + test('allモード: 推移的に辿る', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'all'; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, + ]; + + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); + + assert.deepEqual( + Array.from(related).sort(), + ['app.domain.a', 'app.domain.b', 'app.domain.c'] + ); + }); + }); + test.describe('getVisibleDiagramElements', () => { const packages = [ {fqn: 'app.a'}, From 9e27e75d339934bd95c31964ed62e234dbd1fb0c Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:44:54 +0900 Subject: [PATCH 31/51] Normalize test section naming --- jig-core/src/test/js/package.test.js | 835 ++++++++++++++------------- 1 file changed, 421 insertions(+), 414 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 87fc1ecee..7fef00886 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -269,7 +269,7 @@ test.describe('package.js', () => { }); test.describe('データ取得/整形', () => { - test.describe('データ取得', () => { + test.describe('ロジック', () => { test('getPackageSummaryData: 配列/オブジェクト両対応', () => { setupDocument(); setPackageData([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); @@ -324,246 +324,279 @@ test.describe('package.js', () => { }); test.describe('集計', () => { - test('buildAggregationStatsForPackageFilter: 対象のみ数える', () => { - testContext.aggregationDepth = 0; - const packages = [ - {fqn: 'app.domain.a'}, - {fqn: 'app.domain.b'}, - {fqn: 'app.other.c'}, - ]; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.other.c', to: 'app.domain.a'}, - ]; - - const stats = pkg.buildAggregationStatsForPackageFilter(packages, relations, 'app.domain', 0); - const depth0 = stats.get(0); - - assert.equal(depth0.packageCount, 2); - assert.equal(depth0.relationCount, 1); - }); + test.describe('ロジック', () => { + test('buildAggregationStatsForPackageFilter: 対象のみ数える', () => { + testContext.aggregationDepth = 0; + const packages = [ + {fqn: 'app.domain.a'}, + {fqn: 'app.domain.b'}, + {fqn: 'app.other.c'}, + ]; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.other.c', to: 'app.domain.a'}, + ]; - test('buildAggregationStatsForRelated: 集計深さを反映する', () => { - const aggregationDepth = 1; - const relatedFilterMode = 'all'; - const packages = [ - {fqn: 'app.domain.a'}, - {fqn: 'app.domain.b'}, - {fqn: 'app.other.c'}, - ]; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.other.c'}, - ]; - - const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1, aggregationDepth, relatedFilterMode); - const depth1 = stats.get(1); - - assert.equal(depth1.packageCount, 1); - assert.equal(depth1.relationCount, 0); - }); + const stats = pkg.buildAggregationStatsForPackageFilter(packages, relations, 'app.domain', 0); + const depth0 = stats.get(0); - test('buildAggregationStatsForFilters: directモードの複合集計', () => { - const packages = [ - {fqn: 'app.domain.a'}, - {fqn: 'app.domain.b'}, - {fqn: 'app.domain.c'}, - {fqn: 'app.other.d'}, - ]; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - {from: 'app.domain.c', to: 'app.other.d'}, - {from: 'app.other.d', to: 'app.domain.a'}, - ]; - - const stats = pkg.buildAggregationStatsForFilters( - packages, - relations, - 'app.domain', - 'app.domain.a', - 0, - 0, - 'direct' - ); - const depth0 = stats.get(0); - - assert.equal(depth0.packageCount, 2); - assert.equal(depth0.relationCount, 1); - }); + assert.equal(depth0.packageCount, 2); + assert.equal(depth0.relationCount, 1); + }); - test('buildAggregationStatsForFilters: allモードの複合集計', () => { - const packages = [ - {fqn: 'app.domain.a'}, - {fqn: 'app.domain.b'}, - {fqn: 'app.domain.c'}, - {fqn: 'app.other.d'}, - ]; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - {from: 'app.domain.c', to: 'app.other.d'}, - {from: 'app.other.d', to: 'app.domain.a'}, - ]; - - const stats = pkg.buildAggregationStatsForFilters( - packages, - relations, - 'app.domain', - 'app.domain.a', - 0, - 0, - 'all' - ); - const depth0 = stats.get(0); - - assert.equal(depth0.packageCount, 3); - assert.equal(depth0.relationCount, 2); - }); - }); + test('buildAggregationStatsForRelated: 集計深さを反映する', () => { + const aggregationDepth = 1; + const relatedFilterMode = 'all'; + const packages = [ + {fqn: 'app.domain.a'}, + {fqn: 'app.domain.b'}, + {fqn: 'app.other.c'}, + ]; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.other.c'}, + ]; - test.describe('フィルタ', () => { - test.describe('collectRelatedSet', () => { - test('directモード: 隣接のみを含める', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'direct'; + const stats = pkg.buildAggregationStatsForRelated(packages, relations, 'app.domain.a', 1, aggregationDepth, relatedFilterMode); + const depth1 = stats.get(1); + + assert.equal(depth1.packageCount, 1); + assert.equal(depth1.relationCount, 0); + }); + + test('buildAggregationStatsForFilters: directモードの複合集計', () => { + const packages = [ + {fqn: 'app.domain.a'}, + {fqn: 'app.domain.b'}, + {fqn: 'app.domain.c'}, + {fqn: 'app.other.d'}, + ]; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, + {from: 'app.domain.c', to: 'app.other.d'}, + {from: 'app.other.d', to: 'app.domain.a'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); + const stats = pkg.buildAggregationStatsForFilters( + packages, + relations, + 'app.domain', + 'app.domain.a', + 0, + 0, + 'direct' + ); + const depth0 = stats.get(0); - assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); + assert.equal(depth0.packageCount, 2); + assert.equal(depth0.relationCount, 1); }); - test('allモード: 推移的に辿る', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'all'; + test('buildAggregationStatsForFilters: allモードの複合集計', () => { + const packages = [ + {fqn: 'app.domain.a'}, + {fqn: 'app.domain.b'}, + {fqn: 'app.domain.c'}, + {fqn: 'app.other.d'}, + ]; const relations = [ {from: 'app.domain.a', to: 'app.domain.b'}, {from: 'app.domain.b', to: 'app.domain.c'}, + {from: 'app.domain.c', to: 'app.other.d'}, + {from: 'app.other.d', to: 'app.domain.a'}, ]; - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - - assert.deepEqual( - Array.from(related).sort(), - ['app.domain.a', 'app.domain.b', 'app.domain.c'] + const stats = pkg.buildAggregationStatsForFilters( + packages, + relations, + 'app.domain', + 'app.domain.a', + 0, + 0, + 'all' ); + const depth0 = stats.get(0); + + assert.equal(depth0.packageCount, 3); + assert.equal(depth0.relationCount, 2); }); }); + }); - test.describe('getVisibleDiagramElements', () => { - const packages = [ - {fqn: 'app.a'}, - {fqn: 'app.b'}, - {fqn: 'app.c'}, - {fqn: 'lib.d'}, - ]; - const relations = [ - {from: 'app.a', to: 'app.b'}, - {from: 'app.b', to: 'app.c'}, - {from: 'app.c', to: 'lib.d'}, - ]; + test.describe('フィルタ', () => { + test.describe('ロジック', () => { + test.describe('collectRelatedSet', () => { + test('directモード: 隣接のみを含める', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'direct'; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, + ]; + + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); + + assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); + }); - test('packageFilter: 指定パッケージ配下のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); + test('allモード: 推移的に辿る', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'all'; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, + ]; - test('relatedFilter(direct): 指定パッケージの隣接のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - test('relatedFilter(all): 指定パッケージから到達可能なものすべて表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); + assert.deepEqual( + Array.from(related).sort(), + ['app.domain.a', 'app.domain.b', 'app.domain.c'] + ); + }); }); - test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); - assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - assert.equal(base.uniqueRelations.length, 2); - }); + test.describe('getVisibleDiagramElements', () => { + const packages = [ + {fqn: 'app.a'}, + {fqn: 'app.b'}, + {fqn: 'app.c'}, + {fqn: 'lib.d'}, + ]; + const relations = [ + {from: 'app.a', to: 'app.b'}, + {from: 'app.b', to: 'app.c'}, + {from: 'app.c', to: 'lib.d'}, + ]; - test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); - const filtered = pkg.applyRelatedFilterToDiagramRelations( - base.uniqueRelations, - base.visibleSet, - 'app.b', - 0, - 'direct' - ); - assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - assert.equal(filtered.uniqueRelations.length, 2); - }); + test('packageFilter: 指定パッケージ配下のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); - test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { - const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); - const result = pkg.buildDiagramEdgeLines( - [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], - ensureNodeId - ); - assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); - assert.equal(result.linkStyles.length, 1); - }); + test('relatedFilter(direct): 指定パッケージの隣接のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); - test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { - const visibleFqns = ['com.example.a', 'com.example.b']; - const nodeIdByFqn = new Map([ - ['com.example.a', 'P0'], - ['com.example.b', 'P1'], - ]); + test('relatedFilter(all): 指定パッケージから到達可能なものすべて表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); + }); - const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); + assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(base.uniqueRelations.length, 2); + }); - assert.equal(rootGroup.children.has('com.example'), true); - }); + test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); + const filtered = pkg.applyRelatedFilterToDiagramRelations( + base.uniqueRelations, + base.visibleSet, + 'app.b', + 0, + 'direct' + ); + assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(filtered.uniqueRelations.length, 2); + }); - test('buildSubgraphLines: サブグラフ行を生成する', () => { - const rootGroup = { - key: '', - nodes: [], - children: new Map([ - ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], - ]), - }; - const addNodeLines = (lines, nodeId) => { - lines.push(`node ${nodeId}`); - }; + test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { + const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); + const result = pkg.buildDiagramEdgeLines( + [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], + ensureNodeId + ); + assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); + assert.equal(result.linkStyles.length, 1); + }); - const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); + test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { + const visibleFqns = ['com.example.a', 'com.example.b']; + const nodeIdByFqn = new Map([ + ['com.example.a', 'P0'], + ['com.example.b', 'P1'], + ]); - assert.equal(lines.some(line => line.includes('node P0')), true); - }); + const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); - test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { - const label = pkg.buildDiagramNodeLabel( - 'com.example.domain.model', - 'com.example.domain.model', - 'com.example.domain' - ); - assert.equal(label, 'model'); - }); + assert.equal(rootGroup.children.has('com.example'), true); + }); + + test('buildSubgraphLines: サブグラフ行を生成する', () => { + const rootGroup = { + key: '', + nodes: [], + children: new Map([ + ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], + ]), + }; + const addNodeLines = (lines, nodeId) => { + lines.push(`node ${nodeId}`); + }; + + const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); + + assert.equal(lines.some(line => line.includes('node P0')), true); + }); - test('buildDiagramNodeTooltip: FQNを返す', () => { - assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); - assert.equal(pkg.buildDiagramNodeTooltip(null), ''); + test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { + const label = pkg.buildDiagramNodeLabel( + 'com.example.domain.model', + 'com.example.domain.model', + 'com.example.domain' + ); + assert.equal(label, 'model'); + }); + + test('buildDiagramNodeTooltip: FQNを返す', () => { + assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); + assert.equal(pkg.buildDiagramNodeTooltip(null), ''); + }); }); - }); - test.describe('テーブル', () => { - test('buildPackageRowVisibility: パッケージフィルタのみ', () => { - const visibility = pkg.buildPackageRowVisibility( - ['app.domain', 'app.other'], - 'app.domain' - ); - assert.deepEqual(visibility, [true, false]); + test.describe('テーブル', () => { + test('buildPackageRowVisibility: パッケージフィルタのみ', () => { + const visibility = pkg.buildPackageRowVisibility( + ['app.domain', 'app.other'], + 'app.domain' + ); + assert.deepEqual(visibility, [true, false]); + }); + + test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { + const rowFqns = ['app.domain', 'app.other']; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + [], + 'app.domain', + 0, + 'direct', + null + ); + assert.deepEqual(visibility, [true, false]); + }); + + test('buildRelatedRowVisibility: 関係する行のみ表示', () => { + const rowFqns = ['app.a', 'app.b', 'app.c']; + const relations = [{from: 'app.a', to: 'app.b'}]; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + relations, + null, + 0, + 'direct', + 'app.a' + ); + assert.deepEqual(visibility, [true, true, false]); + }); }); + }); + test.describe('UI', () => { test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); setPackageData({ @@ -587,38 +620,73 @@ test.describe('package.js', () => { assert.equal(rows[1].classList.contains('hidden'), false); assert.equal(rows[2].classList.contains('hidden'), true); }); + }); + }); - test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { - const rowFqns = ['app.domain', 'app.other']; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - [], - 'app.domain', - 0, - 'direct', - null - ); - assert.deepEqual(visibility, [true, false]); + test.describe('描画', () => { + test.describe('ロジック', () => { + test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { + const stats = new Map([ + [0, {packageCount: 2, relationCount: 1}], + [1, {packageCount: 1, relationCount: 1}], + [2, {packageCount: 1, relationCount: 0}], + ]); + + const options = pkg.buildAggregationDepthOptions(stats, 2); + + assert.deepEqual(options, [ + {value: '0', text: '集約なし(P2 / R1)'}, + {value: '1', text: '深さ1(P1 / R1)'}, + ]); }); - test('buildRelatedRowVisibility: 関係する行のみ表示', () => { - const rowFqns = ['app.a', 'app.b', 'app.c']; - const relations = [{from: 'app.a', to: 'app.b'}]; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - relations, - null, - 0, - 'direct', - 'app.a' - ); - assert.deepEqual(visibility, [true, true, false]); + test('buildPackageTableRowSpecs: 行データを整形する', () => { + const rows = [ + {fqn: 'app.a', name: 'A', classCount: 2, incomingCount: 0, outgoingCount: 1}, + ]; + + const specs = pkg.buildPackageTableRowSpecs(rows); + + assert.deepEqual(specs, [{ + fqn: 'app.a', + name: 'A', + classCount: 2, + incomingCount: 0, + outgoingCount: 1, + }]); + }); + + test('buildPackageTableActionSpecs: ボタン文言を返す', () => { + const specs = pkg.buildPackageTableActionSpecs(); + + assert.equal(specs.filter.ariaLabel, 'このパッケージで絞り込み'); + assert.equal(specs.filter.screenReaderText, '絞り込み'); + assert.equal(specs.related.ariaLabel, '関連のみ表示'); + assert.equal(specs.related.screenReaderText, '関連のみ表示'); + }); + + test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { + const candidate = pkg.findDefaultPackageFilterCandidate([ + {fqn: 'app.domain.core'}, + {fqn: 'app.domain.sub'}, + ]); + assert.equal(candidate, 'app.domain'); + }); + + test('normalizePackageFilterValue: 空文字はnull', () => { + assert.equal(pkg.normalizePackageFilterValue(''), null); + assert.equal(pkg.normalizePackageFilterValue(' '), null); + assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); + }); + + test('normalizeAggregationDepthValue: 数値化する', () => { + assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); + assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); + assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); }); }); - }); - test.describe('描画', () => { - test.describe('UI表示', () => { + test.describe('UI', () => { test('renderRelatedFilterTarget: 対象表示を更新する', () => { const mockTarget = { textContent: '' }; const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); @@ -640,24 +708,6 @@ test.describe('package.js', () => { assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); }); - test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { - const stats = new Map([ - [0, {packageCount: 2, relationCount: 1}], - [1, {packageCount: 1, relationCount: 1}], - [2, {packageCount: 1, relationCount: 0}], - ]); - - const options = pkg.buildAggregationDepthOptions(stats, 2); - - assert.deepEqual(options, [ - {value: '0', text: '集約なし(P2 / R1)'}, - {value: '1', text: '深さ1(P1 / R1)'}, - ]); - }); - - }); - - test.describe('一覧/補助', () => { test('renderPackageTable: 行とカウントを描画する', () => { const doc = setupDocument(); setPackageData({ @@ -682,33 +732,6 @@ test.describe('package.js', () => { assert.equal(tbody.children[0].children[6].textContent, '2'); }); - test('buildPackageTableRowSpecs: 行データを整形する', () => { - const rows = [ - {fqn: 'app.a', name: 'A', classCount: 2, incomingCount: 0, outgoingCount: 1}, - ]; - - const specs = pkg.buildPackageTableRowSpecs(rows); - - assert.deepEqual(specs, [{ - fqn: 'app.a', - name: 'A', - classCount: 2, - incomingCount: 0, - outgoingCount: 1, - }]); - }); - - test('buildPackageTableActionSpecs: ボタン文言を返す', () => { - const specs = pkg.buildPackageTableActionSpecs(); - - assert.equal(specs.filter.ariaLabel, 'このパッケージで絞り込み'); - assert.equal(specs.filter.screenReaderText, '絞り込み'); - assert.equal(specs.related.ariaLabel, '関連のみ表示'); - assert.equal(specs.related.screenReaderText, '関連のみ表示'); - }); - }); - - test.describe('既定フィルタ', () => { test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); @@ -729,31 +752,28 @@ test.describe('package.js', () => { assert.equal(testContext.packageFilterFqn, 'app.domain'); assert.equal(input.value, 'app.domain'); }); + }); + }); - test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { - const candidate = pkg.findDefaultPackageFilterCandidate([ - {fqn: 'app.domain.core'}, - {fqn: 'app.domain.sub'}, - ]); - assert.equal(candidate, 'app.domain'); - }); - - test('normalizePackageFilterValue: 空文字はnull', () => { - assert.equal(pkg.normalizePackageFilterValue(''), null); - assert.equal(pkg.normalizePackageFilterValue(' '), null); - assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); - }); + test.describe('ダイアグラム', () => { + test.describe('ロジック', () => { + test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { + const items = pkg.buildMutualDependencyItems( + new Set(['app.alpha::app.beta']), + [ + {from: 'app.alpha.A', to: 'app.beta.B'}, + {from: 'app.beta.B', to: 'app.alpha.A'}, + ], + 0 + ); - test('normalizeAggregationDepthValue: 数値化する', () => { - assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); - assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); - assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); + assert.equal(items.length, 1); + assert.equal(items[0].pairLabel, 'app.alpha <-> app.beta'); + assert.equal(items[0].causes.length, 2); }); }); - }); - test.describe('ダイアグラム', () => { - test.describe('相互依存', () => { + test.describe('UI', () => { test('renderMutualDependencyList: 相互依存と原因を一覧化', () => { const doc = setupDocument(); const container = new Element('div', doc); @@ -775,23 +795,6 @@ test.describe('package.js', () => { assert.equal(container.children[0].children[1].tagName, 'ul'); }); - test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { - const items = pkg.buildMutualDependencyItems( - new Set(['app.alpha::app.beta']), - [ - {from: 'app.alpha.A', to: 'app.beta.B'}, - {from: 'app.beta.B', to: 'app.alpha.A'}, - ], - 0 - ); - - assert.equal(items.length, 1); - assert.equal(items[0].pairLabel, 'app.alpha <-> app.beta'); - assert.equal(items[0].causes.length, 2); - }); - }); - - test.describe('描画', () => { test('renderPackageDiagram: 相互依存を含む描画', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); @@ -814,9 +817,7 @@ test.describe('package.js', () => { const mutual = doc.getElementById('mutual-dependency-list'); assert.equal(mutual.children.length > 0, true); }); - }); - test.describe('分岐/エラー', () => { test('renderPackageDiagram: エッジ数超過で保留/エラー表示', () => { const doc = setupDocument(); // setupDiagramEnvironmentはtestContext.diagramElementを設定する。 @@ -862,119 +863,125 @@ test.describe('package.js', () => { }); test.describe('UI制御', () => { - test('setupPackageFilterControls: 適用/解除をハンドリング', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], - relations: [], - }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - createDepthSelect(doc); - - const {input, applyButton, clearButton} = createPackageFilterControls(doc); - - pkg.setupPackageFilterControls(testContext); - - input.value = 'app.domain'; - applyButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, 'app.domain'); - - clearButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, null); - assert.equal(input.value, ''); + test.describe('UI', () => { + test('setupPackageFilterControls: 適用/解除をハンドリング', () => { + const doc = setupDocument(); + setupDiagramEnvironment(doc, testContext); + setPackageData({ + packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], + relations: [], + }, testContext); + doc.selectorsAll.set('#package-table tbody tr', []); + createDepthSelect(doc); + + const {input, applyButton, clearButton} = createPackageFilterControls(doc); + + pkg.setupPackageFilterControls(testContext); + + input.value = 'app.domain'; + applyButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, 'app.domain'); + + clearButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, null); + assert.equal(input.value, ''); + }); }); }); test.describe('推移簡約', () => { - test('detectStronglyConnectedComponents: 循環を検出する', () => { - const graph = new Map([ - ['a', ['b']], - ['b', ['c']], - ['c', ['a', 'd']], - ['d', ['e']], - ['e', ['f']], - ['f', ['d']], - ]); - const sccs = pkg.detectStronglyConnectedComponents(graph); - const sortedSccs = sccs.map(scc => scc.sort()).sort((a, b) => a[0].localeCompare(b[0])); - assert.deepEqual(sortedSccs, [['a', 'b', 'c'], ['d', 'e', 'f']]); - }); + test.describe('ロジック', () => { + test('detectStronglyConnectedComponents: 循環を検出する', () => { + const graph = new Map([ + ['a', ['b']], + ['b', ['c']], + ['c', ['a', 'd']], + ['d', ['e']], + ['e', ['f']], + ['f', ['d']], + ]); + const sccs = pkg.detectStronglyConnectedComponents(graph); + const sortedSccs = sccs.map(scc => scc.sort()).sort((a, b) => a[0].localeCompare(b[0])); + assert.deepEqual(sortedSccs, [['a', 'b', 'c'], ['d', 'e', 'f']]); + }); - test('transitiveReduction: 単純な推移関係を簡約する', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'c'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'b>c']); - }); + test('transitiveReduction: 単純な推移関係を簡約する', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'c'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'b>c']); + }); - test('transitiveReduction: 循環参照は対象外とする', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'a'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a']); - }); + test('transitiveReduction: 循環参照は対象外とする', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'a'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a']); + }); - test('transitiveReduction: 循環ではないが簡約対象でもない', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'c', to: 'd'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'c>d']); - }); + test('transitiveReduction: 循環ではないが簡約対象でもない', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'c', to: 'd'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'c>d']); + }); - test('transitiveReduction: 循環からの関係は簡約対象にしない', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'a'}, // cycle - {from: 'b', to: 'c'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); + test('transitiveReduction: 循環からの関係は簡約対象にしない', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'a'}, // cycle + {from: 'b', to: 'c'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); + }); }); - test('setupTransitiveReductionControl: UIをセットアップする', () => { - const doc = setupDocument(); - const container = doc.createElement('div'); - const pp = doc.createElement('div'); - const input = doc.createElement('input'); - input.name = 'diagram-direction'; - pp.appendChild(input); - container.appendChild(pp); - doc.selectors.set('input[name="diagram-direction"]', input); - input.parentNode = pp; - pp.parentNode = container; - - // renderDiagramAndTableの副作用をチェックするための準備 - setupDiagramEnvironment(doc, testContext); - setPackageData({packages: [{fqn: 'a'}], relations: []}, testContext); - const depthSelect = createDepthSelect(doc); - const dummyOption = doc.createElement('option'); - dummyOption.id = 'dummy-option-for-test'; - depthSelect.appendChild(dummyOption); - doc.selectorsAll.set('#package-table tbody tr', []); - - - pkg.setupTransitiveReductionControl(testContext); - - const checkbox = doc.getElementById('transitive-reduction-toggle'); - assert.ok(checkbox, 'checkbox should be created'); - assert.equal(checkbox.checked, true); - assert.equal(testContext.transitiveReductionEnabled, true); - - // changeイベントを発火させる - checkbox.checked = false; - checkbox.eventListeners.get('change')(); - - assert.equal(testContext.transitiveReductionEnabled, false); + test.describe('UI', () => { + test('setupTransitiveReductionControl: UIをセットアップする', () => { + const doc = setupDocument(); + const container = doc.createElement('div'); + const pp = doc.createElement('div'); + const input = doc.createElement('input'); + input.name = 'diagram-direction'; + pp.appendChild(input); + container.appendChild(pp); + doc.selectors.set('input[name="diagram-direction"]', input); + input.parentNode = pp; + pp.parentNode = container; + + // renderDiagramAndTableの副作用をチェックするための準備 + setupDiagramEnvironment(doc, testContext); + setPackageData({packages: [{fqn: 'a'}], relations: []}, testContext); + const depthSelect = createDepthSelect(doc); + const dummyOption = doc.createElement('option'); + dummyOption.id = 'dummy-option-for-test'; + depthSelect.appendChild(dummyOption); + doc.selectorsAll.set('#package-table tbody tr', []); + + + pkg.setupTransitiveReductionControl(testContext); + + const checkbox = doc.getElementById('transitive-reduction-toggle'); + assert.ok(checkbox, 'checkbox should be created'); + assert.equal(checkbox.checked, true); + assert.equal(testContext.transitiveReductionEnabled, true); + + // changeイベントを発火させる + checkbox.checked = false; + checkbox.eventListeners.get('change')(); + + assert.equal(testContext.transitiveReductionEnabled, false); + }); }); }); }); From 294a18af58ee87bf2d3d364b905a53b708f41b94 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:50:54 +0900 Subject: [PATCH 32/51] Restructure test hierarchy by feature --- jig-core/src/test/js/package.test.js | 606 +++++++++++++-------------- 1 file changed, 295 insertions(+), 311 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 7fef00886..6591d7125 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -426,177 +426,203 @@ test.describe('package.js', () => { test.describe('フィルタ', () => { test.describe('ロジック', () => { - test.describe('collectRelatedSet', () => { - test('directモード: 隣接のみを含める', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'direct'; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - ]; - - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - - assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); - }); - - test('allモード: 推移的に辿る', () => { - const aggregationDepth = 0; - const relatedFilterMode = 'all'; - const relations = [ - {from: 'app.domain.a', to: 'app.domain.b'}, - {from: 'app.domain.b', to: 'app.domain.c'}, - ]; + test('collectRelatedSet: directモードは隣接のみ含める', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'direct'; + const relations = [ + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, + ]; - const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - assert.deepEqual( - Array.from(related).sort(), - ['app.domain.a', 'app.domain.b', 'app.domain.c'] - ); - }); + assert.deepEqual(Array.from(related).sort(), ['app.domain.a', 'app.domain.b']); }); - test.describe('getVisibleDiagramElements', () => { - const packages = [ - {fqn: 'app.a'}, - {fqn: 'app.b'}, - {fqn: 'app.c'}, - {fqn: 'lib.d'}, - ]; + test('collectRelatedSet: allモードは推移的に辿る', () => { + const aggregationDepth = 0; + const relatedFilterMode = 'all'; const relations = [ - {from: 'app.a', to: 'app.b'}, - {from: 'app.b', to: 'app.c'}, - {from: 'app.c', to: 'lib.d'}, + {from: 'app.domain.a', to: 'app.domain.b'}, + {from: 'app.domain.b', to: 'app.domain.c'}, ]; - test('packageFilter: 指定パッケージ配下のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); - - test('relatedFilter(direct): 指定パッケージの隣接のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); + const related = pkg.collectRelatedSet('app.domain.a', relations, aggregationDepth, relatedFilterMode); - test('relatedFilter(all): 指定パッケージから到達可能なものすべて表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); - }); + assert.deepEqual( + Array.from(related).sort(), + ['app.domain.a', 'app.domain.b', 'app.domain.c'] + ); + }); - test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); - assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - assert.equal(base.uniqueRelations.length, 2); - }); + const packages = [ + {fqn: 'app.a'}, + {fqn: 'app.b'}, + {fqn: 'app.c'}, + {fqn: 'lib.d'}, + ]; + const relations = [ + {from: 'app.a', to: 'app.b'}, + {from: 'app.b', to: 'app.c'}, + {from: 'app.c', to: 'lib.d'}, + ]; - test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); - const filtered = pkg.applyRelatedFilterToDiagramRelations( - base.uniqueRelations, - base.visibleSet, - 'app.b', - 0, - 'direct' - ); - assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - assert.equal(filtered.uniqueRelations.length, 2); - }); + test('getVisibleDiagramElements: packageFilterは配下のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); - test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { - const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); - const result = pkg.buildDiagramEdgeLines( - [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], - ensureNodeId - ); - assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); - assert.equal(result.linkStyles.length, 1); - }); + test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + }); - test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { - const visibleFqns = ['com.example.a', 'com.example.b']; - const nodeIdByFqn = new Map([ - ['com.example.a', 'P0'], - ['com.example.b', 'P1'], - ]); + test('getVisibleDiagramElements: relatedFilter(all)は到達可能を表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); + }); - const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); + assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(base.uniqueRelations.length, 2); + }); - assert.equal(rootGroup.children.has('com.example'), true); - }); + test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { + const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); + const filtered = pkg.applyRelatedFilterToDiagramRelations( + base.uniqueRelations, + base.visibleSet, + 'app.b', + 0, + 'direct' + ); + assert.deepEqual(Array.from(filtered.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); + assert.equal(filtered.uniqueRelations.length, 2); + }); - test('buildSubgraphLines: サブグラフ行を生成する', () => { - const rootGroup = { - key: '', - nodes: [], - children: new Map([ - ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], - ]), - }; - const addNodeLines = (lines, nodeId) => { - lines.push(`node ${nodeId}`); - }; - - const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); - - assert.equal(lines.some(line => line.includes('node P0')), true); - }); + test('buildPackageRowVisibility: パッケージフィルタのみ', () => { + const visibility = pkg.buildPackageRowVisibility( + ['app.domain', 'app.other'], + 'app.domain' + ); + assert.deepEqual(visibility, [true, false]); + }); - test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { - const label = pkg.buildDiagramNodeLabel( - 'com.example.domain.model', - 'com.example.domain.model', - 'com.example.domain' - ); - assert.equal(label, 'model'); - }); + test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { + const rowFqns = ['app.domain', 'app.other']; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + [], + 'app.domain', + 0, + 'direct', + null + ); + assert.deepEqual(visibility, [true, false]); + }); - test('buildDiagramNodeTooltip: FQNを返す', () => { - assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); - assert.equal(pkg.buildDiagramNodeTooltip(null), ''); - }); + test('buildRelatedRowVisibility: 関係する行のみ表示', () => { + const rowFqns = ['app.a', 'app.b', 'app.c']; + const relations = [{from: 'app.a', to: 'app.b'}]; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + relations, + null, + 0, + 'direct', + 'app.a' + ); + assert.deepEqual(visibility, [true, true, false]); }); - test.describe('テーブル', () => { - test('buildPackageRowVisibility: パッケージフィルタのみ', () => { - const visibility = pkg.buildPackageRowVisibility( - ['app.domain', 'app.other'], - 'app.domain' - ); - assert.deepEqual(visibility, [true, false]); - }); + test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { + const candidate = pkg.findDefaultPackageFilterCandidate([ + {fqn: 'app.domain.core'}, + {fqn: 'app.domain.sub'}, + ]); + assert.equal(candidate, 'app.domain'); + }); - test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { - const rowFqns = ['app.domain', 'app.other']; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - [], - 'app.domain', - 0, - 'direct', - null - ); - assert.deepEqual(visibility, [true, false]); - }); + test('normalizePackageFilterValue: 空文字はnull', () => { + assert.equal(pkg.normalizePackageFilterValue(''), null); + assert.equal(pkg.normalizePackageFilterValue(' '), null); + assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); + }); - test('buildRelatedRowVisibility: 関係する行のみ表示', () => { - const rowFqns = ['app.a', 'app.b', 'app.c']; - const relations = [{from: 'app.a', to: 'app.b'}]; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - relations, - null, - 0, - 'direct', - 'app.a' - ); - assert.deepEqual(visibility, [true, true, false]); - }); + test('normalizeAggregationDepthValue: 数値化する', () => { + assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); + assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); + assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); }); }); test.describe('UI', () => { + test('renderRelatedFilterTarget: 対象表示を更新する', () => { + const mockTarget = { textContent: '' }; + const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); + const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); + + test.mock.method(pkg.dom, 'getRelatedFilterTarget', getRelatedFilterTargetMock); + test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); + + testContext.relatedFilterFqn = null; + pkg.renderRelatedFilterTarget(testContext); + assert.equal(mockTarget.textContent, '未選択'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); + + testContext.relatedFilterFqn = 'app.domain'; + pkg.renderRelatedFilterTarget(testContext); + assert.equal(mockTarget.textContent, 'app.domain'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); + }); + + test('setupPackageFilterControls: 適用/解除をハンドリング', () => { + const doc = setupDocument(); + setupDiagramEnvironment(doc, testContext); + setPackageData({ + packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], + relations: [], + }, testContext); + doc.selectorsAll.set('#package-table tbody tr', []); + createDepthSelect(doc); + + const {input, applyButton, clearButton} = createPackageFilterControls(doc); + + pkg.setupPackageFilterControls(testContext); + + input.value = 'app.domain'; + applyButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, 'app.domain'); + + clearButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, null); + assert.equal(input.value, ''); + }); + + test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { + const doc = setupDocument(); + setupDiagramEnvironment(doc, testContext); + setPackageData({ + packages: [ + {fqn: 'app.domain.core'}, + {fqn: 'app.domain.sub'}, + ], + relations: [], + }, testContext); + doc.selectorsAll.set('#package-table tbody tr', []); + const {input} = createPackageFilterControls(doc); + createDepthSelect(doc); // for renderDiagramAndTable + + const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); + + assert.equal(applied, true); + assert.equal(testContext.packageFilterFqn, 'app.domain'); + assert.equal(input.value, 'app.domain'); + }); + test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); setPackageData({ @@ -623,23 +649,8 @@ test.describe('package.js', () => { }); }); - test.describe('描画', () => { + test.describe('テーブル', () => { test.describe('ロジック', () => { - test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { - const stats = new Map([ - [0, {packageCount: 2, relationCount: 1}], - [1, {packageCount: 1, relationCount: 1}], - [2, {packageCount: 1, relationCount: 0}], - ]); - - const options = pkg.buildAggregationDepthOptions(stats, 2); - - assert.deepEqual(options, [ - {value: '0', text: '集約なし(P2 / R1)'}, - {value: '1', text: '深さ1(P1 / R1)'}, - ]); - }); - test('buildPackageTableRowSpecs: 行データを整形する', () => { const rows = [ {fqn: 'app.a', name: 'A', classCount: 2, incomingCount: 0, outgoingCount: 1}, @@ -664,50 +675,9 @@ test.describe('package.js', () => { assert.equal(specs.related.ariaLabel, '関連のみ表示'); assert.equal(specs.related.screenReaderText, '関連のみ表示'); }); - - test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { - const candidate = pkg.findDefaultPackageFilterCandidate([ - {fqn: 'app.domain.core'}, - {fqn: 'app.domain.sub'}, - ]); - assert.equal(candidate, 'app.domain'); - }); - - test('normalizePackageFilterValue: 空文字はnull', () => { - assert.equal(pkg.normalizePackageFilterValue(''), null); - assert.equal(pkg.normalizePackageFilterValue(' '), null); - assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); - }); - - test('normalizeAggregationDepthValue: 数値化する', () => { - assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); - assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); - assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); - }); }); test.describe('UI', () => { - test('renderRelatedFilterTarget: 対象表示を更新する', () => { - const mockTarget = { textContent: '' }; - const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); - const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); - - test.mock.method(pkg.dom, 'getRelatedFilterTarget', getRelatedFilterTargetMock); - test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); - - testContext.relatedFilterFqn = null; - pkg.renderRelatedFilterTarget(testContext); - assert.equal(mockTarget.textContent, '未選択'); - assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); - assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); - - testContext.relatedFilterFqn = 'app.domain'; - pkg.renderRelatedFilterTarget(testContext); - assert.equal(mockTarget.textContent, 'app.domain'); - assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); - assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); - }); - test('renderPackageTable: 行とカウントを描画する', () => { const doc = setupDocument(); setPackageData({ @@ -731,32 +701,79 @@ test.describe('package.js', () => { assert.equal(tbody.children[0].children[5].textContent, '0'); assert.equal(tbody.children[0].children[6].textContent, '2'); }); - - test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [ - {fqn: 'app.domain.core'}, - {fqn: 'app.domain.sub'}, - ], - relations: [], - }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - const {input} = createPackageFilterControls(doc); - createDepthSelect(doc); // for renderDiagramAndTable - - const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); - - assert.equal(applied, true); - assert.equal(testContext.packageFilterFqn, 'app.domain'); - assert.equal(input.value, 'app.domain'); - }); }); }); test.describe('ダイアグラム', () => { test.describe('ロジック', () => { + test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { + const stats = new Map([ + [0, {packageCount: 2, relationCount: 1}], + [1, {packageCount: 1, relationCount: 1}], + [2, {packageCount: 1, relationCount: 0}], + ]); + + const options = pkg.buildAggregationDepthOptions(stats, 2); + + assert.deepEqual(options, [ + {value: '0', text: '集約なし(P2 / R1)'}, + {value: '1', text: '深さ1(P1 / R1)'}, + ]); + }); + + test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { + const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); + const result = pkg.buildDiagramEdgeLines( + [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], + ensureNodeId + ); + assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); + assert.equal(result.linkStyles.length, 1); + }); + + test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { + const visibleFqns = ['com.example.a', 'com.example.b']; + const nodeIdByFqn = new Map([ + ['com.example.a', 'P0'], + ['com.example.b', 'P1'], + ]); + + const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + + assert.equal(rootGroup.children.has('com.example'), true); + }); + + test('buildSubgraphLines: サブグラフ行を生成する', () => { + const rootGroup = { + key: '', + nodes: [], + children: new Map([ + ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], + ]), + }; + const addNodeLines = (lines, nodeId) => { + lines.push(`node ${nodeId}`); + }; + + const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); + + assert.equal(lines.some(line => line.includes('node P0')), true); + }); + + test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { + const label = pkg.buildDiagramNodeLabel( + 'com.example.domain.model', + 'com.example.domain.model', + 'com.example.domain' + ); + assert.equal(label, 'model'); + }); + + test('buildDiagramNodeTooltip: FQNを返す', () => { + assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); + assert.equal(pkg.buildDiagramNodeTooltip(null), ''); + }); + test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { const items = pkg.buildMutualDependencyItems( new Set(['app.alpha::app.beta']), @@ -771,6 +788,60 @@ test.describe('package.js', () => { assert.equal(items[0].pairLabel, 'app.alpha <-> app.beta'); assert.equal(items[0].causes.length, 2); }); + + test('detectStronglyConnectedComponents: 循環を検出する', () => { + const graph = new Map([ + ['a', ['b']], + ['b', ['c']], + ['c', ['a', 'd']], + ['d', ['e']], + ['e', ['f']], + ['f', ['d']], + ]); + const sccs = pkg.detectStronglyConnectedComponents(graph); + const sortedSccs = sccs.map(scc => scc.sort()).sort((a, b) => a[0].localeCompare(b[0])); + assert.deepEqual(sortedSccs, [['a', 'b', 'c'], ['d', 'e', 'f']]); + }); + + test('transitiveReduction: 単純な推移関係を簡約する', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'c'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'b>c']); + }); + + test('transitiveReduction: 循環参照は対象外とする', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'a'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a']); + }); + + test('transitiveReduction: 循環ではないが簡約対象でもない', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'c', to: 'd'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'c>d']); + }); + + test('transitiveReduction: 循環からの関係は簡約対象にしない', () => { + const relations = [ + {from: 'a', to: 'b'}, + {from: 'b', to: 'a'}, // cycle + {from: 'b', to: 'c'}, + {from: 'a', to: 'c'}, + ]; + const result = pkg.transitiveReduction(relations); + assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); + }); }); test.describe('UI', () => { @@ -859,94 +930,7 @@ test.describe('package.js', () => { assert.ok(actionNodeMock.onclick, 'actionNode should have onclick handler'); assert.equal(actionNodeMock.style.display, '', 'actionNode should be displayed'); }); - }); - }); - - test.describe('UI制御', () => { - test.describe('UI', () => { - test('setupPackageFilterControls: 適用/解除をハンドリング', () => { - const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); - setPackageData({ - packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], - relations: [], - }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - createDepthSelect(doc); - const {input, applyButton, clearButton} = createPackageFilterControls(doc); - - pkg.setupPackageFilterControls(testContext); - - input.value = 'app.domain'; - applyButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, 'app.domain'); - - clearButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, null); - assert.equal(input.value, ''); - }); - }); - }); - - test.describe('推移簡約', () => { - test.describe('ロジック', () => { - test('detectStronglyConnectedComponents: 循環を検出する', () => { - const graph = new Map([ - ['a', ['b']], - ['b', ['c']], - ['c', ['a', 'd']], - ['d', ['e']], - ['e', ['f']], - ['f', ['d']], - ]); - const sccs = pkg.detectStronglyConnectedComponents(graph); - const sortedSccs = sccs.map(scc => scc.sort()).sort((a, b) => a[0].localeCompare(b[0])); - assert.deepEqual(sortedSccs, [['a', 'b', 'c'], ['d', 'e', 'f']]); - }); - - test('transitiveReduction: 単純な推移関係を簡約する', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'c'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'b>c']); - }); - - test('transitiveReduction: 循環参照は対象外とする', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'a'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a']); - }); - - test('transitiveReduction: 循環ではないが簡約対象でもない', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'c', to: 'd'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'c>d']); - }); - - test('transitiveReduction: 循環からの関係は簡約対象にしない', () => { - const relations = [ - {from: 'a', to: 'b'}, - {from: 'b', to: 'a'}, // cycle - {from: 'b', to: 'c'}, - {from: 'a', to: 'c'}, - ]; - const result = pkg.transitiveReduction(relations); - assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); - }); - }); - - test.describe('UI', () => { test('setupTransitiveReductionControl: UIをセットアップする', () => { const doc = setupDocument(); const container = doc.createElement('div'); From 5a2ec5e2a7b76e4efd3ef66ad06a11d1eee10713 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 19:59:12 +0900 Subject: [PATCH 33/51] Reorder filter logic tests --- jig-core/src/test/js/package.test.js | 88 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 6591d7125..e97d55160 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -426,6 +426,41 @@ test.describe('package.js', () => { test.describe('フィルタ', () => { test.describe('ロジック', () => { + test('buildPackageRowVisibility: パッケージフィルタのみ', () => { + const visibility = pkg.buildPackageRowVisibility( + ['app.domain', 'app.other'], + 'app.domain' + ); + assert.deepEqual(visibility, [true, false]); + }); + + test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { + const rowFqns = ['app.domain', 'app.other']; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + [], + 'app.domain', + 0, + 'direct', + null + ); + assert.deepEqual(visibility, [true, false]); + }); + + test('buildRelatedRowVisibility: 関係する行のみ表示', () => { + const rowFqns = ['app.a', 'app.b', 'app.c']; + const relations = [{from: 'app.a', to: 'app.b'}]; + const visibility = pkg.buildRelatedRowVisibility( + rowFqns, + relations, + null, + 0, + 'direct', + 'app.a' + ); + assert.deepEqual(visibility, [true, true, false]); + }); + test('collectRelatedSet: directモードは隣接のみ含める', () => { const aggregationDepth = 0; const relatedFilterMode = 'direct'; @@ -467,21 +502,6 @@ test.describe('package.js', () => { {from: 'app.c', to: 'lib.d'}, ]; - test('getVisibleDiagramElements: packageFilterは配下のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); - - test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみ表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); - }); - - test('getVisibleDiagramElements: relatedFilter(all)は到達可能を表示', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); - assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); - }); - test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); @@ -501,39 +521,19 @@ test.describe('package.js', () => { assert.equal(filtered.uniqueRelations.length, 2); }); - test('buildPackageRowVisibility: パッケージフィルタのみ', () => { - const visibility = pkg.buildPackageRowVisibility( - ['app.domain', 'app.other'], - 'app.domain' - ); - assert.deepEqual(visibility, [true, false]); + test('getVisibleDiagramElements: packageFilterは配下のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { - const rowFqns = ['app.domain', 'app.other']; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - [], - 'app.domain', - 0, - 'direct', - null - ); - assert.deepEqual(visibility, [true, false]); + test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみ表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('buildRelatedRowVisibility: 関係する行のみ表示', () => { - const rowFqns = ['app.a', 'app.b', 'app.c']; - const relations = [{from: 'app.a', to: 'app.b'}]; - const visibility = pkg.buildRelatedRowVisibility( - rowFqns, - relations, - null, - 0, - 'direct', - 'app.a' - ); - assert.deepEqual(visibility, [true, true, false]); + test('getVisibleDiagramElements: relatedFilter(all)は到達可能を表示', () => { + const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); + assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); }); test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { From 4da57af33e237725b2f4b395b48fbdc7f74222a5 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:04:27 +0900 Subject: [PATCH 34/51] Align test order with helper dependencies --- jig-core/src/test/js/package.test.js | 236 +++++++++++++-------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index e97d55160..1deb15025 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -426,6 +426,26 @@ test.describe('package.js', () => { test.describe('フィルタ', () => { test.describe('ロジック', () => { + test('normalizePackageFilterValue: 空文字はnull', () => { + assert.equal(pkg.normalizePackageFilterValue(''), null); + assert.equal(pkg.normalizePackageFilterValue(' '), null); + assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); + }); + + test('normalizeAggregationDepthValue: 数値化する', () => { + assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); + assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); + assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); + }); + + test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { + const candidate = pkg.findDefaultPackageFilterCandidate([ + {fqn: 'app.domain.core'}, + {fqn: 'app.domain.sub'}, + ]); + assert.equal(candidate, 'app.domain'); + }); + test('buildPackageRowVisibility: パッケージフィルタのみ', () => { const visibility = pkg.buildPackageRowVisibility( ['app.domain', 'app.other'], @@ -461,6 +481,18 @@ test.describe('package.js', () => { assert.deepEqual(visibility, [true, true, false]); }); + const packages = [ + {fqn: 'app.a'}, + {fqn: 'app.b'}, + {fqn: 'app.c'}, + {fqn: 'lib.d'}, + ]; + const relations = [ + {from: 'app.a', to: 'app.b'}, + {from: 'app.b', to: 'app.c'}, + {from: 'app.c', to: 'lib.d'}, + ]; + test('collectRelatedSet: directモードは隣接のみ含める', () => { const aggregationDepth = 0; const relatedFilterMode = 'direct'; @@ -490,18 +522,6 @@ test.describe('package.js', () => { ); }); - const packages = [ - {fqn: 'app.a'}, - {fqn: 'app.b'}, - {fqn: 'app.c'}, - {fqn: 'lib.d'}, - ]; - const relations = [ - {from: 'app.a', to: 'app.b'}, - {from: 'app.b', to: 'app.c'}, - {from: 'app.c', to: 'lib.d'}, - ]; - test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); @@ -535,26 +555,6 @@ test.describe('package.js', () => { const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); }); - - test('findDefaultPackageFilterCandidate: ドメイン候補を返す', () => { - const candidate = pkg.findDefaultPackageFilterCandidate([ - {fqn: 'app.domain.core'}, - {fqn: 'app.domain.sub'}, - ]); - assert.equal(candidate, 'app.domain'); - }); - - test('normalizePackageFilterValue: 空文字はnull', () => { - assert.equal(pkg.normalizePackageFilterValue(''), null); - assert.equal(pkg.normalizePackageFilterValue(' '), null); - assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); - }); - - test('normalizeAggregationDepthValue: 数値化する', () => { - assert.equal(pkg.normalizeAggregationDepthValue('2'), 2); - assert.equal(pkg.normalizeAggregationDepthValue('0'), 0); - assert.equal(pkg.normalizeAggregationDepthValue('abc'), 0); - }); }); test.describe('UI', () => { @@ -579,27 +579,28 @@ test.describe('package.js', () => { assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); }); - test('setupPackageFilterControls: 適用/解除をハンドリング', () => { + test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); - setupDiagramEnvironment(doc, testContext); setPackageData({ - packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], - relations: [], + packages: [ + {fqn: 'app.a'}, + {fqn: 'app.b'}, + {fqn: 'app.c'}, + ], + relations: [ + {from: 'app.a', to: 'app.b'}, + ], }, testContext); - doc.selectorsAll.set('#package-table tbody tr', []); - createDepthSelect(doc); - - const {input, applyButton, clearButton} = createPackageFilterControls(doc); - - pkg.setupPackageFilterControls(testContext); + const rows = buildPackageRows(doc, ['app.a', 'app.b', 'app.c']); + testContext.aggregationDepth = 0; + testContext.relatedFilterMode = 'direct'; + testContext.packageFilterFqn = null; - input.value = 'app.domain'; - applyButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, 'app.domain'); + pkg.applyRelatedFilterToTable('app.a', testContext); - clearButton.eventListeners.get('click')(); - assert.equal(testContext.packageFilterFqn, null); - assert.equal(input.value, ''); + assert.equal(rows[0].classList.contains('hidden'), false); + assert.equal(rows[1].classList.contains('hidden'), false); + assert.equal(rows[2].classList.contains('hidden'), true); }); test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { @@ -623,28 +624,27 @@ test.describe('package.js', () => { assert.equal(input.value, 'app.domain'); }); - test('applyRelatedFilterToTable: 関係する行のみ表示', () => { + test('setupPackageFilterControls: 適用/解除をハンドリング', () => { const doc = setupDocument(); + setupDiagramEnvironment(doc, testContext); setPackageData({ - packages: [ - {fqn: 'app.a'}, - {fqn: 'app.b'}, - {fqn: 'app.c'}, - ], - relations: [ - {from: 'app.a', to: 'app.b'}, - ], + packages: [{fqn: 'app.domain', name: 'Domain', classCount: 1}], + relations: [], }, testContext); - const rows = buildPackageRows(doc, ['app.a', 'app.b', 'app.c']); - testContext.aggregationDepth = 0; - testContext.relatedFilterMode = 'direct'; - testContext.packageFilterFqn = null; + doc.selectorsAll.set('#package-table tbody tr', []); + createDepthSelect(doc); - pkg.applyRelatedFilterToTable('app.a', testContext); + const {input, applyButton, clearButton} = createPackageFilterControls(doc); - assert.equal(rows[0].classList.contains('hidden'), false); - assert.equal(rows[1].classList.contains('hidden'), false); - assert.equal(rows[2].classList.contains('hidden'), true); + pkg.setupPackageFilterControls(testContext); + + input.value = 'app.domain'; + applyButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, 'app.domain'); + + clearButton.eventListeners.get('click')(); + assert.equal(testContext.packageFilterFqn, null); + assert.equal(input.value, ''); }); }); }); @@ -721,59 +721,6 @@ test.describe('package.js', () => { ]); }); - test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { - const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); - const result = pkg.buildDiagramEdgeLines( - [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], - ensureNodeId - ); - assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); - assert.equal(result.linkStyles.length, 1); - }); - - test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { - const visibleFqns = ['com.example.a', 'com.example.b']; - const nodeIdByFqn = new Map([ - ['com.example.a', 'P0'], - ['com.example.b', 'P1'], - ]); - - const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); - - assert.equal(rootGroup.children.has('com.example'), true); - }); - - test('buildSubgraphLines: サブグラフ行を生成する', () => { - const rootGroup = { - key: '', - nodes: [], - children: new Map([ - ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], - ]), - }; - const addNodeLines = (lines, nodeId) => { - lines.push(`node ${nodeId}`); - }; - - const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); - - assert.equal(lines.some(line => line.includes('node P0')), true); - }); - - test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { - const label = pkg.buildDiagramNodeLabel( - 'com.example.domain.model', - 'com.example.domain.model', - 'com.example.domain' - ); - assert.equal(label, 'model'); - }); - - test('buildDiagramNodeTooltip: FQNを返す', () => { - assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); - assert.equal(pkg.buildDiagramNodeTooltip(null), ''); - }); - test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { const items = pkg.buildMutualDependencyItems( new Set(['app.alpha::app.beta']), @@ -842,6 +789,59 @@ test.describe('package.js', () => { const result = pkg.transitiveReduction(relations); assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); }); + + test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { + const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); + const result = pkg.buildDiagramEdgeLines( + [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], + ensureNodeId + ); + assert.equal(result.edgeLines.some(line => line.includes('<-->')), true); + assert.equal(result.linkStyles.length, 1); + }); + + test('buildDiagramNodeLabel: サブグラフ配下のラベルを短縮する', () => { + const label = pkg.buildDiagramNodeLabel( + 'com.example.domain.model', + 'com.example.domain.model', + 'com.example.domain' + ); + assert.equal(label, 'model'); + }); + + test('buildDiagramNodeTooltip: FQNを返す', () => { + assert.equal(pkg.buildDiagramNodeTooltip('com.example.domain'), 'com.example.domain'); + assert.equal(pkg.buildDiagramNodeTooltip(null), ''); + }); + + test('buildDiagramGroupTree: 共通プレフィックスでグループ化する', () => { + const visibleFqns = ['com.example.a', 'com.example.b']; + const nodeIdByFqn = new Map([ + ['com.example.a', 'P0'], + ['com.example.b', 'P1'], + ]); + + const rootGroup = pkg.buildDiagramGroupTree(visibleFqns, nodeIdByFqn); + + assert.equal(rootGroup.children.has('com.example'), true); + }); + + test('buildSubgraphLines: サブグラフ行を生成する', () => { + const rootGroup = { + key: '', + nodes: [], + children: new Map([ + ['com.example', {key: 'com.example', nodes: ['P0', 'P1'], children: new Map()}], + ]), + }; + const addNodeLines = (lines, nodeId) => { + lines.push(`node ${nodeId}`); + }; + + const lines = pkg.buildSubgraphLines(rootGroup, addNodeLines, text => text); + + assert.equal(lines.some(line => line.includes('node P0')), true); + }); }); test.describe('UI', () => { From 6e05a861fa13039ad44201a549b283739546ad6b Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:06:15 +0900 Subject: [PATCH 35/51] Normalize test ordering across sections --- jig-core/src/test/js/package.test.js | 92 ++++++++++++++-------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 1deb15025..52c0d085d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -270,16 +270,6 @@ test.describe('package.js', () => { test.describe('データ取得/整形', () => { test.describe('ロジック', () => { - test('getPackageSummaryData: 配列/オブジェクト両対応', () => { - setupDocument(); - setPackageData([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); - - const data = pkg.getPackageSummaryData(testContext); - - assert.equal(data.packages.length, 1); - assert.equal(data.relations.length, 0); - }); - test('parsePackageSummaryData: 配列/オブジェクト両対応', () => { const arrayData = pkg.parsePackageSummaryData(JSON.stringify([ {fqn: 'app.a', name: 'A', classCount: 1, description: ''}, @@ -295,6 +285,16 @@ test.describe('package.js', () => { assert.equal(objectData.relations.length, 1); }); + test('getPackageSummaryData: 配列/オブジェクト両対応', () => { + setupDocument(); + setPackageData([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); + + const data = pkg.getPackageSummaryData(testContext); + + assert.equal(data.packages.length, 1); + assert.equal(data.relations.length, 0); + }); + test('getPackageDepth: 深さを返す', () => { assert.equal(pkg.getPackageDepth(''), 0); assert.equal(pkg.getPackageDepth('(default)'), 0); @@ -558,27 +558,6 @@ test.describe('package.js', () => { }); test.describe('UI', () => { - test('renderRelatedFilterTarget: 対象表示を更新する', () => { - const mockTarget = { textContent: '' }; - const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); - const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); - - test.mock.method(pkg.dom, 'getRelatedFilterTarget', getRelatedFilterTargetMock); - test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); - - testContext.relatedFilterFqn = null; - pkg.renderRelatedFilterTarget(testContext); - assert.equal(mockTarget.textContent, '未選択'); - assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); - assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); - - testContext.relatedFilterFqn = 'app.domain'; - pkg.renderRelatedFilterTarget(testContext); - assert.equal(mockTarget.textContent, 'app.domain'); - assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); - assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); - }); - test('applyRelatedFilterToTable: 関係する行のみ表示', () => { const doc = setupDocument(); setPackageData({ @@ -603,6 +582,27 @@ test.describe('package.js', () => { assert.equal(rows[2].classList.contains('hidden'), true); }); + test('renderRelatedFilterTarget: 対象表示を更新する', () => { + const mockTarget = { textContent: '' }; + const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); + const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); + + test.mock.method(pkg.dom, 'getRelatedFilterTarget', getRelatedFilterTargetMock); + test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); + + testContext.relatedFilterFqn = null; + pkg.renderRelatedFilterTarget(testContext); + assert.equal(mockTarget.textContent, '未選択'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); + + testContext.relatedFilterFqn = 'app.domain'; + pkg.renderRelatedFilterTarget(testContext); + assert.equal(mockTarget.textContent, 'app.domain'); + assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); + assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); + }); + test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); @@ -706,21 +706,6 @@ test.describe('package.js', () => { test.describe('ダイアグラム', () => { test.describe('ロジック', () => { - test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { - const stats = new Map([ - [0, {packageCount: 2, relationCount: 1}], - [1, {packageCount: 1, relationCount: 1}], - [2, {packageCount: 1, relationCount: 0}], - ]); - - const options = pkg.buildAggregationDepthOptions(stats, 2); - - assert.deepEqual(options, [ - {value: '0', text: '集約なし(P2 / R1)'}, - {value: '1', text: '深さ1(P1 / R1)'}, - ]); - }); - test('buildMutualDependencyItems: 相互依存の原因を整形する', () => { const items = pkg.buildMutualDependencyItems( new Set(['app.alpha::app.beta']), @@ -842,6 +827,21 @@ test.describe('package.js', () => { assert.equal(lines.some(line => line.includes('node P0')), true); }); + + test('buildAggregationDepthOptions: 集約オプションを組み立てる', () => { + const stats = new Map([ + [0, {packageCount: 2, relationCount: 1}], + [1, {packageCount: 1, relationCount: 1}], + [2, {packageCount: 1, relationCount: 0}], + ]); + + const options = pkg.buildAggregationDepthOptions(stats, 2); + + assert.deepEqual(options, [ + {value: '0', text: '集約なし(P2 / R1)'}, + {value: '1', text: '深さ1(P1 / R1)'}, + ]); + }); }); test.describe('UI', () => { From 57206e0a0787b0969626f6ff71802188b62d4773 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:07:42 +0900 Subject: [PATCH 36/51] Unify test naming tone --- jig-core/src/test/js/package.test.js | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 52c0d085d..dc068a623 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -270,7 +270,7 @@ test.describe('package.js', () => { test.describe('データ取得/整形', () => { test.describe('ロジック', () => { - test('parsePackageSummaryData: 配列/オブジェクト両対応', () => { + test('parsePackageSummaryData: 配列/オブジェクトに対応する', () => { const arrayData = pkg.parsePackageSummaryData(JSON.stringify([ {fqn: 'app.a', name: 'A', classCount: 1, description: ''}, ])); @@ -285,7 +285,7 @@ test.describe('package.js', () => { assert.equal(objectData.relations.length, 1); }); - test('getPackageSummaryData: 配列/オブジェクト両対応', () => { + test('getPackageSummaryData: 配列/オブジェクトに対応する', () => { setupDocument(); setPackageData([{fqn: 'app.a', name: 'A', classCount: 1, description: ''}], testContext); @@ -364,7 +364,7 @@ test.describe('package.js', () => { assert.equal(depth1.relationCount, 0); }); - test('buildAggregationStatsForFilters: directモードの複合集計', () => { + test('buildAggregationStatsForFilters: directモードの複合集計を行う', () => { const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -393,7 +393,7 @@ test.describe('package.js', () => { assert.equal(depth0.relationCount, 1); }); - test('buildAggregationStatsForFilters: allモードの複合集計', () => { + test('buildAggregationStatsForFilters: allモードの複合集計を行う', () => { const packages = [ {fqn: 'app.domain.a'}, {fqn: 'app.domain.b'}, @@ -426,7 +426,7 @@ test.describe('package.js', () => { test.describe('フィルタ', () => { test.describe('ロジック', () => { - test('normalizePackageFilterValue: 空文字はnull', () => { + test('normalizePackageFilterValue: 空文字はnullを返す', () => { assert.equal(pkg.normalizePackageFilterValue(''), null); assert.equal(pkg.normalizePackageFilterValue(' '), null); assert.equal(pkg.normalizePackageFilterValue('app.domain'), 'app.domain'); @@ -446,7 +446,7 @@ test.describe('package.js', () => { assert.equal(candidate, 'app.domain'); }); - test('buildPackageRowVisibility: パッケージフィルタのみ', () => { + test('buildPackageRowVisibility: パッケージフィルタのみを表示する', () => { const visibility = pkg.buildPackageRowVisibility( ['app.domain', 'app.other'], 'app.domain' @@ -454,7 +454,7 @@ test.describe('package.js', () => { assert.deepEqual(visibility, [true, false]); }); - test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみ', () => { + test('buildRelatedRowVisibility: 関連フィルタ未指定はパッケージフィルタのみを表示する', () => { const rowFqns = ['app.domain', 'app.other']; const visibility = pkg.buildRelatedRowVisibility( rowFqns, @@ -467,7 +467,7 @@ test.describe('package.js', () => { assert.deepEqual(visibility, [true, false]); }); - test('buildRelatedRowVisibility: 関係する行のみ表示', () => { + test('buildRelatedRowVisibility: 関係する行のみ表示する', () => { const rowFqns = ['app.a', 'app.b', 'app.c']; const relations = [{from: 'app.a', to: 'app.b'}]; const visibility = pkg.buildRelatedRowVisibility( @@ -493,7 +493,7 @@ test.describe('package.js', () => { {from: 'app.c', to: 'lib.d'}, ]; - test('collectRelatedSet: directモードは隣接のみ含める', () => { + test('collectRelatedSet: directモードは隣接のみを含める', () => { const aggregationDepth = 0; const relatedFilterMode = 'direct'; const relations = [ @@ -541,24 +541,24 @@ test.describe('package.js', () => { assert.equal(filtered.uniqueRelations.length, 2); }); - test('getVisibleDiagramElements: packageFilterは配下のみ表示', () => { + test('getVisibleDiagramElements: packageFilterは配下のみを表示する', () => { const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみ表示', () => { + test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみを表示する', () => { const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('getVisibleDiagramElements: relatedFilter(all)は到達可能を表示', () => { + test('getVisibleDiagramElements: relatedFilter(all)は到達可能なものを表示する', () => { const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); }); }); test.describe('UI', () => { - test('applyRelatedFilterToTable: 関係する行のみ表示', () => { + test('applyRelatedFilterToTable: 関係する行のみ表示する', () => { const doc = setupDocument(); setPackageData({ packages: [ @@ -603,7 +603,7 @@ test.describe('package.js', () => { assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); }); - test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用', () => { + test('applyDefaultPackageFilterIfPresent: ドメインがあれば適用する', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); setPackageData({ @@ -624,7 +624,7 @@ test.describe('package.js', () => { assert.equal(input.value, 'app.domain'); }); - test('setupPackageFilterControls: 適用/解除をハンドリング', () => { + test('setupPackageFilterControls: 適用/解除を扱う', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); setPackageData({ @@ -775,7 +775,7 @@ test.describe('package.js', () => { assert.deepEqual(result.map(r => `${r.from}>${r.to}`).sort(), ['a>b', 'a>c', 'b>a', 'b>c']); }); - test('buildDiagramEdgeLines: 相互依存で双方向リンクを生成', () => { + test('buildDiagramEdgeLines: 相互依存の双方向リンクを生成する', () => { const {ensureNodeId} = pkg.buildDiagramNodeMaps(new Set(['a', 'b']), new Map()); const result = pkg.buildDiagramEdgeLines( [{from: 'a', to: 'b'}, {from: 'b', to: 'a'}], @@ -845,7 +845,7 @@ test.describe('package.js', () => { }); test.describe('UI', () => { - test('renderMutualDependencyList: 相互依存と原因を一覧化', () => { + test('renderMutualDependencyList: 相互依存と原因を一覧化する', () => { const doc = setupDocument(); const container = new Element('div', doc); doc.elementsById.set('mutual-dependency-list', container); @@ -866,7 +866,7 @@ test.describe('package.js', () => { assert.equal(container.children[0].children[1].tagName, 'ul'); }); - test('renderPackageDiagram: 相互依存を含む描画', () => { + test('renderPackageDiagram: 相互依存を含めて描画する', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); setPackageData({ @@ -889,7 +889,7 @@ test.describe('package.js', () => { assert.equal(mutual.children.length > 0, true); }); - test('renderPackageDiagram: エッジ数超過で保留/エラー表示', () => { + test('renderPackageDiagram: エッジ数超過時は保留/エラー表示する', () => { const doc = setupDocument(); // setupDiagramEnvironmentはtestContext.diagramElementを設定する。 // そのdiagramElementがdomヘルパーによって操作されることをモックする。 From a7aa807f5e605e010e08d6d13eb6bcdb2ab152fd Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:11:49 +0900 Subject: [PATCH 37/51] Reorder package.js functions --- .../resources/templates/assets/package.js | 765 +++++++++--------- 1 file changed, 380 insertions(+), 385 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index f9819f85d..15cdf6eb7 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -73,63 +73,6 @@ const dom = { getNodeTextContent: (element) => { return element ? element.textContent : ''; }, }; -function getOrCreateDiagramErrorBox(diagram) { - let errorBox = dom.getDiagramErrorBox(); - if (errorBox) return errorBox; - return dom.createDiagramErrorBox(diagram); -} - -function showDiagramErrorMessage(message, withAction, err, hash, context) { - const diagram = context.diagramElement; - if (!diagram) return; - console.error(message); - if (err) { - console.error(err); - } - if (hash) { - console.error('Mermaid error location:', hash.line, hash.loc); - } - const errorBox = getOrCreateDiagramErrorBox(diagram); - const messageNode = dom.getDiagramErrorMessageNode(); - const actionNode = dom.getDiagramErrorActionNode(); - dom.setNodeTextContent(messageNode, message); - if (actionNode) { - dom.setNodeDisplay(actionNode, withAction ? '' : 'none'); - if (withAction) { - dom.setNodeOnClick(actionNode, function () { - if (!context.pendingDiagramRender) return; - renderDiagramSvg(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); - context.pendingDiagramRender = null; - }); - } else { - dom.setNodeOnClick(actionNode, null); - } - } - dom.setNodeDisplay(errorBox, ''); - dom.setDiagramElementDisplay(diagram, 'none'); -} - -function hideDiagramErrorMessage(diagram) { - const errorBox = getOrCreateDiagramErrorBox(diagram); - const messageNode = dom.getDiagramErrorMessageNode(); - const actionNode = dom.getDiagramErrorActionNode(); - dom.setNodeTextContent(messageNode, ''); - dom.setNodeDisplay(actionNode, 'none'); - dom.setNodeOnClick(actionNode, null); - dom.setNodeDisplay(errorBox, 'none'); - dom.setDiagramElementDisplay(diagram, ''); -} - -function renderDiagramSvg(text, maxEdges, context) { - const diagram = context.diagramElement; - if (!diagram || !window.mermaid) return; - hideDiagramErrorMessage(diagram); - dom.removeDiagramAttribute(diagram, 'data-processed'); - dom.setDiagramContent(diagram, text); - mermaid.initialize({startOnLoad: false, securityLevel: 'loose', maxEdges: maxEdges}); - mermaid.run({nodes: [diagram]}); -} - function getPackageSummaryData(context) { if (context.packageSummaryCache) return context.packageSummaryCache; const jsonText = dom.getNodeTextContent(dom.getPackageDataScript()); @@ -215,6 +158,21 @@ function buildAggregationStatsForPackageFilter(packages, relations, packageFilte return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } +function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, aggregationDepth, relatedFilterMode) { + if (!rootFqn) { + return buildAggregationStats(packages, relations, maxDepth); + } + const aggregatedRoot = getAggregatedFqn(rootFqn, aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, relations, aggregationDepth, relatedFilterMode); + const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth))); + const relatedRelations = relations.filter(relation => { + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); + return relatedSet.has(from) && relatedSet.has(to); + }); + return buildAggregationStats(relatedPackages, relatedRelations, maxDepth); +} + function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, relatedFilterFqn, maxDepth, aggregationDepth, relatedFilterMode) { const withinPackageFilter = fqn => { if (!packageFilterFqn) return true; @@ -249,45 +207,188 @@ function buildAggregationStatsForFilters(packages, relations, packageFilterFqn, return buildAggregationStats(filteredPackages, filteredRelations, maxDepth); } -function buildAggregationStatsForRelated(packages, relations, rootFqn, maxDepth, aggregationDepth, relatedFilterMode) { - if (!rootFqn) { - return buildAggregationStats(packages, relations, maxDepth); +function normalizePackageFilterValue(value) { + const trimmed = (value ?? '').trim(); + return trimmed ? trimmed : null; +} + +function normalizeAggregationDepthValue(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function findDefaultPackageFilterCandidate(packages) { + const domainRoots = packages + .map(item => item.fqn) + .map(fqn => { + const parts = fqn.split('.'); + const domainIndex = parts.indexOf('domain'); + if (domainIndex === -1) return null; + return parts.slice(0, domainIndex + 1).join('.'); + }) + .filter(Boolean); + if (domainRoots.length === 0) return null; + return domainRoots.reduce((best, current) => { + const bestDepth = best.split('.').length; + const currentDepth = current.split('.').length; + return currentDepth < bestDepth ? current : best; + }); +} + +function buildPackageRowVisibility(rowFqns, packageFilterFqn) { + const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + return rowFqns.map(fqn => + !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(filterPrefix) + ); +} + +function buildRelatedRowVisibility(rowFqns, relations, packageFilterFqn, aggregationDepth, relatedFilterMode, relatedFilterFqn) { + const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + const withinPackageFilter = rowFqn => + !packageFilterFqn || rowFqn === packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); + + if (!relatedFilterFqn) { + return rowFqns.map(rowFqn => withinPackageFilter(rowFqn)); } - const aggregatedRoot = getAggregatedFqn(rootFqn, aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, relations, aggregationDepth, relatedFilterMode); - const relatedPackages = packages.filter(item => relatedSet.has(getAggregatedFqn(item.fqn, aggregationDepth))); - const relatedRelations = relations.filter(relation => { + + const filteredRelations = packageFilterFqn + ? relations.filter(relation => + withinPackageFilter(relation.from) && withinPackageFilter(relation.to) + ) + : relations; + const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, aggregationDepth); + const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, aggregationDepth, relatedFilterMode); + return rowFqns.map(rowFqn => { + const aggregatedRow = getAggregatedFqn(rowFqn, aggregationDepth); + return withinPackageFilter(rowFqn) && relatedSet.has(aggregatedRow); + }); +} + +function collectRelatedSet(root, relations, aggregationDepth, relatedFilterMode) { + if (!root) return new Set(); + if (relatedFilterMode === 'direct') { + const relatedSet = new Set([root]); + relations.forEach(relation => { + const from = getAggregatedFqn(relation.from, aggregationDepth); + const to = getAggregatedFqn(relation.to, aggregationDepth); + if (from === root) relatedSet.add(to); + if (to === root) relatedSet.add(from); + }); + return relatedSet; + } + + const adjacency = new Map(); + const addEdge = (from, to) => { + if (!adjacency.has(from)) adjacency.set(from, new Set()); + adjacency.get(from).add(to); + }; + relations.forEach(relation => { const from = getAggregatedFqn(relation.from, aggregationDepth); const to = getAggregatedFqn(relation.to, aggregationDepth); - return relatedSet.has(from) && relatedSet.has(to); + addEdge(from, to); + if (relatedFilterMode === 'all') { + addEdge(to, from); + } }); - return buildAggregationStats(relatedPackages, relatedRelations, maxDepth); + + const relatedSet = new Set([root]); + const queue = [root]; + while (queue.length) { + const current = queue.shift(); + const nextSet = adjacency.get(current); + if (!nextSet) continue; + nextSet.forEach(next => { + if (relatedSet.has(next)) return; + relatedSet.add(next); + queue.push(next); + }); + } + return relatedSet; } -function renderPackageTable(context) { - const {packages, relations} = getPackageSummaryData(context); - const rows = buildPackageTableRows(packages, relations); - const rowSpecs = buildPackageTableRowSpecs(rows); +function buildFilteredDiagramRelations(packages, relations, causeRelationEvidence, packageFilterFqn, aggregationDepth, transitiveReductionEnabled) { + const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; + const withinPackageFilter = fqn => + !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); + const visiblePackages = packageFilterFqn + ? packages.filter(item => withinPackageFilter(item.fqn)) + : packages; + const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); + const filteredRelations = packageFilterFqn + ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) + : relations; + const filteredCauseRelationEvidence = packageFilterFqn + ? causeRelationEvidence.filter(relation => { + const fromPackage = getPackageFqnFromTypeFqn(relation.from); + const toPackage = getPackageFqnFromTypeFqn(relation.to); + return withinPackageFilter(fromPackage) && withinPackageFilter(toPackage); + }) + : causeRelationEvidence; + const visibleRelations = filteredRelations + .map(relation => ({ + from: getAggregatedFqn(relation.from, aggregationDepth), + to: getAggregatedFqn(relation.to, aggregationDepth), + })) + .filter(relation => relation.from !== relation.to); + const uniqueRelationMap = new Map(); + visibleRelations.forEach(relation => { + uniqueRelationMap.set(`${relation.from}::${relation.to}`, relation); + }); + let uniqueRelations = Array.from(uniqueRelationMap.values()); - const tbody = dom.getPackageTableBody(); + if (transitiveReductionEnabled) { + uniqueRelations = transitiveReduction(uniqueRelations); + } - const input = dom.getPackageFilterInput(); - const applyFilter = fqn => { - if (input) { - input.value = fqn; - } - context.packageFilterFqn = fqn; - renderDiagramAndTable(context); - renderRelatedFilterTarget(context); - }; - const applyRelatedFilterForRow = fqn => { - applyRelatedFilter(fqn, context); - }; + return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; +} - rowSpecs.forEach(spec => { - const tr = createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow); - tbody.appendChild(tr); +function applyRelatedFilterToDiagramRelations(uniqueRelations, visibleSet, aggregatedRoot, aggregationDepth, relatedFilterMode) { + const nextVisibleSet = new Set(visibleSet); + let nextRelations = uniqueRelations; + if (aggregatedRoot) { + const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); + if (relatedFilterMode === 'direct') { + nextRelations = uniqueRelations.filter(relation => + relation.from === aggregatedRoot || relation.to === aggregatedRoot + ); + } else { + nextRelations = uniqueRelations.filter(relation => + relatedSet.has(relation.from) && relatedSet.has(relation.to) + ); + } + nextVisibleSet.clear(); + relatedSet.forEach(value => nextVisibleSet.add(value)); + } + nextRelations.forEach(relation => { + nextVisibleSet.add(relation.from); + nextVisibleSet.add(relation.to); }); + return {uniqueRelations: nextRelations, visibleSet: nextVisibleSet}; +} + +function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { + const base = buildFilteredDiagramRelations( + packages, + relations, + causeRelationEvidence, + packageFilterFqn, + aggregationDepth, + transitiveReductionEnabled + ); + const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; + const {uniqueRelations, visibleSet} = applyRelatedFilterToDiagramRelations( + base.uniqueRelations, + base.visibleSet, + aggregatedRoot, + aggregationDepth, + relatedFilterMode + ); + return { + uniqueRelations, + visibleSet, + filteredCauseRelationEvidence: base.filteredCauseRelationEvidence, + }; } function buildPackageTableRows(packages, relations) { @@ -314,6 +415,19 @@ function buildPackageTableRowSpecs(rows) { })); } +function buildPackageTableActionSpecs() { + return { + filter: { + ariaLabel: 'このパッケージで絞り込み', + screenReaderText: '絞り込み', + }, + related: { + ariaLabel: '関連のみ表示', + screenReaderText: '関連のみ表示', + }, + }; +} + function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { const tr = document.createElement('tr'); const actionSpecs = buildPackageTableActionSpecs(); @@ -371,17 +485,30 @@ function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { return tr; } -function buildPackageTableActionSpecs() { - return { - filter: { - ariaLabel: 'このパッケージで絞り込み', - screenReaderText: '絞り込み', - }, - related: { - ariaLabel: '関連のみ表示', - screenReaderText: '関連のみ表示', - }, +function renderPackageTable(context) { + const {packages, relations} = getPackageSummaryData(context); + const rows = buildPackageTableRows(packages, relations); + const rowSpecs = buildPackageTableRowSpecs(rows); + + const tbody = dom.getPackageTableBody(); + + const input = dom.getPackageFilterInput(); + const applyFilter = fqn => { + if (input) { + input.value = fqn; + } + context.packageFilterFqn = fqn; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); + }; + const applyRelatedFilterForRow = fqn => { + applyRelatedFilter(fqn, context); }; + + rowSpecs.forEach(spec => { + const tr = createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow); + tbody.appendChild(tr); + }); } function applyPackageFilterToTable(packageFilterFqn) { @@ -396,13 +523,6 @@ function applyPackageFilterToTable(packageFilterFqn) { }); } -function buildPackageRowVisibility(rowFqns, packageFilterFqn) { - const filterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; - return rowFqns.map(fqn => - !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(filterPrefix) - ); -} - function applyRelatedFilterToTable(fqn, context) { const rows = dom.getPackageTableRows(); const {relations} = getPackageSummaryData(context); @@ -423,113 +543,27 @@ function applyRelatedFilterToTable(fqn, context) { }); } -function buildRelatedRowVisibility(rowFqns, relations, packageFilterFqn, aggregationDepth, relatedFilterMode, relatedFilterFqn) { - const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; - const withinPackageFilter = rowFqn => - !packageFilterFqn || rowFqn === packageFilterFqn || rowFqn.startsWith(packageFilterPrefix); - - if (!relatedFilterFqn) { - return rowFqns.map(rowFqn => withinPackageFilter(rowFqn)); - } - - const filteredRelations = packageFilterFqn - ? relations.filter(relation => - withinPackageFilter(relation.from) && withinPackageFilter(relation.to) - ) - : relations; - const aggregatedRoot = getAggregatedFqn(relatedFilterFqn, aggregationDepth); - const relatedSet = collectRelatedSet(aggregatedRoot, filteredRelations, aggregationDepth, relatedFilterMode); - return rowFqns.map(rowFqn => { - const aggregatedRow = getAggregatedFqn(rowFqn, aggregationDepth); - return withinPackageFilter(rowFqn) && relatedSet.has(aggregatedRow); - }); -} - function renderRelatedFilterTarget(context) { const target = dom.getRelatedFilterTarget(); dom.setRelatedFilterTargetText(target, context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'); } -function collectRelatedSet(root, relations, aggregationDepth, relatedFilterMode) { - if (!root) return new Set(); - if (relatedFilterMode === 'direct') { - const relatedSet = new Set([root]); - relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); - if (from === root) relatedSet.add(to); - if (to === root) relatedSet.add(from); - }); - return relatedSet; - } - - const adjacency = new Map(); - const addEdge = (from, to) => { - if (!adjacency.has(from)) adjacency.set(from, new Set()); - adjacency.get(from).add(to); - }; - relations.forEach(relation => { - const from = getAggregatedFqn(relation.from, aggregationDepth); - const to = getAggregatedFqn(relation.to, aggregationDepth); - addEdge(from, to); - if (relatedFilterMode === 'all') { - addEdge(to, from); - } - }); - - const relatedSet = new Set([root]); - const queue = [root]; - while (queue.length) { - const current = queue.shift(); - const nextSet = adjacency.get(current); - if (!nextSet) continue; - nextSet.forEach(next => { - if (relatedSet.has(next)) return; - relatedSet.add(next); - queue.push(next); - }); - } - return relatedSet; -} - -function renderDiagramAndTable(context) { - renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); - applyRelatedFilterToTable(context.relatedFilterFqn, context); - updateAggregationDepthOptions(getMaxPackageDepth(context), context); +function applyRelatedFilter(fqn, context) { + context.relatedFilterFqn = fqn; + renderDiagramAndTable(context); + renderRelatedFilterTarget(context); } -function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) { - const container = dom.getMutualDependencyList(); - if (!container) return; - const items = buildMutualDependencyItems(mutualPairs, causeRelationEvidence, context.aggregationDepth); - if (items.length === 0) { - container.style.display = 'none'; - container.innerHTML = ''; - return; - } - - container.style.display = ''; - const details = document.createElement('details'); - const summary = document.createElement('summary'); - summary.textContent = '相互依存と原因'; - const list = document.createElement('ul'); - items.forEach(item => { - const itemNode = document.createElement('li'); - const pair = document.createElement('div'); - pair.className = 'pair'; - pair.textContent = item.pairLabel; - itemNode.appendChild(pair); - if (item.causes.length > 0) { - const detailBody = document.createElement('pre'); - detailBody.textContent = item.causes.join('\n'); - itemNode.appendChild(detailBody); - } - list.appendChild(itemNode); - }); - container.innerHTML = ''; - details.appendChild(summary); - details.appendChild(list); - container.appendChild(details); +function applyDefaultPackageFilterIfPresent(context) { + const input = dom.getPackageFilterInput(); + if (!input || input.value.trim()) return false; + const {packages} = getPackageSummaryData(context); + const candidate = findDefaultPackageFilterCandidate(packages); + if (!candidate) return false; + input.value = candidate; + context.packageFilterFqn = candidate; + renderDiagramAndTable(context); + return true; } function buildMutualDependencyItems(mutualPairs, causeRelationEvidence, aggregationDepth) { @@ -790,121 +824,127 @@ function buildDiagramGroupTree(visibleFqns, nodeIdByFqn) { } current = current.children.get(key); } - current.nodes.push(nodeIdByFqn.get(fqn)); - }); - return rootGroup; + current.nodes.push(nodeIdByFqn.get(fqn)); + }); + return rootGroup; +} + +function buildSubgraphLines(rootGroup, addNodeLines, escapeMermaidText) { + const lines = []; + let groupIndex = 0; + const renderGroup = (group, isRoot, parentSubgraphFqnForNodes) => { + group.nodes.forEach(nodeId => addNodeLines(lines, nodeId, parentSubgraphFqnForNodes)); + const childKeys = Array.from(group.children.keys()).sort(); + if (isRoot && group.nodes.length === 0 && childKeys.length === 1) { + renderGroup(group.children.get(childKeys[0]), false, parentSubgraphFqnForNodes); + return; + } + childKeys.forEach(key => { + const child = group.children.get(key); + const childNodeCount = child.nodes.length + child.children.size; + if (childNodeCount <= 1) { + renderGroup(child, false, parentSubgraphFqnForNodes); + return; + } + const groupId = `G${groupIndex++}`; + lines.push(`subgraph ${groupId}["${escapeMermaidText(child.key)}"]`); + renderGroup(child, false, child.key); + lines.push('end'); + }); + }; + renderGroup(rootGroup, true, rootGroup.key); + return lines; +} + +function getOrCreateDiagramErrorBox(diagram) { + let errorBox = dom.getDiagramErrorBox(); + if (errorBox) return errorBox; + return dom.createDiagramErrorBox(diagram); +} + +function showDiagramErrorMessage(message, withAction, err, hash, context) { + const diagram = context.diagramElement; + if (!diagram) return; + console.error(message); + if (err) { + console.error(err); + } + if (hash) { + console.error('Mermaid error location:', hash.line, hash.loc); + } + const errorBox = getOrCreateDiagramErrorBox(diagram); + const messageNode = dom.getDiagramErrorMessageNode(); + const actionNode = dom.getDiagramErrorActionNode(); + dom.setNodeTextContent(messageNode, message); + if (actionNode) { + dom.setNodeDisplay(actionNode, withAction ? '' : 'none'); + if (withAction) { + dom.setNodeOnClick(actionNode, function () { + if (!context.pendingDiagramRender) return; + renderDiagramSvg(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); + context.pendingDiagramRender = null; + }); + } else { + dom.setNodeOnClick(actionNode, null); + } + } + dom.setNodeDisplay(errorBox, ''); + dom.setDiagramElementDisplay(diagram, 'none'); } -function buildSubgraphLines(rootGroup, addNodeLines, escapeMermaidText) { - const lines = []; - let groupIndex = 0; - const renderGroup = (group, isRoot, parentSubgraphFqnForNodes) => { - group.nodes.forEach(nodeId => addNodeLines(lines, nodeId, parentSubgraphFqnForNodes)); - const childKeys = Array.from(group.children.keys()).sort(); - if (isRoot && group.nodes.length === 0 && childKeys.length === 1) { - renderGroup(group.children.get(childKeys[0]), false, parentSubgraphFqnForNodes); - return; - } - childKeys.forEach(key => { - const child = group.children.get(key); - const childNodeCount = child.nodes.length + child.children.size; - if (childNodeCount <= 1) { - renderGroup(child, false, parentSubgraphFqnForNodes); - return; - } - const groupId = `G${groupIndex++}`; - lines.push(`subgraph ${groupId}["${escapeMermaidText(child.key)}"]`); - renderGroup(child, false, child.key); - lines.push('end'); - }); - }; - renderGroup(rootGroup, true, rootGroup.key); - return lines; +function hideDiagramErrorMessage(diagram) { + const errorBox = getOrCreateDiagramErrorBox(diagram); + const messageNode = dom.getDiagramErrorMessageNode(); + const actionNode = dom.getDiagramErrorActionNode(); + dom.setNodeTextContent(messageNode, ''); + dom.setNodeDisplay(actionNode, 'none'); + dom.setNodeOnClick(actionNode, null); + dom.setNodeDisplay(errorBox, 'none'); + dom.setDiagramElementDisplay(diagram, ''); } -function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { - const base = buildFilteredDiagramRelations( - packages, - relations, - causeRelationEvidence, - packageFilterFqn, - aggregationDepth, - transitiveReductionEnabled - ); - const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; - const {uniqueRelations, visibleSet} = applyRelatedFilterToDiagramRelations( - base.uniqueRelations, - base.visibleSet, - aggregatedRoot, - aggregationDepth, - relatedFilterMode - ); - return { - uniqueRelations, - visibleSet, - filteredCauseRelationEvidence: base.filteredCauseRelationEvidence, - }; +function renderDiagramSvg(text, maxEdges, context) { + const diagram = context.diagramElement; + if (!diagram || !window.mermaid) return; + hideDiagramErrorMessage(diagram); + dom.removeDiagramAttribute(diagram, 'data-processed'); + dom.setDiagramContent(diagram, text); + mermaid.initialize({startOnLoad: false, securityLevel: 'loose', maxEdges: maxEdges}); + mermaid.run({nodes: [diagram]}); } -function buildFilteredDiagramRelations(packages, relations, causeRelationEvidence, packageFilterFqn, aggregationDepth, transitiveReductionEnabled) { - const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; - const withinPackageFilter = fqn => - !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); - const visiblePackages = packageFilterFqn - ? packages.filter(item => withinPackageFilter(item.fqn)) - : packages; - const visibleSet = new Set(visiblePackages.map(item => getAggregatedFqn(item.fqn, aggregationDepth))); - const filteredRelations = packageFilterFqn - ? relations.filter(relation => withinPackageFilter(relation.from) && withinPackageFilter(relation.to)) - : relations; - const filteredCauseRelationEvidence = packageFilterFqn - ? causeRelationEvidence.filter(relation => { - const fromPackage = getPackageFqnFromTypeFqn(relation.from); - const toPackage = getPackageFqnFromTypeFqn(relation.to); - return withinPackageFilter(fromPackage) && withinPackageFilter(toPackage); - }) - : causeRelationEvidence; - const visibleRelations = filteredRelations - .map(relation => ({ - from: getAggregatedFqn(relation.from, aggregationDepth), - to: getAggregatedFqn(relation.to, aggregationDepth), - })) - .filter(relation => relation.from !== relation.to); - const uniqueRelationMap = new Map(); - visibleRelations.forEach(relation => { - uniqueRelationMap.set(`${relation.from}::${relation.to}`, relation); - }); - let uniqueRelations = Array.from(uniqueRelationMap.values()); - - if (transitiveReductionEnabled) { - uniqueRelations = transitiveReduction(uniqueRelations); +function renderMutualDependencyList(mutualPairs, causeRelationEvidence, context) { + const container = dom.getMutualDependencyList(); + if (!container) return; + const items = buildMutualDependencyItems(mutualPairs, causeRelationEvidence, context.aggregationDepth); + if (items.length === 0) { + container.style.display = 'none'; + container.innerHTML = ''; + return; } - return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; -} - -function applyRelatedFilterToDiagramRelations(uniqueRelations, visibleSet, aggregatedRoot, aggregationDepth, relatedFilterMode) { - const nextVisibleSet = new Set(visibleSet); - let nextRelations = uniqueRelations; - if (aggregatedRoot) { - const relatedSet = collectRelatedSet(aggregatedRoot, uniqueRelations, aggregationDepth, relatedFilterMode); - if (relatedFilterMode === 'direct') { - nextRelations = uniqueRelations.filter(relation => - relation.from === aggregatedRoot || relation.to === aggregatedRoot - ); - } else { - nextRelations = uniqueRelations.filter(relation => - relatedSet.has(relation.from) && relatedSet.has(relation.to) - ); + container.style.display = ''; + const details = document.createElement('details'); + const summary = document.createElement('summary'); + summary.textContent = '相互依存と原因'; + const list = document.createElement('ul'); + items.forEach(item => { + const itemNode = document.createElement('li'); + const pair = document.createElement('div'); + pair.className = 'pair'; + pair.textContent = item.pairLabel; + itemNode.appendChild(pair); + if (item.causes.length > 0) { + const detailBody = document.createElement('pre'); + detailBody.textContent = item.causes.join('\n'); + itemNode.appendChild(detailBody); } - nextVisibleSet.clear(); - relatedSet.forEach(value => nextVisibleSet.add(value)); - } - nextRelations.forEach(relation => { - nextVisibleSet.add(relation.from); - nextVisibleSet.add(relation.to); + list.appendChild(itemNode); }); - return {uniqueRelations: nextRelations, visibleSet: nextVisibleSet}; + container.innerHTML = ''; + details.appendChild(summary); + details.appendChild(list); + container.appendChild(details); } function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { @@ -960,18 +1000,10 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { } } -function applyRelatedFilter(fqn, context) { - context.relatedFilterFqn = fqn; - renderDiagramAndTable(context); - renderRelatedFilterTarget(context); -} - -if (typeof window !== 'undefined') { - window.filterPackageDiagram = function (nodeId) { - const fqn = packageContext.diagramNodeIdToFqn.get(nodeId); - if (!fqn) return; - applyRelatedFilter(fqn, packageContext); - }; +function renderDiagramAndTable(context) { + renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); + applyRelatedFilterToTable(context.relatedFilterFqn, context); + updateAggregationDepthOptions(getMaxPackageDepth(context), context); } function setupPackageFilterControls(context) { @@ -1066,36 +1098,6 @@ function renderAggregationDepthOptions(select, options, aggregationDepth, maxDep select.value = String(value); } -function applyDefaultPackageFilterIfPresent(context) { - const input = dom.getPackageFilterInput(); - if (!input || input.value.trim()) return false; - const {packages} = getPackageSummaryData(context); - const candidate = findDefaultPackageFilterCandidate(packages); - if (!candidate) return false; - input.value = candidate; - context.packageFilterFqn = candidate; - renderDiagramAndTable(context); - return true; -} - -function findDefaultPackageFilterCandidate(packages) { - const domainRoots = packages - .map(item => item.fqn) - .map(fqn => { - const parts = fqn.split('.'); - const domainIndex = parts.indexOf('domain'); - if (domainIndex === -1) return null; - return parts.slice(0, domainIndex + 1).join('.'); - }) - .filter(Boolean); - if (domainRoots.length === 0) return null; - return domainRoots.reduce((best, current) => { - const bestDepth = best.split('.').length; - const currentDepth = current.split('.').length; - return currentDepth < bestDepth ? current : best; - }); -} - function setupRelatedFilterControls(context) { const select = dom.getRelatedModeSelect(); const clearButton = dom.getClearRelatedFilterButton(); @@ -1117,16 +1119,6 @@ function setupRelatedFilterControls(context) { } } -function normalizePackageFilterValue(value) { - const trimmed = (value ?? '').trim(); - return trimmed ? trimmed : null; -} - -function normalizeAggregationDepthValue(value) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; -} - function setupDiagramDirectionControls(context) { const radios = dom.getDiagramDirectionRadios(); radios.forEach(radio => { @@ -1192,60 +1184,63 @@ if (typeof module !== 'undefined' && module.exports) { dom, // private - getVisibleDiagramElements, - buildFilteredDiagramRelations, - applyRelatedFilterToDiagramRelations, - getAggregatedFqn, - collectRelatedSet, getPackageSummaryData, + parsePackageSummaryData, getPackageDepth, getMaxPackageDepth, + getAggregatedFqn, getCommonPrefixDepth, + getPackageFqnFromTypeFqn, buildAggregationStats, - buildAggregationStatsForFilters, buildAggregationStatsForPackageFilter, buildAggregationStatsForRelated, + buildAggregationStatsForFilters, + normalizePackageFilterValue, + normalizeAggregationDepthValue, + findDefaultPackageFilterCandidate, buildPackageRowVisibility, buildRelatedRowVisibility, - getOrCreateDiagramErrorBox, - showDiagramErrorMessage, - hideDiagramErrorMessage, - renderDiagramSvg, - parsePackageSummaryData, - renderPackageTable, + collectRelatedSet, + buildFilteredDiagramRelations, + applyRelatedFilterToDiagramRelations, + getVisibleDiagramElements, buildPackageTableRows, buildPackageTableRowSpecs, - createPackageTableRow, buildPackageTableActionSpecs, + createPackageTableRow, + renderPackageTable, applyPackageFilterToTable, applyRelatedFilterToTable, renderRelatedFilterTarget, - renderDiagramAndTable, - renderMutualDependencyList, + applyRelatedFilter, + applyDefaultPackageFilterIfPresent, buildMutualDependencyItems, - renderPackageDiagram, + detectStronglyConnectedComponents, + transitiveReduction, + buildMutualPairs, + buildParentFqns, buildMermaidDiagramSource, buildDiagramNodeMaps, buildDiagramEdgeLines, buildDiagramNodeLines, - buildDiagramGroupTree, - buildSubgraphLines, buildDiagramNodeLabel, buildDiagramNodeTooltip, - applyRelatedFilter, + buildDiagramGroupTree, + buildSubgraphLines, + getOrCreateDiagramErrorBox, + showDiagramErrorMessage, + hideDiagramErrorMessage, + renderDiagramSvg, + renderMutualDependencyList, + renderPackageDiagram, + renderDiagramAndTable, setupPackageFilterControls, setupAggregationDepthControl, updateAggregationDepthOptions, buildAggregationDepthOptions, renderAggregationDepthOptions, - applyDefaultPackageFilterIfPresent, - findDefaultPackageFilterCandidate, - normalizePackageFilterValue, - normalizeAggregationDepthValue, setupRelatedFilterControls, setupDiagramDirectionControls, setupTransitiveReductionControl, - detectStronglyConnectedComponents, - transitiveReduction, }; } From c5d8bcf134dc99e1b01f9f0d71e9a2f05a8ae5f1 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:15:02 +0900 Subject: [PATCH 38/51] Rename core render and filter helpers --- .../resources/templates/assets/package.js | 32 +++++++++---------- jig-core/src/test/js/package.test.js | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 15cdf6eb7..243aa6bb3 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -498,11 +498,11 @@ function renderPackageTable(context) { input.value = fqn; } context.packageFilterFqn = fqn; - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); }; const applyRelatedFilterForRow = fqn => { - applyRelatedFilter(fqn, context); + setRelatedFilterAndRender(fqn, context); }; rowSpecs.forEach(spec => { @@ -548,9 +548,9 @@ function renderRelatedFilterTarget(context) { dom.setRelatedFilterTargetText(target, context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'); } -function applyRelatedFilter(fqn, context) { +function setRelatedFilterAndRender(fqn, context) { context.relatedFilterFqn = fqn; - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); } @@ -562,7 +562,7 @@ function applyDefaultPackageFilterIfPresent(context) { if (!candidate) return false; input.value = candidate; context.packageFilterFqn = candidate; - renderDiagramAndTable(context); + updateDiagramAndTable(context); return true; } @@ -1000,7 +1000,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { } } -function renderDiagramAndTable(context) { +function updateDiagramAndTable(context) { renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); applyRelatedFilterToTable(context.relatedFilterFqn, context); updateAggregationDepthOptions(getMaxPackageDepth(context), context); @@ -1014,13 +1014,13 @@ function setupPackageFilterControls(context) { const applyFilter = () => { context.packageFilterFqn = normalizePackageFilterValue(input.value); - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); }; const clearPackageFilter = () => { input.value = ''; context.packageFilterFqn = null; - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); }; @@ -1043,7 +1043,7 @@ function setupAggregationDepthControl(context) { select.value = String(context.aggregationDepth); select.addEventListener('change', () => { context.aggregationDepth = normalizeAggregationDepthValue(select.value); - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); updateAggregationDepthOptions(maxDepth, context); }); @@ -1106,14 +1106,14 @@ function setupRelatedFilterControls(context) { select.addEventListener('change', () => { context.relatedFilterMode = select.value; if (context.relatedFilterFqn) { - renderDiagramAndTable(context); + updateDiagramAndTable(context); } }); if (clearButton) { clearButton.addEventListener('click', () => { context.relatedFilterFqn = null; context.packageFilterFqn = normalizePackageFilterValue(dom.getPackageFilterInput()?.value); - renderDiagramAndTable(context); + updateDiagramAndTable(context); renderRelatedFilterTarget(context); }); } @@ -1128,7 +1128,7 @@ function setupDiagramDirectionControls(context) { radio.addEventListener('change', () => { if (!radio.checked) return; context.diagramDirection = radio.value; - renderDiagramAndTable(context); + updateDiagramAndTable(context); }); }); } @@ -1144,7 +1144,7 @@ function setupTransitiveReductionControl(context) { checkbox.checked = context.transitiveReductionEnabled; checkbox.addEventListener('change', () => { context.transitiveReductionEnabled = checkbox.checked; - renderDiagramAndTable(context); + updateDiagramAndTable(context); }); const label = document.createElement('label'); @@ -1170,7 +1170,7 @@ if (typeof document !== 'undefined') { setupTransitiveReductionControl(packageContext); const applied = applyDefaultPackageFilterIfPresent(packageContext); if (!applied) { - renderDiagramAndTable(packageContext); + updateDiagramAndTable(packageContext); } renderRelatedFilterTarget(packageContext); }); @@ -1212,7 +1212,7 @@ if (typeof module !== 'undefined' && module.exports) { applyPackageFilterToTable, applyRelatedFilterToTable, renderRelatedFilterTarget, - applyRelatedFilter, + setRelatedFilterAndRender, applyDefaultPackageFilterIfPresent, buildMutualDependencyItems, detectStronglyConnectedComponents, @@ -1233,7 +1233,7 @@ if (typeof module !== 'undefined' && module.exports) { renderDiagramSvg, renderMutualDependencyList, renderPackageDiagram, - renderDiagramAndTable, + updateDiagramAndTable, setupPackageFilterControls, setupAggregationDepthControl, updateAggregationDepthOptions, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index dc068a623..adee753e5 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -615,7 +615,7 @@ test.describe('package.js', () => { }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); const {input} = createPackageFilterControls(doc); - createDepthSelect(doc); // for renderDiagramAndTable + createDepthSelect(doc); // for updateDiagramAndTable const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); From 032f5c4e732a2ee3c9d6be37136d8983460afb4c Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:15:26 +0900 Subject: [PATCH 39/51] Rename diagram filter helpers --- .../resources/templates/assets/package.js | 18 +++++++-------- jig-core/src/test/js/package.test.js | 22 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 243aa6bb3..fcecc75e4 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -306,7 +306,7 @@ function collectRelatedSet(root, relations, aggregationDepth, relatedFilterMode) return relatedSet; } -function buildFilteredDiagramRelations(packages, relations, causeRelationEvidence, packageFilterFqn, aggregationDepth, transitiveReductionEnabled) { +function buildVisibleDiagramRelations(packages, relations, causeRelationEvidence, packageFilterFqn, aggregationDepth, transitiveReductionEnabled) { const packageFilterPrefix = packageFilterFqn ? `${packageFilterFqn}.` : null; const withinPackageFilter = fqn => !packageFilterFqn || fqn === packageFilterFqn || fqn.startsWith(packageFilterPrefix); @@ -343,7 +343,7 @@ function buildFilteredDiagramRelations(packages, relations, causeRelationEvidenc return {uniqueRelations, visibleSet, filteredCauseRelationEvidence}; } -function applyRelatedFilterToDiagramRelations(uniqueRelations, visibleSet, aggregatedRoot, aggregationDepth, relatedFilterMode) { +function filterRelatedDiagramRelations(uniqueRelations, visibleSet, aggregatedRoot, aggregationDepth, relatedFilterMode) { const nextVisibleSet = new Set(visibleSet); let nextRelations = uniqueRelations; if (aggregatedRoot) { @@ -367,8 +367,8 @@ function applyRelatedFilterToDiagramRelations(uniqueRelations, visibleSet, aggre return {uniqueRelations: nextRelations, visibleSet: nextVisibleSet}; } -function getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { - const base = buildFilteredDiagramRelations( +function buildVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, aggregationDepth, relatedFilterMode, transitiveReductionEnabled) { + const base = buildVisibleDiagramRelations( packages, relations, causeRelationEvidence, @@ -377,7 +377,7 @@ function getVisibleDiagramElements(packages, relations, causeRelationEvidence, p transitiveReductionEnabled ); const aggregatedRoot = relatedFilterFqn ? getAggregatedFqn(relatedFilterFqn, aggregationDepth) : null; - const {uniqueRelations, visibleSet} = applyRelatedFilterToDiagramRelations( + const {uniqueRelations, visibleSet} = filterRelatedDiagramRelations( base.uniqueRelations, base.visibleSet, aggregatedRoot, @@ -958,7 +958,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { uniqueRelations, visibleSet, filteredCauseRelationEvidence - } = getVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, context.aggregationDepth, context.relatedFilterMode, context.transitiveReductionEnabled); + } = buildVisibleDiagramElements(packages, relations, causeRelationEvidence, packageFilterFqn, relatedFilterFqn, context.aggregationDepth, context.relatedFilterMode, context.transitiveReductionEnabled); const nameByFqn = new Map(packages.map(item => [item.fqn, item.name || item.fqn])); const {source, nodeIdToFqn, mutualPairs} = buildMermaidDiagramSource( @@ -1201,9 +1201,9 @@ if (typeof module !== 'undefined' && module.exports) { buildPackageRowVisibility, buildRelatedRowVisibility, collectRelatedSet, - buildFilteredDiagramRelations, - applyRelatedFilterToDiagramRelations, - getVisibleDiagramElements, + buildVisibleDiagramRelations, + filterRelatedDiagramRelations, + buildVisibleDiagramElements, buildPackageTableRows, buildPackageTableRowSpecs, buildPackageTableActionSpecs, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index adee753e5..4f5dfe79d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -522,15 +522,15 @@ test.describe('package.js', () => { ); }); - test('buildFilteredDiagramRelations: パッケージフィルタを適用する', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], 'app', 0, false); + test('buildVisibleDiagramRelations: パッケージフィルタを適用する', () => { + const base = pkg.buildVisibleDiagramRelations(packages, relations, [], 'app', 0, false); assert.deepEqual(Array.from(base.visibleSet).sort(), ['app.a', 'app.b', 'app.c']); assert.equal(base.uniqueRelations.length, 2); }); - test('applyRelatedFilterToDiagramRelations: relatedSetで絞り込む', () => { - const base = pkg.buildFilteredDiagramRelations(packages, relations, [], null, 0, false); - const filtered = pkg.applyRelatedFilterToDiagramRelations( + test('filterRelatedDiagramRelations: relatedSetで絞り込む', () => { + const base = pkg.buildVisibleDiagramRelations(packages, relations, [], null, 0, false); + const filtered = pkg.filterRelatedDiagramRelations( base.uniqueRelations, base.visibleSet, 'app.b', @@ -541,18 +541,18 @@ test.describe('package.js', () => { assert.equal(filtered.uniqueRelations.length, 2); }); - test('getVisibleDiagramElements: packageFilterは配下のみを表示する', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); + test('buildVisibleDiagramElements: packageFilterは配下のみを表示する', () => { + const {visibleSet} = pkg.buildVisibleDiagramElements(packages, relations, [], 'app', null, 0, 'direct', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('getVisibleDiagramElements: relatedFilter(direct)は隣接のみを表示する', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); + test('buildVisibleDiagramElements: relatedFilter(direct)は隣接のみを表示する', () => { + const {visibleSet} = pkg.buildVisibleDiagramElements(packages, relations, [], null, 'app.b', 0, 'direct', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c']); }); - test('getVisibleDiagramElements: relatedFilter(all)は到達可能なものを表示する', () => { - const {visibleSet} = pkg.getVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); + test('buildVisibleDiagramElements: relatedFilter(all)は到達可能なものを表示する', () => { + const {visibleSet} = pkg.buildVisibleDiagramElements(packages, relations, [], null, 'app.a', 0, 'all', false); assert.deepEqual(Array.from(visibleSet).sort(), ['app.a', 'app.b', 'app.c', 'lib.d']); }); }); From 70d3a31eb6f58c8b56f7a536b5865bfd83f265b1 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:15:51 +0900 Subject: [PATCH 40/51] Rename table row builders --- .../src/main/resources/templates/assets/package.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index fcecc75e4..57eb8eac9 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -391,7 +391,7 @@ function buildVisibleDiagramElements(packages, relations, causeRelationEvidence, }; } -function buildPackageTableRows(packages, relations) { +function buildPackageTableRowData(packages, relations) { const incomingCounts = new Map(); const outgoingCounts = new Map(); relations.forEach(relation => { @@ -428,7 +428,7 @@ function buildPackageTableActionSpecs() { }; } -function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { +function buildPackageTableRowElement(spec, applyFilter, applyRelatedFilterForRow) { const tr = document.createElement('tr'); const actionSpecs = buildPackageTableActionSpecs(); @@ -487,7 +487,7 @@ function createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow) { function renderPackageTable(context) { const {packages, relations} = getPackageSummaryData(context); - const rows = buildPackageTableRows(packages, relations); + const rows = buildPackageTableRowData(packages, relations); const rowSpecs = buildPackageTableRowSpecs(rows); const tbody = dom.getPackageTableBody(); @@ -506,7 +506,7 @@ function renderPackageTable(context) { }; rowSpecs.forEach(spec => { - const tr = createPackageTableRow(spec, applyFilter, applyRelatedFilterForRow); + const tr = buildPackageTableRowElement(spec, applyFilter, applyRelatedFilterForRow); tbody.appendChild(tr); }); } @@ -1204,10 +1204,10 @@ if (typeof module !== 'undefined' && module.exports) { buildVisibleDiagramRelations, filterRelatedDiagramRelations, buildVisibleDiagramElements, - buildPackageTableRows, + buildPackageTableRowData, buildPackageTableRowSpecs, buildPackageTableActionSpecs, - createPackageTableRow, + buildPackageTableRowElement, renderPackageTable, applyPackageFilterToTable, applyRelatedFilterToTable, From 667a9fa4abc1215da144e92097ba1346113c09e7 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:16:17 +0900 Subject: [PATCH 41/51] Rename aggregation option helpers --- .../main/resources/templates/assets/package.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 57eb8eac9..fad073807 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -1003,7 +1003,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { function updateDiagramAndTable(context) { renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); applyRelatedFilterToTable(context.relatedFilterFqn, context); - updateAggregationDepthOptions(getMaxPackageDepth(context), context); + updateAggregationDepthSelectOptions(getMaxPackageDepth(context), context); } function setupPackageFilterControls(context) { @@ -1039,17 +1039,17 @@ function setupAggregationDepthControl(context) { if (!select) return; const {packages} = getPackageSummaryData(context); const maxDepth = packages.reduce((max, item) => Math.max(max, getPackageDepth(item.fqn)), 0); - updateAggregationDepthOptions(maxDepth, context); + updateAggregationDepthSelectOptions(maxDepth, context); select.value = String(context.aggregationDepth); select.addEventListener('change', () => { context.aggregationDepth = normalizeAggregationDepthValue(select.value); updateDiagramAndTable(context); renderRelatedFilterTarget(context); - updateAggregationDepthOptions(maxDepth, context); + updateAggregationDepthSelectOptions(maxDepth, context); }); } -function updateAggregationDepthOptions(maxDepth, context) { +function updateAggregationDepthSelectOptions(maxDepth, context) { const select = dom.getDepthSelect(); if (!select) return; const {packages, relations} = getPackageSummaryData(context); @@ -1063,7 +1063,7 @@ function updateAggregationDepthOptions(maxDepth, context) { context.relatedFilterMode ); const options = buildAggregationDepthOptions(aggregationStats, maxDepth); - renderAggregationDepthOptions(select, options, context.aggregationDepth, maxDepth); + renderAggregationDepthSelectOptions(select, options, context.aggregationDepth, maxDepth); } function buildAggregationDepthOptions(aggregationStats, maxDepth) { @@ -1086,7 +1086,7 @@ function buildAggregationDepthOptions(aggregationStats, maxDepth) { return options; } -function renderAggregationDepthOptions(select, options, aggregationDepth, maxDepth) { +function renderAggregationDepthSelectOptions(select, options, aggregationDepth, maxDepth) { select.innerHTML = ''; options.forEach(option => { const node = document.createElement('option'); @@ -1236,9 +1236,9 @@ if (typeof module !== 'undefined' && module.exports) { updateDiagramAndTable, setupPackageFilterControls, setupAggregationDepthControl, - updateAggregationDepthOptions, + updateAggregationDepthSelectOptions, buildAggregationDepthOptions, - renderAggregationDepthOptions, + renderAggregationDepthSelectOptions, setupRelatedFilterControls, setupDiagramDirectionControls, setupTransitiveReductionControl, From b80de2ff29c59b538b901c5662a583b9f0a66ec9 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:16:48 +0900 Subject: [PATCH 42/51] Rename filter UI helpers --- .../resources/templates/assets/package.js | 36 +++++++++---------- jig-core/src/test/js/package.test.js | 10 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index fad073807..fcbc783c2 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -499,7 +499,7 @@ function renderPackageTable(context) { } context.packageFilterFqn = fqn; updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); }; const applyRelatedFilterForRow = fqn => { setRelatedFilterAndRender(fqn, context); @@ -543,7 +543,7 @@ function applyRelatedFilterToTable(fqn, context) { }); } -function renderRelatedFilterTarget(context) { +function renderRelatedFilterLabel(context) { const target = dom.getRelatedFilterTarget(); dom.setRelatedFilterTargetText(target, context.relatedFilterFqn ? context.relatedFilterFqn : '未選択'); } @@ -551,7 +551,7 @@ function renderRelatedFilterTarget(context) { function setRelatedFilterAndRender(fqn, context) { context.relatedFilterFqn = fqn; updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); } function applyDefaultPackageFilterIfPresent(context) { @@ -1006,7 +1006,7 @@ function updateDiagramAndTable(context) { updateAggregationDepthSelectOptions(getMaxPackageDepth(context), context); } -function setupPackageFilterControls(context) { +function setupPackageFilterControl(context) { const input = dom.getPackageFilterInput(); const applyButton = dom.getApplyPackageFilterButton(); const clearPackageButton = dom.getClearPackageFilterButton(); @@ -1015,13 +1015,13 @@ function setupPackageFilterControls(context) { const applyFilter = () => { context.packageFilterFqn = normalizePackageFilterValue(input.value); updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); }; const clearPackageFilter = () => { input.value = ''; context.packageFilterFqn = null; updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); }; applyButton.addEventListener('click', applyFilter); @@ -1044,7 +1044,7 @@ function setupAggregationDepthControl(context) { select.addEventListener('change', () => { context.aggregationDepth = normalizeAggregationDepthValue(select.value); updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); updateAggregationDepthSelectOptions(maxDepth, context); }); } @@ -1098,7 +1098,7 @@ function renderAggregationDepthSelectOptions(select, options, aggregationDepth, select.value = String(value); } -function setupRelatedFilterControls(context) { +function setupRelatedFilterControl(context) { const select = dom.getRelatedModeSelect(); const clearButton = dom.getClearRelatedFilterButton(); if (!select) return; @@ -1114,12 +1114,12 @@ function setupRelatedFilterControls(context) { context.relatedFilterFqn = null; context.packageFilterFqn = normalizePackageFilterValue(dom.getPackageFilterInput()?.value); updateDiagramAndTable(context); - renderRelatedFilterTarget(context); + renderRelatedFilterLabel(context); }); } } -function setupDiagramDirectionControls(context) { +function setupDiagramDirectionControl(context) { const radios = dom.getDiagramDirectionRadios(); radios.forEach(radio => { if (radio.value === context.diagramDirection) { @@ -1163,16 +1163,16 @@ if (typeof document !== 'undefined') { if (!body || !body.classList.contains("package-list")) return; setupSortableTables(); renderPackageTable(packageContext); - setupPackageFilterControls(packageContext); + setupPackageFilterControl(packageContext); setupAggregationDepthControl(packageContext); - setupRelatedFilterControls(packageContext); - setupDiagramDirectionControls(packageContext); + setupRelatedFilterControl(packageContext); + setupDiagramDirectionControl(packageContext); setupTransitiveReductionControl(packageContext); const applied = applyDefaultPackageFilterIfPresent(packageContext); if (!applied) { updateDiagramAndTable(packageContext); } - renderRelatedFilterTarget(packageContext); + renderRelatedFilterLabel(packageContext); }); } @@ -1211,7 +1211,7 @@ if (typeof module !== 'undefined' && module.exports) { renderPackageTable, applyPackageFilterToTable, applyRelatedFilterToTable, - renderRelatedFilterTarget, + renderRelatedFilterLabel, setRelatedFilterAndRender, applyDefaultPackageFilterIfPresent, buildMutualDependencyItems, @@ -1234,13 +1234,13 @@ if (typeof module !== 'undefined' && module.exports) { renderMutualDependencyList, renderPackageDiagram, updateDiagramAndTable, - setupPackageFilterControls, + setupPackageFilterControl, setupAggregationDepthControl, updateAggregationDepthSelectOptions, buildAggregationDepthOptions, renderAggregationDepthSelectOptions, - setupRelatedFilterControls, - setupDiagramDirectionControls, + setupRelatedFilterControl, + setupDiagramDirectionControl, setupTransitiveReductionControl, }; } diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 4f5dfe79d..f648dff0f 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -582,7 +582,7 @@ test.describe('package.js', () => { assert.equal(rows[2].classList.contains('hidden'), true); }); - test('renderRelatedFilterTarget: 対象表示を更新する', () => { + test('renderRelatedFilterLabel: 対象表示を更新する', () => { const mockTarget = { textContent: '' }; const getRelatedFilterTargetMock = test.mock.fn(() => mockTarget); const setRelatedFilterTargetTextMock = test.mock.fn((element, text) => { element.textContent = text; }); @@ -591,13 +591,13 @@ test.describe('package.js', () => { test.mock.method(pkg.dom, 'setRelatedFilterTargetText', setRelatedFilterTargetTextMock); testContext.relatedFilterFqn = null; - pkg.renderRelatedFilterTarget(testContext); + pkg.renderRelatedFilterLabel(testContext); assert.equal(mockTarget.textContent, '未選択'); assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 1); assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[0].arguments, [mockTarget, '未選択']); testContext.relatedFilterFqn = 'app.domain'; - pkg.renderRelatedFilterTarget(testContext); + pkg.renderRelatedFilterLabel(testContext); assert.equal(mockTarget.textContent, 'app.domain'); assert.equal(setRelatedFilterTargetTextMock.mock.calls.length, 2); assert.deepEqual(setRelatedFilterTargetTextMock.mock.calls[1].arguments, [mockTarget, 'app.domain']); @@ -624,7 +624,7 @@ test.describe('package.js', () => { assert.equal(input.value, 'app.domain'); }); - test('setupPackageFilterControls: 適用/解除を扱う', () => { + test('setupPackageFilterControl: 適用/解除を扱う', () => { const doc = setupDocument(); setupDiagramEnvironment(doc, testContext); setPackageData({ @@ -636,7 +636,7 @@ test.describe('package.js', () => { const {input, applyButton, clearButton} = createPackageFilterControls(doc); - pkg.setupPackageFilterControls(testContext); + pkg.setupPackageFilterControl(testContext); input.value = 'app.domain'; applyButton.eventListeners.get('click')(); From bd60263bce89e7ac8525bc186f3a3d2f8aed0149 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:17:14 +0900 Subject: [PATCH 43/51] Rename diagram render helper --- jig-core/src/main/resources/templates/assets/package.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index fcbc783c2..736085643 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -881,7 +881,7 @@ function showDiagramErrorMessage(message, withAction, err, hash, context) { if (withAction) { dom.setNodeOnClick(actionNode, function () { if (!context.pendingDiagramRender) return; - renderDiagramSvg(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); + renderDiagramWithMermaid(context.pendingDiagramRender.text, context.pendingDiagramRender.maxEdges, context); context.pendingDiagramRender = null; }); } else { @@ -903,7 +903,7 @@ function hideDiagramErrorMessage(diagram) { dom.setDiagramElementDisplay(diagram, ''); } -function renderDiagramSvg(text, maxEdges, context) { +function renderDiagramWithMermaid(text, maxEdges, context) { const diagram = context.diagramElement; if (!diagram || !window.mermaid) return; hideDiagramErrorMessage(diagram); @@ -996,7 +996,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { showDiagramErrorMessage(`Mermaid parse error: ${message}${location}`, isEdgeLimit, err, hash, context); }; } - renderDiagramSvg(context.lastDiagramSource, context.DEFAULT_MAX_EDGES, context); + renderDiagramWithMermaid(context.lastDiagramSource, context.DEFAULT_MAX_EDGES, context); } } @@ -1230,7 +1230,7 @@ if (typeof module !== 'undefined' && module.exports) { getOrCreateDiagramErrorBox, showDiagramErrorMessage, hideDiagramErrorMessage, - renderDiagramSvg, + renderDiagramWithMermaid, renderMutualDependencyList, renderPackageDiagram, updateDiagramAndTable, From 8e4aeed9fdd1a162d247b1a17145d4993a9a0ae8 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:17:44 +0900 Subject: [PATCH 44/51] Rename mutual dependency pair helper --- jig-core/src/main/resources/templates/assets/package.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 736085643..47976f7d6 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -685,7 +685,7 @@ function transitiveReduction(relations) { return relations.filter(edge => !toRemove.has(`${edge.from}::${edge.to}`)); } -function buildMutualPairs(relations) { +function buildMutualDependencyPairs(relations) { const relationKey = (from, to) => `${from}::${to}`; const canonicalPairKey = (from, to) => (from < to ? `${from}::${to}` : `${to}::${from}`); const relationSet = new Set(relations.map(relation => relationKey(relation.from, relation.to))); @@ -752,7 +752,7 @@ function buildDiagramNodeMaps(visibleSet, nameByFqn) { } function buildDiagramEdgeLines(uniqueRelations, ensureNodeId) { - const mutualPairs = buildMutualPairs(uniqueRelations); + const mutualPairs = buildMutualDependencyPairs(uniqueRelations); const linkStyles = []; let linkIndex = 0; const edgeLines = []; @@ -1217,7 +1217,7 @@ if (typeof module !== 'undefined' && module.exports) { buildMutualDependencyItems, detectStronglyConnectedComponents, transitiveReduction, - buildMutualPairs, + buildMutualDependencyPairs, buildParentFqns, buildMermaidDiagramSource, buildDiagramNodeMaps, From 88d38b7decbdee9850ece94feaf6eb1003bdf214 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:18:18 +0900 Subject: [PATCH 45/51] Rename related table filter --- jig-core/src/main/resources/templates/assets/package.js | 6 +++--- jig-core/src/test/js/package.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 47976f7d6..aecb1dcc0 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -523,7 +523,7 @@ function applyPackageFilterToTable(packageFilterFqn) { }); } -function applyRelatedFilterToTable(fqn, context) { +function filterRelatedTableRows(fqn, context) { const rows = dom.getPackageTableRows(); const {relations} = getPackageSummaryData(context); const rowFqns = Array.from(rows, row => { @@ -1002,7 +1002,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { function updateDiagramAndTable(context) { renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); - applyRelatedFilterToTable(context.relatedFilterFqn, context); + filterRelatedTableRows(context.relatedFilterFqn, context); updateAggregationDepthSelectOptions(getMaxPackageDepth(context), context); } @@ -1210,7 +1210,7 @@ if (typeof module !== 'undefined' && module.exports) { buildPackageTableRowElement, renderPackageTable, applyPackageFilterToTable, - applyRelatedFilterToTable, + filterRelatedTableRows, renderRelatedFilterLabel, setRelatedFilterAndRender, applyDefaultPackageFilterIfPresent, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index f648dff0f..02644216d 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -558,7 +558,7 @@ test.describe('package.js', () => { }); test.describe('UI', () => { - test('applyRelatedFilterToTable: 関係する行のみ表示する', () => { + test('filterRelatedTableRows: 関係する行のみ表示する', () => { const doc = setupDocument(); setPackageData({ packages: [ @@ -575,7 +575,7 @@ test.describe('package.js', () => { testContext.relatedFilterMode = 'direct'; testContext.packageFilterFqn = null; - pkg.applyRelatedFilterToTable('app.a', testContext); + pkg.filterRelatedTableRows('app.a', testContext); assert.equal(rows[0].classList.contains('hidden'), false); assert.equal(rows[1].classList.contains('hidden'), false); From 696aa7291e35e797f5657ae3048ec4fb724fbede Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:21:00 +0900 Subject: [PATCH 46/51] Rename diagram/table refresh helper --- .../resources/templates/assets/package.js | 26 +++++++++---------- jig-core/src/test/js/package.test.js | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index aecb1dcc0..2e0f909d4 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -498,7 +498,7 @@ function renderPackageTable(context) { input.value = fqn; } context.packageFilterFqn = fqn; - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); }; const applyRelatedFilterForRow = fqn => { @@ -550,7 +550,7 @@ function renderRelatedFilterLabel(context) { function setRelatedFilterAndRender(fqn, context) { context.relatedFilterFqn = fqn; - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); } @@ -562,7 +562,7 @@ function applyDefaultPackageFilterIfPresent(context) { if (!candidate) return false; input.value = candidate; context.packageFilterFqn = candidate; - updateDiagramAndTable(context); + renderDiagramAndTable(context); return true; } @@ -1000,7 +1000,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { } } -function updateDiagramAndTable(context) { +function renderDiagramAndTable(context) { renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); filterRelatedTableRows(context.relatedFilterFqn, context); updateAggregationDepthSelectOptions(getMaxPackageDepth(context), context); @@ -1014,13 +1014,13 @@ function setupPackageFilterControl(context) { const applyFilter = () => { context.packageFilterFqn = normalizePackageFilterValue(input.value); - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); }; const clearPackageFilter = () => { input.value = ''; context.packageFilterFqn = null; - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); }; @@ -1043,7 +1043,7 @@ function setupAggregationDepthControl(context) { select.value = String(context.aggregationDepth); select.addEventListener('change', () => { context.aggregationDepth = normalizeAggregationDepthValue(select.value); - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); updateAggregationDepthSelectOptions(maxDepth, context); }); @@ -1106,14 +1106,14 @@ function setupRelatedFilterControl(context) { select.addEventListener('change', () => { context.relatedFilterMode = select.value; if (context.relatedFilterFqn) { - updateDiagramAndTable(context); + renderDiagramAndTable(context); } }); if (clearButton) { clearButton.addEventListener('click', () => { context.relatedFilterFqn = null; context.packageFilterFqn = normalizePackageFilterValue(dom.getPackageFilterInput()?.value); - updateDiagramAndTable(context); + renderDiagramAndTable(context); renderRelatedFilterLabel(context); }); } @@ -1128,7 +1128,7 @@ function setupDiagramDirectionControl(context) { radio.addEventListener('change', () => { if (!radio.checked) return; context.diagramDirection = radio.value; - updateDiagramAndTable(context); + renderDiagramAndTable(context); }); }); } @@ -1144,7 +1144,7 @@ function setupTransitiveReductionControl(context) { checkbox.checked = context.transitiveReductionEnabled; checkbox.addEventListener('change', () => { context.transitiveReductionEnabled = checkbox.checked; - updateDiagramAndTable(context); + renderDiagramAndTable(context); }); const label = document.createElement('label'); @@ -1170,7 +1170,7 @@ if (typeof document !== 'undefined') { setupTransitiveReductionControl(packageContext); const applied = applyDefaultPackageFilterIfPresent(packageContext); if (!applied) { - updateDiagramAndTable(packageContext); + renderDiagramAndTable(packageContext); } renderRelatedFilterLabel(packageContext); }); @@ -1233,7 +1233,7 @@ if (typeof module !== 'undefined' && module.exports) { renderDiagramWithMermaid, renderMutualDependencyList, renderPackageDiagram, - updateDiagramAndTable, + renderDiagramAndTable, setupPackageFilterControl, setupAggregationDepthControl, updateAggregationDepthSelectOptions, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 02644216d..ced48c2fa 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -615,7 +615,7 @@ test.describe('package.js', () => { }, testContext); doc.selectorsAll.set('#package-table tbody tr', []); const {input} = createPackageFilterControls(doc); - createDepthSelect(doc); // for updateDiagramAndTable + createDepthSelect(doc); // for renderDiagramAndTable const applied = pkg.applyDefaultPackageFilterIfPresent(testContext); From be9e468e5b04ef585aada5c64461b653824811ff Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:21:34 +0900 Subject: [PATCH 47/51] Rename package filter table helper --- jig-core/src/main/resources/templates/assets/package.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 2e0f909d4..270d967f4 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -511,7 +511,7 @@ function renderPackageTable(context) { }); } -function applyPackageFilterToTable(packageFilterFqn) { +function filterPackageTableRows(packageFilterFqn) { const rows = dom.getPackageTableRows(); const rowFqns = Array.from(rows, row => { const fqnCell = row.querySelector('td.fqn'); @@ -1209,7 +1209,7 @@ if (typeof module !== 'undefined' && module.exports) { buildPackageTableActionSpecs, buildPackageTableRowElement, renderPackageTable, - applyPackageFilterToTable, + filterPackageTableRows, filterRelatedTableRows, renderRelatedFilterLabel, setRelatedFilterAndRender, From 684ea3c4468f2f9337206c1de1ae013c4faa4bfd Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 20:26:12 +0900 Subject: [PATCH 48/51] Rename aggregation select render helpers --- .../main/resources/templates/assets/package.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 270d967f4..6a7312ff9 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -1003,7 +1003,7 @@ function renderPackageDiagram(context, packageFilterFqn, relatedFilterFqn) { function renderDiagramAndTable(context) { renderPackageDiagram(context, context.packageFilterFqn, context.relatedFilterFqn); filterRelatedTableRows(context.relatedFilterFqn, context); - updateAggregationDepthSelectOptions(getMaxPackageDepth(context), context); + renderAggregationDepthSelectOptions(getMaxPackageDepth(context), context); } function setupPackageFilterControl(context) { @@ -1039,17 +1039,17 @@ function setupAggregationDepthControl(context) { if (!select) return; const {packages} = getPackageSummaryData(context); const maxDepth = packages.reduce((max, item) => Math.max(max, getPackageDepth(item.fqn)), 0); - updateAggregationDepthSelectOptions(maxDepth, context); + renderAggregationDepthSelectOptions(maxDepth, context); select.value = String(context.aggregationDepth); select.addEventListener('change', () => { context.aggregationDepth = normalizeAggregationDepthValue(select.value); renderDiagramAndTable(context); renderRelatedFilterLabel(context); - updateAggregationDepthSelectOptions(maxDepth, context); + renderAggregationDepthSelectOptions(maxDepth, context); }); } -function updateAggregationDepthSelectOptions(maxDepth, context) { +function renderAggregationDepthSelectOptions(maxDepth, context) { const select = dom.getDepthSelect(); if (!select) return; const {packages, relations} = getPackageSummaryData(context); @@ -1063,7 +1063,7 @@ function updateAggregationDepthSelectOptions(maxDepth, context) { context.relatedFilterMode ); const options = buildAggregationDepthOptions(aggregationStats, maxDepth); - renderAggregationDepthSelectOptions(select, options, context.aggregationDepth, maxDepth); + renderAggregationDepthOptionsIntoSelect(select, options, context.aggregationDepth, maxDepth); } function buildAggregationDepthOptions(aggregationStats, maxDepth) { @@ -1086,7 +1086,7 @@ function buildAggregationDepthOptions(aggregationStats, maxDepth) { return options; } -function renderAggregationDepthSelectOptions(select, options, aggregationDepth, maxDepth) { +function renderAggregationDepthOptionsIntoSelect(select, options, aggregationDepth, maxDepth) { select.innerHTML = ''; options.forEach(option => { const node = document.createElement('option'); @@ -1236,9 +1236,9 @@ if (typeof module !== 'undefined' && module.exports) { renderDiagramAndTable, setupPackageFilterControl, setupAggregationDepthControl, - updateAggregationDepthSelectOptions, - buildAggregationDepthOptions, renderAggregationDepthSelectOptions, + buildAggregationDepthOptions, + renderAggregationDepthOptionsIntoSelect, setupRelatedFilterControl, setupDiagramDirectionControl, setupTransitiveReductionControl, From a8afd1ddcf21dc2e9e7dfef37e5f88d73a90e720 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 21:45:24 +0900 Subject: [PATCH 49/51] Restore diagram node click handler --- jig-core/src/main/resources/templates/assets/package.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index 6a7312ff9..af20fffbb 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -554,6 +554,14 @@ function setRelatedFilterAndRender(fqn, context) { renderRelatedFilterLabel(context); } +if (typeof window !== 'undefined') { + window.filterPackageDiagram = function (nodeId) { + const fqn = packageContext.diagramNodeIdToFqn.get(nodeId); + if (!fqn) return; + setRelatedFilterAndRender(fqn, packageContext); + }; +} + function applyDefaultPackageFilterIfPresent(context) { const input = dom.getPackageFilterInput(); if (!input || input.value.trim()) return false; From 15285c8a37844899372f4adc491770de874ee77d Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 21:51:50 +0900 Subject: [PATCH 50/51] Add diagram click handler registration --- .../main/resources/templates/assets/package.js | 9 ++++++--- jig-core/src/test/js/package.test.js | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index af20fffbb..ff1e571c6 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -554,11 +554,12 @@ function setRelatedFilterAndRender(fqn, context) { renderRelatedFilterLabel(context); } -if (typeof window !== 'undefined') { +function registerDiagramClickHandler(context, applyRelatedFilter = setRelatedFilterAndRender) { + if (typeof window === 'undefined') return; window.filterPackageDiagram = function (nodeId) { - const fqn = packageContext.diagramNodeIdToFqn.get(nodeId); + const fqn = context.diagramNodeIdToFqn.get(nodeId); if (!fqn) return; - setRelatedFilterAndRender(fqn, packageContext); + applyRelatedFilter(fqn, context); }; } @@ -1176,6 +1177,7 @@ if (typeof document !== 'undefined') { setupRelatedFilterControl(packageContext); setupDiagramDirectionControl(packageContext); setupTransitiveReductionControl(packageContext); + registerDiagramClickHandler(packageContext); const applied = applyDefaultPackageFilterIfPresent(packageContext); if (!applied) { renderDiagramAndTable(packageContext); @@ -1242,6 +1244,7 @@ if (typeof module !== 'undefined' && module.exports) { renderMutualDependencyList, renderPackageDiagram, renderDiagramAndTable, + registerDiagramClickHandler, setupPackageFilterControl, setupAggregationDepthControl, renderAggregationDepthSelectOptions, diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index ced48c2fa..6027696d3 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -931,6 +931,21 @@ test.describe('package.js', () => { assert.equal(actionNodeMock.style.display, '', 'actionNode should be displayed'); }); + test('registerDiagramClickHandler: クリックで関連フィルタへ切り替える', () => { + global.window = {}; + testContext.diagramNodeIdToFqn = new Map([['P1', 'app.example']]); + let called = null; + const applyRelatedFilter = (fqn, context) => { + called = {fqn, context}; + }; + + pkg.registerDiagramClickHandler(testContext, applyRelatedFilter); + + global.window.filterPackageDiagram('P1'); + + assert.deepEqual(called, {fqn: 'app.example', context: testContext}); + }); + test('setupTransitiveReductionControl: UIをセットアップする', () => { const doc = setupDocument(); const container = doc.createElement('div'); From b1725497a4cd8119ce41dfe7fc51a9e0428e4be1 Mon Sep 17 00:00:00 2001 From: irof Date: Fri, 6 Feb 2026 21:59:18 +0900 Subject: [PATCH 51/51] Bind diagram click handler name --- .../main/resources/templates/assets/package.js | 7 +++++-- jig-core/src/test/js/package.test.js | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/jig-core/src/main/resources/templates/assets/package.js b/jig-core/src/main/resources/templates/assets/package.js index ff1e571c6..819b19993 100644 --- a/jig-core/src/main/resources/templates/assets/package.js +++ b/jig-core/src/main/resources/templates/assets/package.js @@ -14,6 +14,8 @@ const packageContext = { transitiveReductionEnabled: true, }; +const DIAGRAM_CLICK_HANDLER_NAME = 'filterPackageDiagram'; + const dom = { getRelatedFilterTarget: () => document.getElementById('related-filter-target'), setRelatedFilterTargetText: (element, text) => { if (element) element.textContent = text; }, @@ -556,7 +558,7 @@ function setRelatedFilterAndRender(fqn, context) { function registerDiagramClickHandler(context, applyRelatedFilter = setRelatedFilterAndRender) { if (typeof window === 'undefined') return; - window.filterPackageDiagram = function (nodeId) { + window[DIAGRAM_CLICK_HANDLER_NAME] = function (nodeId) { const fqn = context.diagramNodeIdToFqn.get(nodeId); if (!fqn) return; applyRelatedFilter(fqn, context); @@ -795,7 +797,7 @@ function buildDiagramNodeLines(visibleSet, nodeIdByFqn, nodeIdToFqn, nodeLabelBy const displayLabel = buildDiagramNodeLabel(nodeLabelById.get(nodeId), fqn, parentSubgraphFqn); lines.push(`${nodeId}["${escapeMermaidText(displayLabel)}"]`); const tooltip = escapeMermaidText(buildDiagramNodeTooltip(fqn)); - lines.push(`click ${nodeId} filterPackageDiagram "${tooltip}"`); + lines.push(`click ${nodeId} ${DIAGRAM_CLICK_HANDLER_NAME} "${tooltip}"`); if (fqn && parentFqns.has(fqn)) { lines.push(`class ${nodeId} parentPackage`); } @@ -1191,6 +1193,7 @@ if (typeof module !== 'undefined' && module.exports) { module.exports = { // public packageContext, + DIAGRAM_CLICK_HANDLER_NAME, dom, // private diff --git a/jig-core/src/test/js/package.test.js b/jig-core/src/test/js/package.test.js index 6027696d3..e69dc326a 100644 --- a/jig-core/src/test/js/package.test.js +++ b/jig-core/src/test/js/package.test.js @@ -842,6 +842,21 @@ test.describe('package.js', () => { {value: '1', text: '深さ1(P1 / R1)'}, ]); }); + + test('buildDiagramNodeLines: クリックハンドラ名を埋め込む', () => { + const visibleSet = new Set(['app.a']); + const {nodeIdByFqn, nodeIdToFqn, nodeLabelById} = pkg.buildDiagramNodeMaps(visibleSet, new Map([['app.a', 'A']])); + const {nodeLines} = pkg.buildDiagramNodeLines( + visibleSet, + nodeIdByFqn, + nodeIdToFqn, + nodeLabelById, + text => text + ); + const clickLine = nodeLines.find(line => line.startsWith('click ')); + assert.ok(clickLine); + assert.equal(clickLine.includes(pkg.DIAGRAM_CLICK_HANDLER_NAME), true); + }); }); test.describe('UI', () => { @@ -941,7 +956,7 @@ test.describe('package.js', () => { pkg.registerDiagramClickHandler(testContext, applyRelatedFilter); - global.window.filterPackageDiagram('P1'); + global.window[pkg.DIAGRAM_CLICK_HANDLER_NAME]('P1'); assert.deepEqual(called, {fqn: 'app.example', context: testContext}); });