2021-12-12 14:56:12 -08:00
/ *
Copyright ( c ) Microsoft Corporation .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2022-09-20 18:41:51 -07:00
import type { TestAttachment , TestCase , TestResult , TestStep } from './types' ;
2021-12-12 14:56:12 -08:00
import * as React from 'react' ;
import { TreeItem } from './treeItem' ;
2024-07-15 12:20:22 -07:00
import { msToString } from './utils' ;
2021-12-14 19:25:07 -08:00
import { AutoChip } from './chip' ;
2021-12-12 14:56:12 -08:00
import { traceImage } from './images' ;
2024-12-16 15:25:32 +01:00
import { Anchor , AttachmentLink , generateTraceUrl , testResultHref } from './links' ;
2021-12-12 14:56:12 -08:00
import { statusIcon } from './statusIcon' ;
2023-12-22 10:17:35 -08:00
import type { ImageDiff } from '@web/shared/imageDiffView' ;
import { ImageDiffView } from '@web/shared/imageDiffView' ;
2024-10-10 16:49:17 -07:00
import { TestErrorView , TestScreenshotErrorView } from './testErrorView' ;
2024-12-16 15:25:32 +01:00
import * as icons from './icons' ;
2021-12-12 14:56:12 -08:00
import './testResultView.css' ;
2024-12-16 15:25:32 +01:00
interface ImageDiffWithAnchors extends ImageDiff {
anchors : string [ ] ;
}
2025-01-02 17:48:59 +01:00
function groupImageDiffs ( screenshots : Set < TestAttachment > , result : TestResult ) : ImageDiffWithAnchors [ ] {
2024-12-16 15:25:32 +01:00
const snapshotNameToImageDiff = new Map < string , ImageDiffWithAnchors > ( ) ;
2022-03-30 16:42:08 -08:00
for ( const attachment of screenshots ) {
const match = attachment . name . match ( /^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/ ) ;
2022-03-11 09:46:13 -07:00
if ( ! match )
continue ;
const [ , name , category , extension = '' ] = match ;
const snapshotName = name + extension ;
2022-03-30 16:42:08 -08:00
let imageDiff = snapshotNameToImageDiff . get ( snapshotName ) ;
if ( ! imageDiff ) {
2024-12-16 15:25:32 +01:00
imageDiff = { name : snapshotName , anchors : [ ` attachment- ${ name } ` ] } ;
2022-03-30 16:42:08 -08:00
snapshotNameToImageDiff . set ( snapshotName , imageDiff ) ;
2022-03-11 09:46:13 -07:00
}
2025-01-02 17:48:59 +01:00
imageDiff . anchors . push ( ` attachment- ${ result . attachments . indexOf ( attachment ) } ` ) ;
2022-03-30 16:42:08 -08:00
if ( category === 'actual' )
2022-04-01 17:11:15 -08:00
imageDiff . actual = { attachment } ;
2022-03-30 16:42:08 -08:00
if ( category === 'expected' )
2022-04-01 17:11:15 -08:00
imageDiff . expected = { attachment , title : 'Expected' } ;
2022-03-30 16:42:08 -08:00
if ( category === 'previous' )
2022-04-01 17:11:15 -08:00
imageDiff . expected = { attachment , title : 'Previous' } ;
2022-03-30 16:42:08 -08:00
if ( category === 'diff' )
2022-04-01 17:11:15 -08:00
imageDiff . diff = { attachment } ;
2022-03-11 09:46:13 -07:00
}
2022-03-30 16:42:08 -08:00
for ( const [ name , diff ] of snapshotNameToImageDiff ) {
2022-04-01 17:11:15 -08:00
if ( ! diff . actual || ! diff . expected ) {
2022-03-30 16:42:08 -08:00
snapshotNameToImageDiff . delete ( name ) ;
} else {
2022-04-01 17:11:15 -08:00
screenshots . delete ( diff . actual . attachment ) ;
screenshots . delete ( diff . expected . attachment ) ;
2022-03-30 16:42:08 -08:00
screenshots . delete ( diff . diff ? . attachment ! ) ;
}
}
return [ . . . snapshotNameToImageDiff . values ( ) ] ;
2022-03-11 09:46:13 -07:00
}
2021-12-15 19:19:43 -08:00
2021-12-12 14:56:12 -08:00
export const TestResultView : React.FC < {
test : TestCase ,
result : TestResult ,
2024-12-16 15:25:32 +01:00
} > = ( { test , result } ) = > {
const { screenshots , videos , traces , otherAttachments , diffs , errors , otherAttachmentAnchors , screenshotAnchors } = React . useMemo ( ( ) = > {
2025-01-02 17:48:59 +01:00
const attachments = result . attachments ;
2022-03-30 16:42:08 -08:00
const screenshots = new Set ( attachments . filter ( a = > a . contentType . startsWith ( 'image/' ) ) ) ;
2025-01-02 17:48:59 +01:00
const screenshotAnchors = [ . . . screenshots ] . map ( a = > ` attachment- ${ attachments . indexOf ( a ) } ` ) ;
2024-10-14 14:16:19 +02:00
const videos = attachments . filter ( a = > a . contentType . startsWith ( 'video/' ) ) ;
2022-03-30 16:42:08 -08:00
const traces = attachments . filter ( a = > a . name === 'trace' ) ;
const otherAttachments = new Set < TestAttachment > ( attachments ) ;
2024-12-16 15:25:32 +01:00
[ . . . screenshots , . . . videos , . . . traces ] . forEach ( a = > otherAttachments . delete ( a ) ) ;
2025-01-02 17:48:59 +01:00
const otherAttachmentAnchors = [ . . . otherAttachments ] . map ( a = > ` attachment- ${ attachments . indexOf ( a ) } ` ) ;
const diffs = groupImageDiffs ( screenshots , result ) ;
2024-10-10 16:49:17 -07:00
const errors = classifyErrors ( result . errors , diffs ) ;
2024-12-16 15:25:32 +01:00
return { screenshots : [ . . . screenshots ] , videos , traces , otherAttachments , diffs , errors , otherAttachmentAnchors , screenshotAnchors } ;
2022-08-18 20:12:33 +02:00
} , [ result ] ) ;
2021-12-12 14:56:12 -08:00
return < div className = 'test-result' >
2024-10-10 16:49:17 -07:00
{ ! ! errors . length && < AutoChip header = 'Errors' >
{ errors . map ( ( error , index ) = > {
if ( error . type === 'screenshot' )
return < TestScreenshotErrorView key = { 'test-result-error-message-' + index } errorPrefix = { error . errorPrefix } diff = { error . diff ! } errorSuffix = { error . errorSuffix } > < / TestScreenshotErrorView > ;
return < TestErrorView key = { 'test-result-error-message-' + index } error = { error . error ! } > < / TestErrorView > ;
} ) }
2021-12-14 19:25:07 -08:00
< / AutoChip > }
{ ! ! result . steps . length && < AutoChip header = 'Test Steps' >
2024-12-16 15:25:32 +01:00
{ result . steps . map ( ( step , i ) = > < StepTreeItem key = { ` step- ${ i } ` } step = { step } result = { result } test = { test } depth = { 0 } / > ) }
2021-12-14 19:25:07 -08:00
< / AutoChip > }
2021-12-12 14:56:12 -08:00
2022-03-30 16:42:08 -08:00
{ diffs . map ( ( diff , index ) = >
2024-12-16 15:25:32 +01:00
< Anchor key = { ` diff- ${ index } ` } id = { diff . anchors } >
< AutoChip dataTestId = 'test-results-image-diff' header = { ` Image mismatch: ${ diff . name } ` } revealOnAnchorId = { diff . anchors } >
2024-11-19 16:40:02 +01:00
< ImageDiffView diff = { diff } / >
< / AutoChip >
< / Anchor >
2022-03-11 09:46:13 -07:00
) }
2021-12-12 14:56:12 -08:00
2024-12-16 15:25:32 +01:00
{ ! ! screenshots . length && < AutoChip header = 'Screenshots' revealOnAnchorId = { screenshotAnchors } >
2021-12-12 14:56:12 -08:00
{ screenshots . map ( ( a , i ) = > {
2025-01-02 17:48:59 +01:00
return < Anchor key = { ` screenshot- ${ i } ` } id = { ` attachment- ${ result . attachments . indexOf ( a ) } ` } >
2023-09-11 08:44:00 +02:00
< a href = { a . path } >
2023-12-22 10:17:35 -08:00
< img className = 'screenshot' src = { a . path } / >
2023-09-11 08:44:00 +02:00
< / a >
2025-01-02 17:48:59 +01:00
< AttachmentLink attachment = { a } result = { result } > < / AttachmentLink >
2024-12-16 15:25:32 +01:00
< / Anchor > ;
2021-12-12 14:56:12 -08:00
} ) }
2021-12-14 19:25:07 -08:00
< / AutoChip > }
2021-12-12 14:56:12 -08:00
2024-12-16 15:25:32 +01:00
{ ! ! traces . length && < Anchor id = 'attachment-trace' > < AutoChip header = 'Traces' revealOnAnchorId = 'attachment-trace' >
2022-02-16 09:09:42 -08:00
{ < div >
2022-07-11 19:47:15 -07:00
< a href = { generateTraceUrl ( traces ) } >
2023-12-22 10:17:35 -08:00
< img className = 'screenshot' src = { traceImage } style = { { width : 192 , height : 117 , marginLeft : 20 } } / >
2021-12-12 14:56:12 -08:00
< / a >
2025-01-02 17:48:59 +01:00
{ traces . map ( ( a , i ) = > < AttachmentLink key = { ` trace- ${ i } ` } attachment = { a } result = { result } linkName = { traces . length === 1 ? 'trace' : ` trace- ${ i + 1 } ` } > < / AttachmentLink > ) }
2022-02-16 09:09:42 -08:00
< / div > }
2024-11-19 16:40:02 +01:00
< / AutoChip > < / Anchor > }
2021-12-12 14:56:12 -08:00
2024-12-16 15:25:32 +01:00
{ ! ! videos . length && < Anchor id = 'attachment-video' > < AutoChip header = 'Videos' revealOnAnchorId = 'attachment-video' >
2021-12-12 14:56:12 -08:00
{ videos . map ( ( a , i ) = > < div key = { ` video- ${ i } ` } >
< video controls >
< source src = { a . path } type = { a . contentType } / >
< / video >
2025-01-02 17:48:59 +01:00
< AttachmentLink attachment = { a } result = { result } > < / AttachmentLink >
2021-12-12 14:56:12 -08:00
< / div > ) }
2024-11-19 16:40:02 +01:00
< / AutoChip > < / Anchor > }
2021-12-12 14:56:12 -08:00
2024-12-16 15:25:32 +01:00
{ ! ! otherAttachments . size && < AutoChip header = 'Attachments' revealOnAnchorId = { otherAttachmentAnchors } >
{ [ . . . otherAttachments ] . map ( ( a , i ) = >
2025-01-02 17:48:59 +01:00
< Anchor key = { ` attachment-link- ${ i } ` } id = { ` attachment- ${ result . attachments . indexOf ( a ) } ` } >
< AttachmentLink attachment = { a } result = { result } openInNewTab = { a . contentType . startsWith ( 'text/html' ) } / >
2024-12-16 15:25:32 +01:00
< / Anchor >
2024-09-02 08:35:53 +02:00
) }
2021-12-14 19:25:07 -08:00
< / AutoChip > }
2021-12-12 14:56:12 -08:00
< / div > ;
} ;
2024-10-10 16:49:17 -07:00
function classifyErrors ( testErrors : string [ ] , diffs : ImageDiff [ ] ) {
return testErrors . map ( error = > {
2024-10-25 12:36:39 -07:00
const firstLine = error . split ( '\n' ) [ 0 ] ;
if ( firstLine . includes ( 'toHaveScreenshot' ) || firstLine . includes ( 'toMatchSnapshot' ) ) {
2024-10-10 16:49:17 -07:00
const matchingDiff = diffs . find ( diff = > {
const attachmentName = diff . actual ? . attachment . name ;
return attachmentName && error . includes ( attachmentName ) ;
} ) ;
if ( matchingDiff ) {
const lines = error . split ( '\n' ) ;
const index = lines . findIndex ( line = > /Expected:|Previous:|Received:/ . test ( line ) ) ;
const errorPrefix = index !== - 1 ? lines . slice ( 0 , index ) . join ( '\n' ) : lines [ 0 ] ;
const diffIndex = lines . findIndex ( line = > / +Diff:/ . test ( line ) ) ;
const errorSuffix = diffIndex !== - 1 ? lines . slice ( diffIndex + 2 ) . join ( '\n' ) : lines . slice ( 1 ) . join ( '\n' ) ;
return { type : 'screenshot' , diff : matchingDiff , errorPrefix , errorSuffix } ;
}
}
return { type : 'regular' , error } ;
} ) ;
}
2021-12-12 14:56:12 -08:00
const StepTreeItem : React.FC < {
2024-12-16 15:25:32 +01:00
test : TestCase ;
result : TestResult ;
2021-12-12 14:56:12 -08:00
step : TestStep ;
depth : number ,
2024-12-16 15:25:32 +01:00
} > = ( { test , step , result , depth } ) = > {
return < TreeItem title = { < span aria-label = { step . title } >
2021-12-12 14:56:12 -08:00
< span style = { { float : 'right' } } > { msToString ( step . duration ) } < / span >
2025-01-02 17:48:59 +01:00
{ step . attachments . length > 0 && < a style = { { float : 'right' } } title = 'link to attachment' href = { testResultHref ( { test , result , anchor : ` attachment- ${ step . attachments [ 0 ] } ` } ) } onClick = { evt = > { evt . stopPropagation ( ) ; } } > { icons . attachment ( ) } < / a > }
2021-12-12 14:56:12 -08:00
{ statusIcon ( step . error || step . duration === - 1 ? 'failed' : 'passed' ) }
< span > { step . title } < / span >
2022-01-03 21:17:17 -08:00
{ step . count > 1 && < > ✕ < span className = 'test-result-counter' > { step . count } < / span > < / > }
2021-12-12 14:56:12 -08:00
{ step . location && < span className = 'test-result-path' > — { step . location . file } : { step . location . line } < / span > }
< / span > } loadChildren = { step . steps . length + ( step . snippet ? 1 : 0 ) ? ( ) = > {
2024-12-16 15:25:32 +01:00
const children = step . steps . map ( ( s , i ) = > < StepTreeItem key = { i } step = { s } depth = { depth + 1 } result = { result } test = { test } / > ) ;
2021-12-12 14:56:12 -08:00
if ( step . snippet )
2024-12-16 15:25:32 +01:00
children . unshift ( < TestErrorView testId = 'test-snippet' key = 'line' error = { step . snippet } / > ) ;
2021-12-12 14:56:12 -08:00
return children ;
2024-12-16 15:25:32 +01:00
} : undefined } depth = { depth } / > ;
2021-12-12 14:56:12 -08:00
} ;