diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/Mdl.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/Mdl.java index 7eb9afdff0..d8ea0763d4 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/Mdl.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/Mdl.java @@ -65,7 +65,7 @@ public enum Actrs implements Actor { .description( "dependency" ) .tags( set( "abc", "ghi", "jkl", "mno" ) ) ) .call( a -> a - .from( Actrs.AVA ).to( Actrs.BEN ) + .from( Actrs.AVA ).to( Actrs.CHE ) .request( REQ ).response( RES ) ) ); /** diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/FileIndexTest.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/FileIndexTest.java index 2545de3eeb..c294bf8103 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/FileIndexTest.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/FileIndexTest.java @@ -1,5 +1,6 @@ package com.mastercard.test.flow.report.index; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import com.mastercard.test.flow.report.seq.Browser; @@ -16,4 +17,13 @@ class FileIndexTest extends AbstractIndexTest { FileIndexTest() { super( report.fileUrl() ); } + + /** + * Shows that the interactions panel is not shown when we can't load the flow + * details + */ + @Test + void noInteractions() { + iseq.hasInteractionSummary( "" ); + } } diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java index 6eabc82f32..c0de960c40 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import com.mastercard.test.flow.report.seq.Browser; @@ -23,6 +24,61 @@ class ServedIndexTest extends AbstractIndexTest { super( report.url() ); } + /** + * Checks that the interaction diagram for all flows is show as expected + */ + @Test + void interactions() { + iseq.hasInteractionSummary( "2 interactions between 3 actors" ) + .expandInteractions() + .hasInteractions( + "Nodes:", + " AVA", + " BEN", + " CHE", + "Edges:", + " AVA normal solid BEN", + " AVA normal solid CHE" ); + } + + /** + * Checks that the interaction diagram for a filtered flow list is show as + * expected + */ + @Test + void filteredInteractions() { + iseq.clickTag( "PASS" ) + .hasInteractionSummary( "1 interactions between 2 actors" ) + .expandInteractions() + .hasInteractions( + "Nodes:", + " AVA", + " BEN", + " CHE", + "Edges:", + " AVA normal solid BEN", + " AVA thick solid CHE " ); + } + + /** + * Checks that the interaction diagram highlights the hovered flow as expected + */ + @Test + void hoveredInteractions() { + iseq + .expandInteractions() + .hoverEntry( "basis [PASS, abc, def]" ) + .hasInteractionSummary( "1 interactions between 2 actors" ) + .hasInteractions( + "Nodes:", + " AVA", + " BEN", + " CHE", + "Edges:", + " AVA thick solid BEN", + " AVA normal dotted CHE" ); + } + /** * Checks that the expected log file has been created *

diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java index e2f3aba2b3..899c41d738 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java @@ -3,18 +3,30 @@ import static java.time.Duration.ofSeconds; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.openqa.selenium.support.ui.ExpectedConditions.elementToBeClickable; +import java.io.StringReader; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + import org.junit.jupiter.api.Assertions; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.WebDriverWait; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; /** * Encapsulates the nuts and bolts of interacting with the index page so the @@ -56,6 +68,42 @@ public IndexSequence index( String... arguments ) { return get( url + args ); } + /** + * Hovers the pointer over an index entry + * + * @param name The text of the index item (same format as asserted by + * {@link #hasFlows(String...)}) + * @return this + */ + public IndexSequence hoverEntry( String name ) { + trace( "hoverEntry", name ); + List flowItems = driver.findElements( By.tagName( "app-flow-nav-item" ) ); + int width = flowItems.stream() + .map( IndexSequence::flowDescription ) + .mapToInt( String::length ) + .max() + .orElse( 0 ); + WebElement linkItem = flowItems.stream() + .filter( e -> name.equals( printFlow( width, e ) ) ) + .findFirst() + .orElse( null ); + if( linkItem == null ) { + Assertions.fail( String.format( + "Failed to find detail link '%s' in%s", + name, + flowItems.stream() + .map( e -> "\n" + printFlow( width, e ) ) + .collect( joining() ) ) ); + } + else { + new Actions( driver ) + .moveToElement( linkItem ) + .build() + .perform(); + } + return self(); + } + /** * Navigates to a flow detail page by clicking on an index item * @@ -301,6 +349,117 @@ public IndexSequence dragToInclude( String tag ) { return dragChip( tag, "tag_exclude", "tag_include" ); } + /** + * Asserts on the displayed interaction summary text. + * + * @param expected The expected text, or the empty string if we're expecting + * that element not to be shown at all + * @return this + */ + public IndexSequence hasInteractionSummary( String expected ) { + trace( "hasInteractionSummary", expected ); + assertEquals( expected, driver + .findElements( By.id( "interaction_summary" ) ).stream() + .map( WebElement::getText ) + .collect( joining( "\n" ) ), + "Interaction summary" ); + return self(); + } + + /** + * Clicks on the expansion panel to show the interaction diagram. + * + * @return this + */ + public IndexSequence expandInteractions() { + trace( "expandInteractions" ); + driver.findElement( By.id( "interactions_title" ) ) + .click(); + new WebDriverWait( driver, ofSeconds( 2 ) ) + .until( elementToBeClickable( By.id( "interactions_diagram" ) ) ); + return self(); + } + + /** + * Asserts on the displayed interaction diagram. + * + * @param expected The name of the resource file that contains the expected SVG + * content + * @return this + */ + public IndexSequence hasInteractions( String... expected ) { + trace( "hasInteractions", (Object[]) expected ); + String svg = driver + .findElement( By.id( "interactions_diagram" ) ) + .getAttribute( "innerHTML" ); + + String svgSummary = summariseSVG( svg ); + + assertEquals( + copypasta( expected ), + copypasta( svgSummary ), + "interaction diagram structure" ); + + return self(); + } + + /** + * Extracts the gist of the interaction diagram. + * + * @param svg The diagram + * @return A string that describes the diagram + */ + private static String summariseSVG( String svg ) { + try { + Document doc = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse( new InputSource( new StringReader( svg ) ) ); + XPath xpath = XPathFactory.newInstance().newXPath(); + + StringBuilder sb = new StringBuilder(); + + sb.append( "Nodes:" ); + NodeList nl = (NodeList) xpath.compile( "//span[@class='nodeLabel']" ) + .evaluate( doc, XPathConstants.NODESET ); + for( int i = 0; i < nl.getLength(); i++ ) { + sb.append( "\n " ) + .append( nl.item( i ).getTextContent() ); + } + + sb.append( "\nEdges:" ); + nl = (NodeList) xpath.compile( "//path[@class!='arrowMarkerPath']" ) + .evaluate( doc, XPathConstants.NODESET ); + for( int i = 0; i < nl.getLength(); i++ ) { + Set classes = Stream.of( nl.item( i ) + .getAttributes() + .getNamedItem( "class" ) + .getTextContent() + .split( " " ) ) + .collect( toSet() ); + + sb.append( "\n " ); + Stream.of( "LS-", "edge-thickness-", "edge-pattern-", "LE-" ) + .forEach( prefix -> { + sb.append( " " ) + .append( classes.stream() + .filter( c -> c.startsWith( prefix ) ) + .map( c -> c.substring( prefix.length() ) ) + .findFirst().orElse( prefix + "???" ) ); + } ); + + if( nl.item( i ).getAttributes().getNamedItem( "style" ).getTextContent() + .contains( "stroke-width: 0" ) ) { + sb.append( " " ); + } + } + + return sb.toString(); + } + catch( Exception e ) { + throw new IllegalStateException( "failed to summarise\n" + svg, e ); + } + } + private IndexSequence dragChip( String text, String sourceId, String destId ) { WebElement source = driver.findElement( By.id( sourceId ) ); WebElement chip = source.findElements( By.tagName( "mat-chip" ) ).stream() @@ -338,4 +497,5 @@ private static String printFlow( int descWidth, WebElement e ) { String fmt = "%-" + descWidth + "s %s"; return String.format( fmt, flowDescription( e ), flowTags( e ) ); } + } diff --git a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html index 5adacc8eab..a7b72ea911 100644 --- a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html +++ b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html @@ -1,10 +1,10 @@ - Interactions - {{summary}} + Interactions + {{summary}}

-

+        

     
\ No newline at end of file