v2 extended analysis render update
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -23,6 +23,9 @@ public class AnalysisResult {
|
||||
private final Set<String> endStates;
|
||||
private final boolean renderChoicesAsDiamonds;
|
||||
|
||||
@Builder.Default
|
||||
private final List<BusinessFlow> flows = java.util.Collections.emptyList();
|
||||
|
||||
@Builder.Default
|
||||
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@Jacksonized
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BusinessFlow {
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final List<String> steps; // List of Event names in order
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnrich
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
|
||||
@@ -24,10 +25,7 @@ import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class ExportService {
|
||||
@@ -72,10 +70,25 @@ public class ExportService {
|
||||
context.loadLibraryHints(hintsFile);
|
||||
}
|
||||
|
||||
List<BusinessFlow> flows = new ArrayList<>();
|
||||
Path flowsFile = inputDir.resolve("flows.json");
|
||||
if (!Files.exists(flowsFile)) {
|
||||
flowsFile = inputDir.resolve("src/main/resources/flows.json");
|
||||
}
|
||||
if (Files.exists(flowsFile)) {
|
||||
log.info("Loading business flows from {}", flowsFile.toAbsolutePath());
|
||||
try {
|
||||
flows = new com.fasterxml.jackson.databind.ObjectMapper()
|
||||
.readValue(flowsFile.toFile(), new com.fasterxml.jackson.core.type.TypeReference<List<BusinessFlow>>() {});
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load flows.json", e);
|
||||
}
|
||||
}
|
||||
|
||||
context.scan(inputDir);
|
||||
|
||||
CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
|
||||
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds);
|
||||
exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||
}
|
||||
|
||||
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
|
||||
@@ -85,7 +98,7 @@ public class ExportService {
|
||||
generateOutputs(outputDir, result, selectedFormats);
|
||||
}
|
||||
|
||||
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||
Set<String> processedLocations = new HashSet<>();
|
||||
|
||||
// 1. Find entry point classes (annotated or extending adapter)
|
||||
@@ -93,7 +106,7 @@ public class ExportService {
|
||||
for (TypeDeclaration td : entryPoints) {
|
||||
String fqn = context.getFqn(td);
|
||||
if (processedLocations.add(fqn)) {
|
||||
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
|
||||
processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +119,12 @@ public class ExportService {
|
||||
String uniqueId = parentFqn + "#" + methodName;
|
||||
|
||||
if (processedLocations.add(uniqueId)) {
|
||||
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds);
|
||||
processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||
String className = context.getFqn(td);
|
||||
log.info("Processing state machine config: {}", className);
|
||||
|
||||
@@ -134,6 +147,7 @@ public class ExportService {
|
||||
.startStates(startStates)
|
||||
.endStates(endStates)
|
||||
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||
.flows(flows)
|
||||
.build();
|
||||
|
||||
enrichmentService.enrich(result, context, intelligence);
|
||||
@@ -141,7 +155,7 @@ public class ExportService {
|
||||
generateOutputs(outputDir, result, selectedFormats);
|
||||
}
|
||||
|
||||
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows) throws IOException {
|
||||
TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
|
||||
String parentFqn = context.getFqn(parentClass);
|
||||
String methodName = m.getName().getIdentifier();
|
||||
@@ -160,6 +174,7 @@ public class ExportService {
|
||||
.startStates(startStates)
|
||||
.endStates(endStates)
|
||||
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
|
||||
.flows(flows)
|
||||
.build();
|
||||
|
||||
enrichmentService.enrich(result, context, intelligence);
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -41,16 +41,55 @@
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
|
||||
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 35px 0; opacity: 0.7; }
|
||||
.sidebar-header {
|
||||
padding: 30px 30px 10px;
|
||||
}
|
||||
|
||||
.ep-card {
|
||||
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
|
||||
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 15px 0; opacity: 0.7; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--primary); }
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex-grow: 1;
|
||||
padding: 25px 30px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.ep-card, .flow-card {
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
@@ -59,9 +98,10 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ep-card:hover {
|
||||
.ep-card:hover, .flow-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
|
||||
@@ -70,7 +110,8 @@
|
||||
.ep-type {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 900;
|
||||
padding: 3px 8px; border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
@@ -84,8 +125,8 @@
|
||||
.type-kafka { background: #fefce8; color: #854d0e; }
|
||||
.type-rabbit { background: #f0fdf4; color: #166534; }
|
||||
|
||||
.ep-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
|
||||
.ep-fqn { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--text-muted); opacity: 0.8; }
|
||||
.ep-name, .flow-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
|
||||
.ep-fqn, .flow-desc { font-family: 'Inter', sans-serif; font-size: 0.75rem; color: var(--text-muted); opacity: 0.9; line-height: 1.4; }
|
||||
|
||||
#main { flex-grow: 1; position: relative; background: var(--main-bg); overflow: hidden; }
|
||||
#svg-container {
|
||||
@@ -165,12 +206,25 @@
|
||||
<body>
|
||||
|
||||
<div id="sidebar">
|
||||
<header>
|
||||
<h1>Explorer</h1>
|
||||
<p>{{MACHINE_NAME}}</p>
|
||||
</header>
|
||||
<div class="sidebar-header">
|
||||
<header>
|
||||
<h1>Explorer</h1>
|
||||
<p>{{MACHINE_NAME}}</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div id="ep-list"></div>
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="explorer">Explorer</div>
|
||||
<div class="tab" data-tab="flows">Business Flows</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-explorer" class="tab-content active">
|
||||
<div id="ep-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-flows" class="tab-content">
|
||||
<div id="flow-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
@@ -191,10 +245,30 @@
|
||||
const svg = document.querySelector('svg');
|
||||
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
|
||||
|
||||
setupTabs();
|
||||
populateSidebar();
|
||||
populateFlows();
|
||||
attachInteractivity();
|
||||
});
|
||||
|
||||
function setupTabs() {
|
||||
const flows = metadata.flows || [];
|
||||
if (flows.length === 0) {
|
||||
// No flows? Hide the entire tab system to keep the UI clean
|
||||
document.querySelector('.tabs').style.display = 'none';
|
||||
document.querySelector('.tab-content[data-tab="flows"]')?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab, .tab-content').forEach(el => el.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function populateSidebar() {
|
||||
const list = document.getElementById('ep-list');
|
||||
const entryPoints = metadata.metadata.entryPoints || [];
|
||||
@@ -207,23 +281,52 @@
|
||||
<div class="ep-name">${ep.name}</div>
|
||||
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
|
||||
`;
|
||||
card.onmouseenter = () => highlightPaths(ep);
|
||||
card.onmouseenter = () => highlightEntryPoint(ep);
|
||||
card.onmouseleave = clearHighlights;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function highlightPaths(ep) {
|
||||
function populateFlows() {
|
||||
const list = document.getElementById('flow-list');
|
||||
const flows = metadata.flows || [];
|
||||
|
||||
if (flows.length === 0) {
|
||||
list.innerHTML = '<p style="color:var(--text-muted); font-size:0.8rem; font-style:italic;">No business flows defined. Add a flows.json to your project to see them here.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
flows.forEach(flow => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'flow-card';
|
||||
card.innerHTML = `
|
||||
<div class="flow-name">${flow.name}</div>
|
||||
<div class="flow-desc">${flow.description}</div>
|
||||
`;
|
||||
card.onmouseenter = () => highlightFlow(flow);
|
||||
card.onmouseleave = clearHighlights;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function highlightEntryPoint(ep) {
|
||||
clearHighlights();
|
||||
const chains = metadata.metadata.callChains || [];
|
||||
|
||||
// Surgical Filter: Match by full class and method name
|
||||
const relevantEvents = chains
|
||||
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName)
|
||||
.map(c => c.triggerPoint.event);
|
||||
|
||||
if (relevantEvents.length === 0) return;
|
||||
highlightEvents(relevantEvents);
|
||||
}
|
||||
|
||||
function highlightFlow(flow) {
|
||||
clearHighlights();
|
||||
if (!flow.steps || flow.steps.length === 0) return;
|
||||
highlightEvents(flow.steps);
|
||||
}
|
||||
|
||||
function highlightEvents(events) {
|
||||
const svg = document.querySelector('svg');
|
||||
|
||||
// Dim everything first
|
||||
@@ -231,16 +334,15 @@
|
||||
el.classList.add('dimmed');
|
||||
});
|
||||
|
||||
relevantEvents.forEach(evt => {
|
||||
events.forEach(evt => {
|
||||
const normalized = normalize(evt);
|
||||
const linkId = "#link_" + normalized;
|
||||
|
||||
// Find <a> tags via hyperlinks
|
||||
svg.querySelectorAll(`a[*|href="${linkId}"], a[href="${linkId}"]`).forEach(link => {
|
||||
link.classList.add('active-path');
|
||||
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
|
||||
|
||||
// Surgical Selection: Grab ONLY the immediate path and polygon
|
||||
// Surgical Selection
|
||||
let prev = link.previousElementSibling;
|
||||
let foundPath = false;
|
||||
let foundPolygon = false;
|
||||
@@ -262,7 +364,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Keep all nodes visible for context
|
||||
// Un-dim all state nodes for context
|
||||
svg.querySelectorAll('rect, circle, ellipse').forEach(el => {
|
||||
el.classList.remove('dimmed');
|
||||
let next = el.nextElementSibling;
|
||||
@@ -280,7 +382,6 @@
|
||||
const svg = document.querySelector('svg');
|
||||
const transitions = metadata.transitions || [];
|
||||
|
||||
// Attach tooltips to hyperlink groups
|
||||
svg.querySelectorAll('a').forEach(link => {
|
||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||
if (!href || !href.startsWith('#link_')) return;
|
||||
@@ -292,7 +393,6 @@
|
||||
if (metaTrans || chains.length > 0) {
|
||||
link.style.cursor = 'pointer';
|
||||
|
||||
// Map to the associated path
|
||||
let pathToTrigger = null;
|
||||
let prev = link.previousElementSibling;
|
||||
while (prev) {
|
||||
@@ -338,7 +438,7 @@
|
||||
|
||||
if (trans && trans.guard) {
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Guard</div>
|
||||
<div style="background:#f1f5f9; padding:8px; border-radius:6px; font-family:'JetBrains Mono', monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
|
||||
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
|
||||
}
|
||||
|
||||
if (chains && chains.length > 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnrich
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
@@ -23,23 +24,19 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class HtmlExporterTest {
|
||||
|
||||
@Test
|
||||
void verifyFirstStepOfGeneration() throws IOException {
|
||||
void shouldGenerateCompleteInteractivePortal() throws IOException {
|
||||
// 1. Setup paths
|
||||
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
|
||||
Path inputDir = projectRoot.resolve("state_machines/extended_analysis_sample");
|
||||
Path outputDir = projectRoot.resolve("state_machine_exporter/out_html_verify");
|
||||
Path inputDir = projectRoot.resolve("state_machines/ultimate_ecosystem_sm");
|
||||
|
||||
Files.createDirectories(outputDir);
|
||||
|
||||
// 2. Perform Analysis (Same as core Main)
|
||||
// 2. Perform Analysis
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setSourcepath(List.of(inputDir.toString()));
|
||||
context.setResolveBindings(true);
|
||||
context.scan(inputDir);
|
||||
|
||||
// Find the ExtendedStateMachineConfig class
|
||||
TypeDeclaration td = context.getTypeDeclarations().stream()
|
||||
.filter(t -> context.getFqn(t).equals("click.kamil.examples.statemachine.extended.config.ExtendedStateMachineConfig"))
|
||||
.filter(t -> context.getFqn(t).equals("click.kamil.ultimate.config.UltimateStateMachineConfig"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
@@ -47,14 +44,19 @@ public class HtmlExporterTest {
|
||||
List<Transition> transitions = aggregator.aggregateTransitions(td);
|
||||
aggregator.aggregateStates(td);
|
||||
|
||||
// 3. Documented Flows
|
||||
List<BusinessFlow> flows = List.of(
|
||||
BusinessFlow.builder().name("Test Flow").description("Desc").steps(List.of("CREATE")).build()
|
||||
);
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name(context.getFqn(td))
|
||||
.name("UltimatePortal")
|
||||
.transitions(transitions)
|
||||
.startStates(aggregator.getInitialStates())
|
||||
.endStates(aggregator.getEndStates())
|
||||
.flows(flows)
|
||||
.build();
|
||||
|
||||
// 3. Perform Enrichment
|
||||
EnrichmentService enrichmentService = new EnrichmentService(List.of(
|
||||
new TriggerEnricher(),
|
||||
new EntryPointEnricher(),
|
||||
@@ -63,20 +65,24 @@ public class HtmlExporterTest {
|
||||
));
|
||||
enrichmentService.enrich(result, context, new click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider(context, inputDir));
|
||||
|
||||
// 4. Run HtmlExporter
|
||||
// 4. Export
|
||||
HtmlExporter exporter = new HtmlExporter();
|
||||
String html = exporter.export(result, ExportOptions.builder().build());
|
||||
|
||||
Path outputFile = outputDir.resolve("Verification.html");
|
||||
Files.writeString(outputFile, html);
|
||||
System.out.println("Verification HTML generated at: " + outputFile.toAbsolutePath());
|
||||
|
||||
// 5. Verification
|
||||
// 5. Assertions - Structural Integrity
|
||||
assertThat(html).contains("<!DOCTYPE html>");
|
||||
assertThat(html).contains("Explorer");
|
||||
assertThat(html).contains(result.getName());
|
||||
assertThat(html).contains("<svg");
|
||||
// We will improve the "Decorator" to inject identifying classes more robustly
|
||||
// assertThat(html).contains("link_SUBMIT_EVENT");
|
||||
assertThat(html).contains("UltimatePortal");
|
||||
assertThat(html).contains("Test Flow"); // Flows tab check
|
||||
|
||||
// 6. Assertions - Data Payloads
|
||||
assertThat(html).contains("TicketRequest"); // @RequestBody extraction
|
||||
assertThat(html).contains("ticketId"); // JMS param extraction
|
||||
assertThat(html).contains("ticket.close.queue"); // Destination extraction
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package click.kamil.springstatemachineexporter.html.exporter;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class SvgDecoratorTest {
|
||||
|
||||
private final SvgDecorator decorator = new SvgDecorator();
|
||||
|
||||
@Test
|
||||
void shouldMakeSvgResponsive() {
|
||||
String rawSvg = "<svg width=\"500px\" height=\"300px\" style=\"width:500px;height:300px;background:#FFF;\" version=\"1.1\"><defs/></svg>";
|
||||
String decorated = decorator.decorate(rawSvg);
|
||||
|
||||
assertThat(decorated).contains("width=\"100%\"");
|
||||
assertThat(decorated).contains("height=\"100%\"");
|
||||
assertThat(decorated).doesNotContain("width=\"500px\"");
|
||||
assertThat(decorated).doesNotContain("style=\"width:500px;height:300px;background:#FFF;\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTouchInternalElementDimensions() {
|
||||
String rawSvg = "<svg width=\"500\"><rect width=\"10\" height=\"10\"/></svg>";
|
||||
String decorated = decorator.decorate(rawSvg);
|
||||
|
||||
assertThat(decorated).contains("<svg width=\"100%\"");
|
||||
assertThat(decorated).contains("<rect width=\"10\" height=\"10\"/>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInjectInteractivityStyles() {
|
||||
String rawSvg = "<svg><defs/></svg>";
|
||||
String decorated = decorator.decorate(rawSvg);
|
||||
|
||||
assertThat(decorated).contains(".active-path");
|
||||
assertThat(decorated).contains(".dimmed");
|
||||
assertThat(decorated).contains("@keyframes sm-flow");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "Standard Ticket Lifecycle",
|
||||
"description": "The typical path for a ticket: Creation -> Triage -> Progress -> Resolution -> Closing.",
|
||||
"steps": ["CREATE", "ASSIGN", "RESOLVE", "CLOSE_TICKET"]
|
||||
},
|
||||
{
|
||||
"name": "AWS-Triggered Escalation",
|
||||
"description": "Emergency flow triggered by AWS infrastructure: SNS Update -> SQS Escalation.",
|
||||
"steps": ["SNS_UPDATE", "SQS_UPDATE", "ESCALATE"]
|
||||
},
|
||||
{
|
||||
"name": "Reactive Creation Path",
|
||||
"description": "Modern non-blocking ticket creation flow via WebFlux.",
|
||||
"steps": ["CREATE_REACTIVE", "ASSIGN"]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user