transition enricher
This commit is contained in:
@@ -58,7 +58,20 @@ public class HtmlExporter implements StateMachineExporter {
|
||||
|
||||
// Check if we should render metadata pane
|
||||
boolean hideMetadataProp = Boolean.getBoolean("html.hideMetadataPane");
|
||||
boolean hasEntryPoints = result.getMetadata() != null && result.getMetadata().getEntryPoints() != null && !result.getMetadata().getEntryPoints().isEmpty();
|
||||
boolean hasEntryPoints = false;
|
||||
if (result.getMetadata() != null && result.getMetadata().getEntryPoints() != null && !result.getMetadata().getEntryPoints().isEmpty()) {
|
||||
Set<String> smEvents = result.getTransitions().stream()
|
||||
.filter(t -> t.getEvent() != null)
|
||||
.map(t -> {
|
||||
String eventStr = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
||||
return eventStr == null ? null : eventStr.replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
if (result.getMetadata().getCallChains() != null) {
|
||||
hasEntryPoints = result.getMetadata().getCallChains().stream()
|
||||
.anyMatch(c -> c.getMatchedTransitions() != null && !c.getMatchedTransitions().isEmpty());
|
||||
}
|
||||
}
|
||||
boolean hasFlows = result.getFlows() != null && !result.getFlows().isEmpty();
|
||||
boolean shouldRenderPane = !hideMetadataProp && options.isIncludeMetadataPane() && (hasEntryPoints || hasFlows);
|
||||
|
||||
|
||||
@@ -327,14 +327,13 @@
|
||||
const list = document.getElementById('ep-list');
|
||||
if (!list) return;
|
||||
const entryPoints = metadata.metadata.entryPoints || [];
|
||||
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
|
||||
|
||||
entryPoints.forEach(ep => {
|
||||
// Quality Filter: Hide if it doesn't trigger a machine event
|
||||
const chains = (metadata.metadata.callChains || []).filter(c =>
|
||||
c.entryPoint.className === ep.className &&
|
||||
c.entryPoint.methodName === ep.methodName &&
|
||||
machineEvents.has(c.triggerPoint.event)
|
||||
c.matchedTransitions && c.matchedTransitions.length > 0
|
||||
);
|
||||
|
||||
if (chains.length === 0) return;
|
||||
@@ -348,6 +347,22 @@
|
||||
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
|
||||
${sourceHint}
|
||||
`;
|
||||
|
||||
// Add tags for matched events
|
||||
let triggersHtml = '<div class="ep-transitions" style="margin-top: 8px;">Triggers: ';
|
||||
const addedEvents = new Set();
|
||||
chains.forEach(c => {
|
||||
c.matchedTransitions.forEach(t => {
|
||||
const evName = t.event || "";
|
||||
if (!addedEvents.has(evName)) {
|
||||
addedEvents.add(evName);
|
||||
triggersHtml += `<span class="badge transition" style="font-size: 0.65rem; padding: 2px 6px;">${evName}</span> `;
|
||||
}
|
||||
});
|
||||
});
|
||||
triggersHtml += '</div>';
|
||||
card.innerHTML += triggersHtml;
|
||||
|
||||
card.onmouseenter = () => highlightEntryPoint(ep);
|
||||
card.onmouseleave = clearHighlights;
|
||||
list.appendChild(card);
|
||||
@@ -379,14 +394,21 @@
|
||||
|
||||
function highlightEntryPoint(ep) {
|
||||
clearHighlights();
|
||||
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
|
||||
const chains = metadata.metadata.callChains || [];
|
||||
const relevantEvents = chains
|
||||
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName && machineEvents.has(c.triggerPoint.event))
|
||||
.map(c => c.triggerPoint.event);
|
||||
|
||||
let steps = [];
|
||||
chains.forEach(c => {
|
||||
if (c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName) {
|
||||
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
|
||||
c.matchedTransitions.forEach(t => {
|
||||
steps.push({ event: t.event, source: t.sourceState });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (relevantEvents.length === 0) return;
|
||||
highlightEvents(relevantEvents);
|
||||
if (steps.length === 0) return;
|
||||
highlightEvents(steps);
|
||||
}
|
||||
|
||||
function highlightFlow(flow) {
|
||||
@@ -404,19 +426,38 @@
|
||||
});
|
||||
|
||||
steps.forEach(step => {
|
||||
let linkId = "";
|
||||
if (step.includes("->")) {
|
||||
// Named Choice Branch: "SRC->TGT"
|
||||
let targetEvent = "";
|
||||
let targetSource = "";
|
||||
let targetAnon = "";
|
||||
|
||||
if (typeof step === 'object') {
|
||||
targetEvent = normalize(step.event);
|
||||
targetSource = normalize(step.source);
|
||||
} else if (step.includes("->")) {
|
||||
const parts = step.split("->");
|
||||
linkId = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
||||
targetAnon = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
||||
} else {
|
||||
// Standard Event
|
||||
linkId = "#link_" + normalize(step);
|
||||
targetEvent = normalize(step);
|
||||
}
|
||||
|
||||
svg.querySelectorAll('a').forEach(link => {
|
||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||
if (href === linkId) {
|
||||
if (!href) return;
|
||||
|
||||
let matches = false;
|
||||
if (targetAnon) {
|
||||
matches = (href === targetAnon);
|
||||
} else if (targetEvent) {
|
||||
if (targetSource) {
|
||||
// Exact match
|
||||
matches = (href === "#link_" + targetSource + "__" + targetEvent);
|
||||
} else {
|
||||
// Match all sources for this event
|
||||
matches = href.startsWith("#link_") && href.endsWith("__" + targetEvent);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
highlightLinkGroup(link);
|
||||
}
|
||||
});
|
||||
@@ -481,20 +522,31 @@
|
||||
const isAnon = linkId.startsWith('anon_');
|
||||
|
||||
let metaTrans = null;
|
||||
let cleanEventName = linkId;
|
||||
if (isAnon) {
|
||||
metaTrans = transitions.find(t =>
|
||||
!t.event &&
|
||||
"anon_" + normalize(t.sourceStates[0].fullIdentifier) + "_" + normalize(t.targetStates[0].fullIdentifier) === linkId
|
||||
);
|
||||
cleanEventName = "Internal logic";
|
||||
} else {
|
||||
metaTrans = transitions.find(t => normalize(t.event) === linkId);
|
||||
metaTrans = transitions.find(t => {
|
||||
if (!t.event) return false;
|
||||
const evStr = t.event.fullIdentifier || t.event.rawName || t.event;
|
||||
const sourceStr = t.sourceStates && t.sourceStates.length > 0 ? (t.sourceStates[0].fullIdentifier || t.sourceStates[0].rawName) : "";
|
||||
const expectedLinkId = normalize(sourceStr) + "__" + normalize(evStr);
|
||||
return expectedLinkId === linkId;
|
||||
});
|
||||
if (metaTrans && metaTrans.event) {
|
||||
cleanEventName = metaTrans.event.fullIdentifier || metaTrans.event.rawName || metaTrans.event;
|
||||
}
|
||||
}
|
||||
|
||||
if (metaTrans || isAnon) {
|
||||
link.style.cursor = 'pointer';
|
||||
const setupTippy = (el) => {
|
||||
tippy(el, {
|
||||
content: createTooltip(isAnon ? "Internal logic" : linkId, metaTrans),
|
||||
content: createTooltip(cleanEventName, metaTrans),
|
||||
allowHTML: true,
|
||||
theme: 'modern',
|
||||
interactive: true,
|
||||
@@ -520,7 +572,13 @@
|
||||
}
|
||||
|
||||
function createTooltip(name, trans) {
|
||||
const chains = (metadata.metadata.callChains || []).filter(c => normalize(c.triggerPoint.event) === name);
|
||||
const chains = (metadata.metadata.callChains || []).filter(c => {
|
||||
if (!c.matchedTransitions || c.matchedTransitions.length === 0) return false;
|
||||
return c.matchedTransitions.some(t => {
|
||||
const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;
|
||||
return normalize(cEventStr) === name;
|
||||
});
|
||||
});
|
||||
|
||||
let html = `<div class="tooltip-content">
|
||||
<div style="font-weight:900; font-size:1.1rem; margin-bottom:12px; border-bottom:1px solid #eee; padding-bottom:8px; line-height:1.3;">
|
||||
|
||||
@@ -81,8 +81,84 @@ public class HtmlExporterTest {
|
||||
|
||||
// 7. Assertions - Surgical SVG Identification
|
||||
// Every event should be wrapped in an identifying hyperlink for robust JS selection
|
||||
assertThat(html).contains("#link_CREATE");
|
||||
assertThat(html).contains("#link_ASSIGN");
|
||||
assertThat(html).contains("#link_CLOSE_TICKET");
|
||||
assertThat(html).contains("__CREATE");
|
||||
assertThat(html).contains("__ASSIGN");
|
||||
assertThat(html).contains("__CLOSE_TICKET");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderSidebarWhenMatchedTransitionsExist() throws IOException {
|
||||
click.kamil.springstatemachineexporter.analysis.model.EntryPoint ep = click.kamil.springstatemachineexporter.analysis.model.EntryPoint.builder().name("Test").build();
|
||||
click.kamil.springstatemachineexporter.analysis.model.CallChain chain = click.kamil.springstatemachineexporter.analysis.model.CallChain.builder()
|
||||
.matchedTransitions(List.of(click.kamil.springstatemachineexporter.analysis.model.MatchedTransition.builder().event("TEST_EVENT").build()))
|
||||
.build();
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata metadata = click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.entryPoints(List.of(ep))
|
||||
.callChains(List.of(chain))
|
||||
.build();
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("SidebarTest")
|
||||
.transitions(List.of())
|
||||
.startStates(java.util.Collections.emptySet())
|
||||
.endStates(java.util.Collections.emptySet())
|
||||
.metadata(metadata)
|
||||
.build();
|
||||
|
||||
HtmlExporter exporter = new HtmlExporter();
|
||||
String html = exporter.export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
||||
|
||||
assertThat(html).contains("id=\"sidebar\"");
|
||||
assertThat(html).contains("id=\"ep-list\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRenderSidebarWhenNoMatchedTransitionsExist() throws IOException {
|
||||
click.kamil.springstatemachineexporter.analysis.model.EntryPoint ep = click.kamil.springstatemachineexporter.analysis.model.EntryPoint.builder().name("Test").build();
|
||||
click.kamil.springstatemachineexporter.analysis.model.CallChain chain = click.kamil.springstatemachineexporter.analysis.model.CallChain.builder()
|
||||
.triggerPoint(click.kamil.springstatemachineexporter.analysis.model.TriggerPoint.builder().event("TEST_EVENT").build())
|
||||
.matchedTransitions(List.of()) // EMPTY
|
||||
.build();
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata metadata = click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.entryPoints(List.of(ep))
|
||||
.callChains(List.of(chain))
|
||||
.build();
|
||||
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(click.kamil.springstatemachineexporter.model.Event.of("TEST_EVENT"));
|
||||
transition.setSourceStates(List.of());
|
||||
transition.setTargetStates(List.of());
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("SidebarTestEmpty")
|
||||
.transitions(List.of(transition))
|
||||
.startStates(java.util.Collections.emptySet())
|
||||
.endStates(java.util.Collections.emptySet())
|
||||
.metadata(metadata)
|
||||
.build();
|
||||
|
||||
HtmlExporter exporter = new HtmlExporter();
|
||||
String html = exporter.export(result, ExportOptions.builder().includeMetadataPane(true).build());
|
||||
|
||||
assertThat(html).doesNotContain("id=\"sidebar\"");
|
||||
assertThat(html).doesNotContain("id=\"ep-list\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContainCorrectJsLogicForEventTooltipResolution() throws IOException {
|
||||
HtmlExporter exporter = new HtmlExporter();
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("JsTest")
|
||||
.transitions(List.of())
|
||||
.startStates(java.util.Collections.emptySet())
|
||||
.endStates(java.util.Collections.emptySet())
|
||||
.build();
|
||||
|
||||
String html = exporter.export(result, ExportOptions.builder().build());
|
||||
|
||||
// Verify that the template gracefully unwraps the Event JSON object for interactive hovers in attachInteractivity
|
||||
assertThat(html).contains("const evStr = t.event.fullIdentifier || t.event.rawName || t.event;");
|
||||
|
||||
// Verify that the template gracefully unwraps the Event JSON object when filtering call chains in createTooltip
|
||||
assertThat(html).contains("const cEventStr = typeof t.event === 'object' ? (t.event.fullIdentifier || t.event.rawName || t.event) : t.event;");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user