2021-06-07 02:09:53 +02: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-06-30 21:17:08 +02:00
import { colors , ms as milliseconds , parseStackTraceLine } from 'playwright-core/lib/utilsBundle' ;
2021-06-07 02:09:53 +02:00
import fs from 'fs' ;
import path from 'path' ;
2023-01-24 02:44:23 +01:00
import type { FullConfig , TestCase , Suite , TestResult , TestError , FullResult , TestStep , Location , Reporter } from '../../types/testReporter' ;
2023-01-27 02:26:47 +01:00
import type { FullConfigInternal } from '../common/types' ;
import { codeFrameColumns } from '../common/babelBundle' ;
2022-12-20 23:13:10 +01:00
import { monotonicTime } from 'playwright-core/lib/utils' ;
2021-06-07 02:09:53 +02:00
2021-10-04 10:32:56 +02:00
export type TestResultOutput = { chunk : string | Buffer , type : 'stdout' | 'stderr' } ;
export const kOutputSymbol = Symbol ( 'output' ) ;
type Annotation = {
title : string ;
message : string ;
2022-01-05 01:00:55 +01:00
location? : Location ;
2021-10-04 10:32:56 +02:00
} ;
type ErrorDetails = {
message : string ;
2022-01-05 01:00:55 +01:00
location? : Location ;
2021-10-04 10:32:56 +02:00
} ;
type TestSummary = {
skipped : number ;
expected : number ;
2022-08-02 21:55:43 +02:00
interrupted : TestCase [ ] ;
2021-10-04 10:32:56 +02:00
unexpected : TestCase [ ] ;
flaky : TestCase [ ] ;
failuresToPrint : TestCase [ ] ;
2022-07-28 23:46:21 +02:00
fatalErrors : TestError [ ] ;
2021-10-04 10:32:56 +02:00
} ;
2021-08-12 01:44:19 +02:00
2023-01-24 02:44:23 +01:00
export class BaseReporter implements Reporter {
2021-06-07 02:09:53 +02:00
duration = 0 ;
2022-04-02 03:32:34 +02:00
config ! : FullConfigInternal ;
2021-06-07 02:09:53 +02:00
suite ! : Suite ;
2022-07-13 22:54:26 +02:00
totalTestCount = 0 ;
2021-06-29 19:55:46 +02:00
result ! : FullResult ;
2022-01-10 22:22:09 +01:00
private fileDurations = new Map < string , number > ( ) ;
private monotonicStartTime : number = 0 ;
private _omitFailures : boolean ;
2021-12-18 06:07:04 +01:00
private readonly _ttyWidthForTest : number ;
2022-07-28 23:46:21 +02:00
private _fatalErrors : TestError [ ] = [ ] ;
2021-10-16 04:18:56 +02:00
constructor ( options : { omitFailures? : boolean } = { } ) {
this . _omitFailures = options . omitFailures || false ;
2021-12-18 06:07:04 +01:00
this . _ttyWidthForTest = parseInt ( process . env . PWTEST_TTY_WIDTH || '' , 10 ) ;
2021-10-16 04:18:56 +02:00
}
2021-06-07 02:09:53 +02:00
2023-01-26 00:38:23 +01:00
onBegin ( config : FullConfig , suite : Suite ) {
2021-06-07 02:09:53 +02:00
this . monotonicStartTime = monotonicTime ( ) ;
2022-04-02 03:32:34 +02:00
this . config = config as FullConfigInternal ;
2021-06-07 02:09:53 +02:00
this . suite = suite ;
2022-07-13 22:54:26 +02:00
this . totalTestCount = suite . allTests ( ) . length ;
2021-06-07 02:09:53 +02:00
}
2021-08-12 01:44:19 +02:00
onStdOut ( chunk : string | Buffer , test? : TestCase , result? : TestResult ) {
this . _appendOutput ( { chunk , type : 'stdout' } , result ) ;
}
onStdErr ( chunk : string | Buffer , test? : TestCase , result? : TestResult ) {
this . _appendOutput ( { chunk , type : 'stderr' } , result ) ;
2021-06-07 02:09:53 +02:00
}
2021-08-12 01:44:19 +02:00
private _appendOutput ( output : TestResultOutput , result : TestResult | undefined ) {
if ( ! result )
return ;
( result as any ) [ kOutputSymbol ] = ( result as any ) [ kOutputSymbol ] || [ ] ;
( result as any ) [ kOutputSymbol ] . push ( output ) ;
2021-06-07 02:09:53 +02:00
}
2021-07-19 23:54:18 +02:00
onTestEnd ( test : TestCase , result : TestResult ) {
2022-02-08 05:10:13 +01:00
// Ignore any tests that are run in parallel.
for ( let suite : Suite | undefined = test . parent ; suite ; suite = suite . parent ) {
if ( ( suite as any ) . _parallelMode === 'parallel' )
return ;
}
2021-07-17 00:23:50 +02:00
const projectName = test . titlePath ( ) [ 1 ] ;
2021-07-16 07:02:10 +02:00
const relativePath = relativeTestPath ( this . config , test ) ;
2021-07-17 00:23:50 +02:00
const fileAndProject = ( projectName ? ` [ ${ projectName } ] › ` : '' ) + relativePath ;
2021-06-15 07:45:58 +02:00
const duration = this . fileDurations . get ( fileAndProject ) || 0 ;
this . fileDurations . set ( fileAndProject , duration + result . duration ) ;
2021-06-07 02:09:53 +02:00
}
onError ( error : TestError ) {
2023-01-09 18:33:09 +01:00
this . _fatalErrors . push ( error ) ;
2021-06-07 02:09:53 +02:00
}
2021-06-29 19:55:46 +02:00
async onEnd ( result : FullResult ) {
2021-06-07 02:09:53 +02:00
this . duration = monotonicTime ( ) - this . monotonicStartTime ;
2021-06-29 19:55:46 +02:00
this . result = result ;
2021-06-07 02:09:53 +02:00
}
2022-09-15 00:25:24 +02:00
protected ttyWidth() {
return this . _ttyWidthForTest || process . stdout . columns || 0 ;
}
2022-06-23 02:03:54 +02:00
protected fitToScreen ( line : string , prefix? : string ) : string {
2022-09-15 00:25:24 +02:00
const ttyWidth = this . ttyWidth ( ) ;
2022-02-11 17:33:56 +01:00
if ( ! ttyWidth ) {
// Guard against the case where we cannot determine available width.
return line ;
}
2022-06-23 02:03:54 +02:00
return fitToWidth ( line , ttyWidth , prefix ) ;
2021-12-18 06:07:04 +01:00
}
2021-11-03 19:17:23 +01:00
protected generateStartingMessage() {
2023-02-02 00:25:26 +01:00
const jobs = Math . min ( this . config . workers , this . config . _internal . maxConcurrentTestGroups ) ;
2021-11-03 19:17:23 +01:00
const shardDetails = this . config . shard ? ` , shard ${ this . config . shard . current } of ${ this . config . shard . total } ` : '' ;
2022-09-29 19:20:21 +02:00
return ` \ nRunning ${ this . totalTestCount } test ${ this . totalTestCount !== 1 ? 's' : '' } using ${ jobs } worker ${ jobs !== 1 ? 's' : '' } ${ shardDetails } ` ;
2021-11-03 19:17:23 +01:00
}
2021-10-04 10:32:56 +02:00
protected getSlowTests ( ) : [ string , number ] [ ] {
2021-06-15 07:45:58 +02:00
if ( ! this . config . reportSlowTests )
2021-10-04 10:32:56 +02:00
return [ ] ;
2021-06-07 02:09:53 +02:00
const fileDurations = [ . . . this . fileDurations . entries ( ) ] ;
fileDurations . sort ( ( a , b ) = > b [ 1 ] - a [ 1 ] ) ;
2021-06-15 07:45:58 +02:00
const count = Math . min ( fileDurations . length , this . config . reportSlowTests . max || Number . POSITIVE_INFINITY ) ;
2021-10-04 10:32:56 +02:00
const threshold = this . config . reportSlowTests . threshold ;
2022-08-18 20:12:33 +02:00
return fileDurations . filter ( ( [ , duration ] ) = > duration > threshold ) . slice ( 0 , count ) ;
2021-10-04 10:32:56 +02:00
}
2022-08-02 21:55:43 +02:00
protected generateSummaryMessage ( { skipped , expected , interrupted , unexpected , flaky , fatalErrors } : TestSummary ) {
2021-10-04 10:32:56 +02:00
const tokens : string [ ] = [ ] ;
if ( unexpected . length ) {
tokens . push ( colors . red ( ` ${ unexpected . length } failed ` ) ) ;
for ( const test of unexpected )
tokens . push ( colors . red ( formatTestHeader ( this . config , test , ' ' ) ) ) ;
}
2022-08-02 21:55:43 +02:00
if ( interrupted . length ) {
2022-08-10 06:17:30 +02:00
tokens . push ( colors . yellow ( ` ${ interrupted . length } interrupted ` ) ) ;
2022-08-02 21:55:43 +02:00
for ( const test of interrupted )
2022-08-10 06:17:30 +02:00
tokens . push ( colors . yellow ( formatTestHeader ( this . config , test , ' ' ) ) ) ;
2022-08-02 21:55:43 +02:00
}
2021-10-04 10:32:56 +02:00
if ( flaky . length ) {
tokens . push ( colors . yellow ( ` ${ flaky . length } flaky ` ) ) ;
for ( const test of flaky )
tokens . push ( colors . yellow ( formatTestHeader ( this . config , test , ' ' ) ) ) ;
2021-06-07 02:09:53 +02:00
}
2021-10-04 10:32:56 +02:00
if ( skipped )
tokens . push ( colors . yellow ( ` ${ skipped } skipped ` ) ) ;
if ( expected )
tokens . push ( colors . green ( ` ${ expected } passed ` ) + colors . dim ( ` ( ${ milliseconds ( this . duration ) } ) ` ) ) ;
if ( this . result . status === 'timedout' )
tokens . push ( colors . red ( ` Timed out waiting ${ this . config . globalTimeout / 1000 } s for the entire test run ` ) ) ;
2022-12-21 18:36:59 +01:00
if ( fatalErrors . length && expected + unexpected . length + interrupted . length + flaky . length > 0 )
2022-08-06 06:21:43 +02:00
tokens . push ( colors . red ( ` ${ fatalErrors . length === 1 ? '1 error was not a part of any test' : fatalErrors . length + ' errors were not a part of any test' } , see above for details ` ) ) ;
2021-10-04 10:32:56 +02:00
return tokens . join ( '\n' ) ;
2021-06-07 02:09:53 +02:00
}
2021-10-04 10:32:56 +02:00
protected generateSummary ( ) : TestSummary {
2021-06-07 02:09:53 +02:00
let skipped = 0 ;
let expected = 0 ;
2022-08-02 21:55:43 +02:00
const interrupted : TestCase [ ] = [ ] ;
const interruptedToPrint : TestCase [ ] = [ ] ;
2021-07-19 23:54:18 +02:00
const unexpected : TestCase [ ] = [ ] ;
const flaky : TestCase [ ] = [ ] ;
2021-06-07 02:09:53 +02:00
2021-07-16 21:40:33 +02:00
this . suite . allTests ( ) . forEach ( test = > {
2021-07-19 02:40:59 +02:00
switch ( test . outcome ( ) ) {
2021-07-29 00:43:37 +02:00
case 'skipped' : {
2022-08-02 21:55:43 +02:00
if ( test . results . some ( result = > result . status === 'interrupted' ) ) {
if ( test . results . some ( result = > ! ! result . error ) )
interruptedToPrint . push ( test ) ;
interrupted . push ( test ) ;
} else {
++ skipped ;
}
2021-07-29 00:43:37 +02:00
break ;
}
2021-06-07 02:09:53 +02:00
case 'expected' : ++ expected ; break ;
case 'unexpected' : unexpected . push ( test ) ; break ;
case 'flaky' : flaky . push ( test ) ; break ;
}
} ) ;
2022-08-02 21:55:43 +02:00
const failuresToPrint = [ . . . unexpected , . . . flaky , . . . interruptedToPrint ] ;
2021-10-04 10:32:56 +02:00
return {
skipped ,
expected ,
2022-08-02 21:55:43 +02:00
interrupted ,
2021-10-04 10:32:56 +02:00
unexpected ,
flaky ,
2022-07-28 23:46:21 +02:00
failuresToPrint ,
fatalErrors : this._fatalErrors ,
2021-10-04 10:32:56 +02:00
} ;
}
2021-06-07 02:09:53 +02:00
2021-10-04 10:32:56 +02:00
epilogue ( full : boolean ) {
const summary = this . generateSummary ( ) ;
const summaryMessage = this . generateSummaryMessage ( summary ) ;
2021-10-16 04:18:56 +02:00
if ( full && summary . failuresToPrint . length && ! this . _omitFailures )
2021-10-04 10:32:56 +02:00
this . _printFailures ( summary . failuresToPrint ) ;
2021-06-07 02:09:53 +02:00
this . _printSlowTests ( ) ;
2021-10-04 10:32:56 +02:00
this . _printSummary ( summaryMessage ) ;
2021-06-07 02:09:53 +02:00
}
2021-07-19 23:54:18 +02:00
private _printFailures ( failures : TestCase [ ] ) {
2021-10-04 10:32:56 +02:00
console . log ( '' ) ;
2021-06-07 02:09:53 +02:00
failures . forEach ( ( test , index ) = > {
2021-10-04 10:32:56 +02:00
console . log ( formatFailure ( this . config , test , {
index : index + 1 ,
} ) . message ) ;
} ) ;
}
private _printSlowTests() {
2021-11-08 21:54:18 +01:00
const slowTests = this . getSlowTests ( ) ;
slowTests . forEach ( ( [ file , duration ] ) = > {
console . log ( colors . yellow ( ' Slow test file: ' ) + file + colors . yellow ( ` ( ${ milliseconds ( duration ) } ) ` ) ) ;
2021-06-07 02:09:53 +02:00
} ) ;
2021-11-08 21:54:18 +01:00
if ( slowTests . length )
console . log ( colors . yellow ( ' Consider splitting slow test files to speed up parallel execution' ) ) ;
2021-06-07 02:09:53 +02:00
}
2021-10-16 04:18:56 +02:00
private _printSummary ( summary : string ) {
2023-02-07 18:48:46 +01:00
if ( summary . trim ( ) )
2021-10-16 04:18:56 +02:00
console . log ( summary ) ;
2021-10-04 10:32:56 +02:00
}
2021-08-25 21:19:50 +02:00
willRetry ( test : TestCase ) : boolean {
return test . outcome ( ) === 'unexpected' && test . results . length <= test . retries ;
2021-06-07 02:09:53 +02:00
}
}
2022-01-05 01:00:55 +01:00
export function formatFailure ( config : FullConfig , test : TestCase , options : { index? : number , includeStdio? : boolean , includeAttachments? : boolean } = { } ) : {
2021-10-04 10:32:56 +02:00
message : string ,
annotations : Annotation [ ]
} {
2022-01-05 01:00:55 +01:00
const { index , includeStdio , includeAttachments = true } = options ;
2021-09-14 03:07:40 +02:00
const lines : string [ ] = [ ] ;
2021-10-04 10:32:56 +02:00
const title = formatTestTitle ( config , test ) ;
const annotations : Annotation [ ] = [ ] ;
const header = formatTestHeader ( config , test , ' ' , index ) ;
lines . push ( colors . red ( header ) ) ;
2021-06-07 02:09:53 +02:00
for ( const result of test . results ) {
2021-10-04 10:32:56 +02:00
const resultLines : string [ ] = [ ] ;
2022-02-03 03:33:51 +01:00
const errors = formatResultFailure ( config , test , result , ' ' , colors . enabled ) ;
if ( ! errors . length )
2021-06-07 02:09:53 +02:00
continue ;
2022-02-03 03:33:51 +01:00
const retryLines = [ ] ;
2021-07-29 00:43:37 +02:00
if ( result . retry ) {
2022-02-03 03:33:51 +01:00
retryLines . push ( '' ) ;
retryLines . push ( colors . gray ( pad ( ` Retry # ${ result . retry } ` , '-' ) ) ) ;
2021-09-14 03:07:40 +02:00
}
2022-02-03 03:33:51 +01:00
resultLines . push ( . . . retryLines ) ;
resultLines . push ( . . . errors . map ( error = > '\n' + error . message ) ) ;
2021-10-04 10:32:56 +02:00
if ( includeAttachments ) {
for ( let i = 0 ; i < result . attachments . length ; ++ i ) {
const attachment = result . attachments [ i ] ;
2022-03-01 18:11:53 +01:00
const hasPrintableContent = attachment . contentType . startsWith ( 'text/' ) && attachment . body ;
if ( ! attachment . path && ! hasPrintableContent )
continue ;
2021-10-04 10:32:56 +02:00
resultLines . push ( '' ) ;
resultLines . push ( colors . cyan ( pad ( ` attachment # ${ i + 1 } : ${ attachment . name } ( ${ attachment . contentType } ) ` , '-' ) ) ) ;
if ( attachment . path ) {
const relativePath = path . relative ( process . cwd ( ) , attachment . path ) ;
resultLines . push ( colors . cyan ( ` ${ relativePath } ` ) ) ;
// Make this extensible
if ( attachment . name === 'trace' ) {
resultLines . push ( colors . cyan ( ` Usage: ` ) ) ;
resultLines . push ( '' ) ;
resultLines . push ( colors . cyan ( ` npx playwright show-trace ${ relativePath } ` ) ) ;
resultLines . push ( '' ) ;
}
} else {
2022-02-10 21:33:38 +01:00
if ( attachment . contentType . startsWith ( 'text/' ) && attachment . body ) {
let text = attachment . body . toString ( ) ;
2021-10-04 10:32:56 +02:00
if ( text . length > 300 )
text = text . slice ( 0 , 300 ) + '...' ;
resultLines . push ( colors . cyan ( ` ${ text } ` ) ) ;
}
2021-09-14 03:07:40 +02:00
}
2021-10-04 10:32:56 +02:00
resultLines . push ( colors . cyan ( pad ( ' ' , '-' ) ) ) ;
2021-09-14 03:07:40 +02:00
}
2021-07-29 00:43:37 +02:00
}
2021-08-12 01:44:19 +02:00
const output = ( ( result as any ) [ kOutputSymbol ] || [ ] ) as TestResultOutput [ ] ;
2021-10-04 10:32:56 +02:00
if ( includeStdio && output . length ) {
2021-08-12 01:44:19 +02:00
const outputText = output . map ( ( { chunk , type } ) = > {
const text = chunk . toString ( 'utf8' ) ;
if ( type === 'stderr' )
return colors . red ( stripAnsiEscapes ( text ) ) ;
return text ;
} ) . join ( '' ) ;
2021-10-04 10:32:56 +02:00
resultLines . push ( '' ) ;
resultLines . push ( colors . gray ( pad ( '--- Test output' , '-' ) ) + '\n\n' + outputText + '\n' + pad ( '' , '-' ) ) ;
}
2022-02-03 03:33:51 +01:00
for ( const error of errors ) {
annotations . push ( {
location : error.location ,
title ,
message : [ header , . . . retryLines , error . message ] . join ( '\n' ) ,
} ) ;
}
2021-10-04 10:32:56 +02:00
lines . push ( . . . resultLines ) ;
2021-06-07 02:09:53 +02:00
}
2021-09-14 03:07:40 +02:00
lines . push ( '' ) ;
2021-10-04 10:32:56 +02:00
return {
message : lines.join ( '\n' ) ,
annotations
} ;
2021-06-07 02:09:53 +02:00
}
2022-02-03 03:33:51 +01:00
export function formatResultFailure ( config : FullConfig , test : TestCase , result : TestResult , initialIndent : string , highlightCode : boolean ) : ErrorDetails [ ] {
const errorDetails : ErrorDetails [ ] = [ ] ;
2022-02-23 21:32:12 +01:00
if ( result . status === 'passed' && test . expectedStatus === 'failed' ) {
2022-02-03 03:33:51 +01:00
errorDetails . push ( {
message : indent ( colors . red ( ` Expected to fail, but passed. ` ) , initialIndent ) ,
} ) ;
2021-08-25 21:19:50 +02:00
}
2022-08-02 21:55:43 +02:00
if ( result . status === 'interrupted' ) {
errorDetails . push ( {
message : indent ( colors . red ( ` Test was interrupted. ` ) , initialIndent ) ,
} ) ;
}
2022-02-03 03:33:51 +01:00
for ( const error of result . errors ) {
const formattedError = formatError ( config , error , highlightCode , test . location . file ) ;
errorDetails . push ( {
message : indent ( formattedError . message , initialIndent ) ,
location : formattedError.location ,
} ) ;
2021-10-04 10:32:56 +02:00
}
2022-02-03 03:33:51 +01:00
return errorDetails ;
2021-08-05 22:36:47 +02:00
}
2022-11-18 20:35:29 +01:00
export function relativeFilePath ( config : FullConfig , file : string ) : string {
2022-01-05 01:00:55 +01:00
return path . relative ( config . rootDir , file ) || path . basename ( file ) ;
}
2021-07-19 23:54:18 +02:00
function relativeTestPath ( config : FullConfig , test : TestCase ) : string {
2022-01-05 01:00:55 +01:00
return relativeFilePath ( config , test . location . file ) ;
2021-06-15 07:45:58 +02:00
}
2022-10-25 00:54:53 +02:00
export function stepSuffix ( step : TestStep | undefined ) {
2021-08-18 01:41:36 +02:00
const stepTitles = step ? step . titlePath ( ) : [ ] ;
return stepTitles . map ( t = > ' › ' + t ) . join ( '' ) ;
}
2022-08-20 01:42:21 +02:00
export function formatTestTitle ( config : FullConfig , test : TestCase , step? : TestStep , omitLocation : boolean = false ) : string {
2021-07-17 00:23:50 +02:00
// root, project, file, ...describes, test
const [ , projectName , , . . . titles ] = test . titlePath ( ) ;
2022-08-20 01:42:21 +02:00
let location ;
if ( omitLocation )
location = ` ${ relativeTestPath ( config , test ) } ` ;
else
2022-10-25 00:54:53 +02:00
location = ` ${ relativeTestPath ( config , test ) } : ${ step ? . location ? . line ? ? test . location . line } : ${ step ? . location ? . column ? ? test . location . column } ` ;
2021-07-17 00:23:50 +02:00
const projectTitle = projectName ? ` [ ${ projectName } ] › ` : '' ;
2021-10-18 05:58:06 +02:00
return ` ${ projectTitle } ${ location } › ${ titles . join ( ' › ' ) } ${ stepSuffix ( step ) } ` ;
2021-06-07 02:09:53 +02:00
}
2021-07-19 23:54:18 +02:00
function formatTestHeader ( config : FullConfig , test : TestCase , indent : string , index? : number ) : string {
2021-06-07 02:09:53 +02:00
const title = formatTestTitle ( config , test ) ;
2021-08-25 21:19:50 +02:00
const header = ` ${ indent } ${ index ? index + ') ' : '' } ${ title } ` ;
2021-09-16 01:28:57 +02:00
return pad ( header , '=' ) ;
2021-06-07 02:09:53 +02:00
}
2022-01-05 01:00:55 +01:00
export function formatError ( config : FullConfig , error : TestError , highlightCode : boolean , file? : string ) : ErrorDetails {
2022-12-21 18:36:59 +01:00
const message = error . message || error . value || '' ;
2021-06-07 02:09:53 +02:00
const stack = error . stack ;
2022-12-21 18:36:59 +01:00
if ( ! stack && ! error . location )
return { message } ;
2022-02-03 03:33:51 +01:00
const tokens = [ ] ;
2022-12-21 18:36:59 +01:00
// Now that we filter out internals from our stack traces, we can safely render
// the helper / original exception locations.
const parsedStack = stack ? prepareErrorStack ( stack ) : undefined ;
tokens . push ( parsedStack ? . message || message ) ;
let location = error . location ;
if ( parsedStack && ! location )
location = parsedStack . location ;
if ( location ) {
try {
const source = fs . readFileSync ( location . file , 'utf8' ) ;
const codeFrame = codeFrameColumns ( source , { start : location } , { highlightCode } ) ;
// Convert /var/folders to /private/var/folders on Mac.
if ( ! file || fs . realpathSync ( file ) !== location . file ) {
2022-01-05 01:00:55 +01:00
tokens . push ( '' ) ;
2022-12-21 18:36:59 +01:00
tokens . push ( colors . gray ( ` at ` ) + ` ${ relativeFilePath ( config , location . file ) } : ${ location . line } ` ) ;
2022-01-05 01:00:55 +01:00
}
2022-12-21 18:36:59 +01:00
tokens . push ( '' ) ;
tokens . push ( codeFrame ) ;
} catch ( e ) {
// Failed to read the source file - that's ok.
2021-06-07 02:09:53 +02:00
}
2022-12-21 18:36:59 +01:00
}
if ( parsedStack ) {
2021-06-07 02:09:53 +02:00
tokens . push ( '' ) ;
2022-12-21 18:36:59 +01:00
tokens . push ( colors . dim ( parsedStack . stackLines . join ( '\n' ) ) ) ;
2021-06-07 02:09:53 +02:00
}
2022-12-21 18:36:59 +01:00
2021-10-04 10:32:56 +02:00
return {
2022-01-05 01:00:55 +01:00
location ,
2021-10-04 10:32:56 +02:00
message : tokens.join ( '\n' ) ,
} ;
2021-06-07 02:09:53 +02:00
}
function pad ( line : string , char : string ) : string {
2021-08-12 01:44:19 +02:00
if ( line )
line += ' ' ;
return line + colors . gray ( char . repeat ( Math . max ( 0 , 100 - line . length ) ) ) ;
2021-06-07 02:09:53 +02:00
}
function indent ( lines : string , tab : string ) {
return lines . replace ( /^(?=.+$)/gm , tab ) ;
}
2022-08-15 18:28:55 +02:00
export function prepareErrorStack ( stack : string ) : {
2021-09-30 23:18:36 +02:00
message : string ;
stackLines : string [ ] ;
2022-01-05 01:00:55 +01:00
location? : Location ;
2021-09-30 23:18:36 +02:00
} {
const lines = stack . split ( '\n' ) ;
let firstStackLine = lines . findIndex ( line = > line . startsWith ( ' at ' ) ) ;
2022-01-05 01:00:55 +01:00
if ( firstStackLine === - 1 )
firstStackLine = lines . length ;
2021-09-30 23:18:36 +02:00
const message = lines . slice ( 0 , firstStackLine ) . join ( '\n' ) ;
const stackLines = lines . slice ( firstStackLine ) ;
2022-01-05 01:00:55 +01:00
let location : Location | undefined ;
2021-09-16 06:28:36 +02:00
for ( const line of stackLines ) {
2022-06-30 21:17:08 +02:00
const { frame : parsed , fileName : resolvedFile } = parseStackTraceLine ( line ) ;
if ( ! parsed || ! resolvedFile )
2022-01-07 20:06:47 +01:00
continue ;
2022-10-27 00:18:31 +02:00
if ( belongsToNodeModules ( resolvedFile ) )
continue ;
2022-08-15 18:28:55 +02:00
location = { file : resolvedFile , column : parsed.column || 0 , line : parsed.line || 0 } ;
break ;
2021-06-07 02:09:53 +02:00
}
2022-01-05 01:00:55 +01:00
return { message , stackLines , location } ;
2021-06-07 02:09:53 +02:00
}
2022-06-23 02:03:54 +02:00
const ansiRegex = new RegExp ( '([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))' , 'g' ) ;
2021-08-12 01:44:19 +02:00
export function stripAnsiEscapes ( str : string ) : string {
2022-02-01 02:14:59 +01:00
return str . replace ( ansiRegex , '' ) ;
2021-06-07 02:09:53 +02:00
}
2021-12-18 06:07:04 +01:00
2022-06-23 02:03:54 +02:00
// Leaves enough space for the "prefix" to also fit.
function fitToWidth ( line : string , width : number , prefix? : string ) : string {
const prefixLength = prefix ? stripAnsiEscapes ( prefix ) . length : 0 ;
width -= prefixLength ;
2021-12-18 06:07:04 +01:00
if ( line . length <= width )
return line ;
2022-06-23 02:03:54 +02:00
// Even items are plain text, odd items are control sequences.
const parts = line . split ( ansiRegex ) ;
const taken : string [ ] = [ ] ;
for ( let i = parts . length - 1 ; i >= 0 ; i -- ) {
if ( i % 2 ) {
// Include all control sequences to preserve formatting.
taken . push ( parts [ i ] ) ;
} else {
let part = parts [ i ] . substring ( parts [ i ] . length - width ) ;
if ( part . length < parts [ i ] . length && part . length > 0 ) {
// Add ellipsis if we are truncating.
part = '\u2026' + part . substring ( 1 ) ;
}
taken . push ( part ) ;
width -= part . length ;
}
2021-12-18 06:07:04 +01:00
}
2022-06-23 02:03:54 +02:00
return taken . reverse ( ) . join ( '' ) ;
2021-12-18 06:07:04 +01:00
}
2022-10-27 00:18:31 +02:00
function belongsToNodeModules ( file : string ) {
return file . includes ( ` ${ path . sep } node_modules ${ path . sep } ` ) ;
}
2023-02-07 18:48:46 +01:00
export function separator ( ) : string {
const columns = process . stdout ? . columns || 30 ;
return colors . dim ( '⎯' . repeat ( Math . min ( 100 , columns ) ) ) ;
}