2021-07-15 22:02:10 -07: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 .
* /
2025-01-02 17:48:59 +01:00
import type { Reporter , TestCase , TestResult , TestStep } from '../../packages/playwright-test/reporter' ;
2024-04-22 18:29:26 -07:00
import { test , expect } from './playwright-test-fixtures' ;
2021-07-15 22:02:10 -07:00
2021-07-20 15:03:01 -05:00
const smallReporterJS = `
2024-04-22 18:29:26 -07:00
const path = require ( 'path' ) ;
function formatLocation ( location ) {
if ( ! location )
return ' @ <no location>' ;
return ' @ ' + path . basename ( location . file ) + ':' + location . line ;
}
function formatTitle ( test ) {
let titlePath = test . titlePath ( ) ;
if ( ! titlePath [ 0 ] )
[ , . . . titlePath ] = titlePath ;
return titlePath . join ( ' > ' ) ;
}
const asciiRegex = new RegExp ( '[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)|(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TZcf-ntqry=><~]))' , 'g' ) ;
function stripAnsi ( str ) {
return str . replace ( asciiRegex , '' ) ;
}
2021-07-20 15:03:01 -05:00
class Reporter {
2024-04-22 18:29:26 -07:00
constructor ( options ) {
this . options = options ;
}
trimError ( message ) {
if ( this . options . skipErrorMessage )
return '<error message>' ;
const lines = message . split ( '\\n' ) ;
return lines [ 0 ] . trimEnd ( ) ;
}
printErrors ( errors , prefix ) {
for ( const error of errors ) {
const errorLocation = this . options . skipErrorLocation ? '' : formatLocation ( error . location ) ;
console . log ( ( prefix || ' error: ' ) + this . trimError ( error . message ) + errorLocation ) ;
if ( this . options . printErrorSnippet && error . snippet ) {
const lines = [ '======' , . . . error . snippet . split ( '\\n' ) , '======' ] ;
console . log ( lines . map ( line = > stripAnsi ( ' ' + line ) . trimEnd ( ) ) . join ( '\\n' ) ) ;
}
}
}
printsToStdio() {
return true ;
}
2021-07-20 15:03:01 -05:00
onBegin ( config , suite ) {
2024-04-22 18:29:26 -07:00
console . log ( ) ; // for nicer expectations
console . log ( 'onBegin: ' + suite . allTests ( ) . length + ' tests total' ) ;
if ( this . options . onBegin )
console . log ( ' options.onBegin=' + this . options . onBegin ) ;
}
onTestBegin ( test , result ) {
console . log ( 'onTestBegin: ' + formatTitle ( test ) + '; retry #' + result . retry ) ;
2021-07-20 15:03:01 -05:00
}
2024-04-22 18:29:26 -07:00
onTestEnd ( test , result ) {
2024-04-23 10:51:27 -07:00
const status = this . options . printTestStatus ? '[' + result . status + '] ' : '' ;
console . log ( 'onTestEnd: ' + status + formatTitle ( test ) + '; retry #' + result . retry ) ;
2024-04-22 18:29:26 -07:00
this . printErrors ( result . errors ) ;
}
onStepBegin ( test , result , step ) {
}
onStepEnd ( test , result , step ) {
if ( this . options . printSteps ) {
2025-05-23 16:03:50 -07:00
console . log ( 'onStepEnd: [' + step . category + '] ' + step . title ) ;
2024-04-22 18:29:26 -07:00
if ( step . error )
this . printErrors ( [ step . error ] ) ;
}
}
// For easier debugging.
onStdOut ( data ) {
process . stdout . write ( data . toString ( ) ) ;
}
// For easier debugging.
onStdErr ( data ) {
process . stderr . write ( data . toString ( ) ) ;
}
2021-11-12 06:47:41 -08:00
onError ( error ) {
2024-04-22 18:29:26 -07:00
this . printErrors ( [ error ] , 'onError: ' ) ;
2021-11-12 06:47:41 -08:00
}
2024-04-22 18:29:26 -07:00
async onEnd() {
await new Promise ( f = > setTimeout ( f , 500 ) ) ;
console . log ( 'onEnd' ) ;
if ( this . options . onEnd )
console . log ( ' options.onEnd=' + this . options . onEnd ) ;
2021-07-20 15:03:01 -05:00
}
2024-04-22 18:29:26 -07:00
async onExit() {
console . log ( 'onExit' ) ;
2023-04-04 10:50:40 -07:00
}
2021-07-20 15:03:01 -05:00
}
module . exports = Reporter ;
` ;
2023-06-16 21:30:55 -07:00
for ( const useIntermediateMergeReport of [ false , true ] as const ) {
test . describe ( ` ${ useIntermediateMergeReport ? 'merged' : 'created' } ` , ( ) = > {
test . use ( { useIntermediateMergeReport } ) ;
test ( 'should work with custom reporter' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
2024-04-22 18:29:26 -07:00
'reporter.ts' : smallReporterJS ,
2023-06-16 21:30:55 -07:00
'playwright.config.ts' : `
module . exports = {
reporter : [
2024-04-22 18:29:26 -07:00
[ './reporter.ts' , { onBegin : 'begin-data' , onEnd : 'end-data' } ]
2023-06-16 21:30:55 -07:00
] ,
projects : [
{ name : 'foo' , repeatEach : 2 } ,
{ name : 'bar' } ,
] ,
} ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'not run' , async ( { } ) = > {
} ) ;
test . only ( 'is run' , async ( { } ) = > {
} ) ;
`
} , { reporter : '' , workers : 1 } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 3 tests total
options . onBegin = begin - data
onTestBegin : foo > a . test . ts > is run ; retry # 0
onTestEnd : foo > a . test . ts > is run ; retry # 0
onTestBegin : foo > a . test . ts > is run ; retry # 0
onTestEnd : foo > a . test . ts > is run ; retry # 0
onTestBegin : bar > a . test . ts > is run ; retry # 0
onTestEnd : bar > a . test . ts > is run ; retry # 0
onEnd
options . onEnd = end - data
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should work without a file extension' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'pass' , async ( { } ) = > {
} ) ;
`
} , { reporter : '' , workers : 1 } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 1 tests total
onTestBegin : > a . test . ts > pass ; retry # 0
onTestEnd : > a . test . ts > pass ; retry # 0
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should report onEnd after global teardown' , async ( { runInlineTest } ) = > {
test . skip ( useIntermediateMergeReport ) ;
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'globalSetup.ts' : `
module . exports = ( ) = > {
return ( ) = > console . log ( '\\n%%global teardown' ) ;
} ;
` ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
globalSetup : './globalSetup' ,
} ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'pass' , async ( { } ) = > {
} ) ;
`
} , { reporter : '' , workers : 1 } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 1 tests total
onTestBegin : > a . test . ts > pass ; retry # 0
onTestEnd : > a . test . ts > pass ; retry # 0
% % global teardown
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should load reporter from node_modules' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'node_modules/my-reporter/index.js' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : 'my-reporter' ,
} ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'pass' , async ( { } ) = > {
} ) ;
`
} , { reporter : '' , workers : 1 } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 1 tests total
onTestBegin : > a . test . ts > pass ; retry # 0
onTestEnd : > a . test . ts > pass ; retry # 0
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should not have internal error when steps are finished after timeout' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'a.test.ts' : `
import { test as base , expect } from '@playwright/test' ;
const test = base . extend ( {
page : async ( { page } , use ) = > {
await use ( page ) ;
// Timeout in fixture teardown that will resolve on browser.close.
await page . waitForNavigation ( ) ;
} ,
} ) ;
test ( 'pass' , async ( { page } ) = > {
// Timeout in the test.
await page . click ( 'foo' ) ;
} ) ;
`
} , { workers : 1 , timeout : 1000 , reporter : 'dot' , retries : 1 } ) ;
expect ( result . exitCode ) . toBe ( 1 ) ;
expect ( result . failed ) . toBe ( 1 ) ;
expect ( result . output ) . not . toContain ( 'Internal error' ) ;
} ) ;
test ( 'should report forbid-only error to reporter' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test . only ( 'pass' , ( ) = > { } ) ;
`
} , { 'reporter' : '' , 'forbid-only' : true } ) ;
expect ( result . exitCode ) . toBe ( 1 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 0 tests total
onError : Error : item focused with '.only' is not allowed due to the '--forbid-only' CLI flag : "a.test.ts pass" @ a.test.ts :3
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should report no-tests error to reporter' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
`
} , { 'reporter' : '' } ) ;
expect ( result . exitCode ) . toBe ( 1 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 0 tests total
onError : Error : No tests found @ < no location >
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should report require error to reporter' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'a.spec.js' : `
throw new Error ( 'Oh my!' ) ;
` ,
} , { 'reporter' : '' } ) ;
expect ( result . exitCode ) . toBe ( 1 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 0 tests total
onError : Error : Oh my ! @ a.spec.js :2
onError : Error : No tests found @ < no location >
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should report global setup error to reporter' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
globalSetup : './globalSetup' ,
} ;
` ,
'globalSetup.ts' : `
module . exports = ( ) = > {
throw new Error ( 'Oh my!' ) ;
} ;
` ,
'a.spec.js' : `
const { test , expect } = require ( '@playwright/test' ) ;
test ( 'test' , ( ) = > { } ) ;
` ,
} , { 'reporter' : '' } ) ;
expect ( result . exitCode ) . toBe ( 1 ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 0 tests total
onError : Error : Oh my ! @ globalSetup.ts :3
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'should report correct tests/suites when using grep' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'a.spec.js' : `
import { test , expect } from '@playwright/test' ;
test . describe ( '@foo' , ( ) = > {
test ( 'test1' , async ( { } ) = > {
console . log ( '%%test1' ) ;
} ) ;
test ( 'test2' , async ( { } ) = > {
console . log ( '%%test2' ) ;
} ) ;
} ) ;
test ( 'test3' , async ( { } ) = > {
console . log ( '%%test3' ) ;
} ) ;
` ,
} , { 'grep' : '@foo' } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . output ) . toContain ( '%%test1' ) ;
expect ( result . output ) . toContain ( '%%test2' ) ;
expect ( result . output ) . not . toContain ( '%%test3' ) ;
const fileSuite = result . report . suites [ 0 ] ;
expect ( fileSuite . suites ! . length ) . toBe ( 1 ) ;
expect ( fileSuite . suites ! [ 0 ] . specs . length ) . toBe ( 2 ) ;
expect ( fileSuite . specs . length ) . toBe ( 0 ) ;
} ) ;
test ( 'should use sourceMap-based file suite names' , async ( { runInlineTest } ) = > {
test . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/11028' } ) ;
const result = await runInlineTest ( {
'reporter.js' : `
class Reporter {
onBegin ( config , suite ) {
console . log ( suite . suites [ 0 ] . suites [ 0 ] . location . file ) ;
}
}
module . exports = Reporter ;
` ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'a.spec.js' :
2023-02-14 19:20:56 -08:00
` var __create = Object.create;
2022-02-01 13:04:54 -08:00
var __defProp = Object . defineProperty ;
var __getOwnPropDesc = Object . getOwnPropertyDescriptor ;
var __getOwnPropNames = Object . getOwnPropertyNames ;
var __getProtoOf = Object . getPrototypeOf ;
var __hasOwnProp = Object . prototype . hasOwnProperty ;
var __markAsModule = ( target ) = > __defProp ( target , "__esModule" , { value : true } ) ;
var __reExport = ( target , module2 , desc ) = > {
if ( module2 && typeof module2 === "object" || typeof module2 === "function" ) {
for ( let key of __getOwnPropNames ( module2 ) )
if ( ! __hasOwnProp . call ( target , key ) && key !== "default" )
__defProp ( target , key , { get : ( ) = > module2 [ key ] , enumerable : ! ( desc = __getOwnPropDesc ( module2 , key ) ) || desc . enumerable } ) ;
}
return target ;
} ;
var __toModule = ( module2 ) = > {
return __reExport ( __markAsModule ( __defProp ( module2 != null ? __create ( __getProtoOf ( module2 ) ) : { } , "default" , module2 && module2 . __esModule && "default" in module2 ? { get : ( ) = > module2 . default , enumerable : true } : { value : module2 , enumerable : true } ) ) , module2 ) ;
} ;
var import_test = __toModule ( require ( "@playwright/test" ) ) ;
( 0 , import_test . test ) ( "pass" , async ( ) = > {
} ) ;
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vc3JjL2Euc3BlYy50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiaW1wb3J0IHsgdGVzdCB9IGZyb20gXCJAcGxheXdyaWdodC90ZXN0XCI7XG5cbnRlc3QoJ3Bhc3MnLCBhc3luYyAoKSA9PiB7fSk7Il0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsa0JBQXFCO0FBRXJCLHNCQUFLLFFBQVEsWUFBWTtBQUFBOyIsCiAgIm5hbWVzIjogW10KfQo=`,
2023-06-16 21:30:55 -07:00
} , { 'reporter' : '' } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . output ) . toContain ( 'a.spec.ts' ) ;
} ) ;
test ( 'parallelIndex is presented in onTestEnd' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : `
class Reporter {
onTestEnd ( test , result ) {
console . log ( 'parallelIndex: ' + result . parallelIndex )
}
}
module . exports = Reporter ; ` ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'a.spec.js' : `
const { test , expect } = require ( '@playwright/test' ) ;
test ( 'test' , ( ) = > { } ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
expect ( result . output ) . toContain ( 'parallelIndex: 0' ) ;
} ) ;
test ( 'test and step error should have code snippet' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
2024-04-22 18:29:26 -07:00
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : ` module.exports = { reporter: [['./reporter', { printSteps: true, printErrorSnippet: true }]] }; ` ,
2023-06-16 21:30:55 -07:00
'a.spec.js' : `
const { test , expect } = require ( '@playwright/test' ) ;
test ( 'test' , async ( ) = > {
await test . step ( 'step' , async ( ) = > {
expect ( 1 ) . toBe ( 2 ) ;
} ) ;
} ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 1 tests total
onTestBegin : > a . spec . js > test ; retry # 0
2025-05-23 16:03:50 -07:00
onStepEnd : [ hook ] Before Hooks
onStepEnd : [ expect ] toBe
2024-04-22 18:29:26 -07:00
error : Error : expect ( received ) . toBe ( expected ) // Object.is equality @ a.spec.js:5
=== ===
3 | test ( 'test' , async ( ) = > {
4 | await test . step ( 'step' , async ( ) = > {
> 5 | expect ( 1 ) . toBe ( 2 ) ;
| ^
6 | } ) ;
7 | } ) ;
8 |
=== ===
2025-05-23 16:03:50 -07:00
onStepEnd : [ test . step ] step
2024-04-22 18:29:26 -07:00
error : Error : expect ( received ) . toBe ( expected ) // Object.is equality @ a.spec.js:5
=== ===
3 | test ( 'test' , async ( ) = > {
4 | await test . step ( 'step' , async ( ) = > {
> 5 | expect ( 1 ) . toBe ( 2 ) ;
| ^
6 | } ) ;
7 | } ) ;
8 |
=== ===
2025-05-23 16:03:50 -07:00
onStepEnd : [ hook ] After Hooks
onStepEnd : [ hook ] Worker Cleanup
2024-04-22 18:29:26 -07:00
onTestEnd : > a . spec . js > test ; retry # 0
error : Error : expect ( received ) . toBe ( expected ) // Object.is equality @ a.spec.js:5
=== ===
3 | test ( 'test' , async ( ) = > {
4 | await test . step ( 'step' , async ( ) = > {
> 5 | expect ( 1 ) . toBe ( 2 ) ;
| ^
6 | } ) ;
7 | } ) ;
8 |
=== ===
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
test ( 'onError should have code snippet' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
2024-04-22 18:29:26 -07:00
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : ` module.exports = { reporter: [['./reporter', { printSteps: true, printErrorSnippet: true }]] }; ` ,
2023-06-16 21:30:55 -07:00
'a.spec.js' : `
const { test , expect } = require ( '@playwright/test' ) ;
throw new Error ( 'test' ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
2024-04-22 18:29:26 -07:00
expect ( result . output ) . toBe ( `
onBegin : 0 tests total
onError : Error : test @ a.spec.js :3
=== ===
at a.spec.js :3
1 |
2 | const { test , expect } = require ( '@playwright/test' ) ;
> 3 | throw new Error ( 'test' ) ;
| ^
4 |
=== ===
onError : Error : No tests found @ < no location >
onEnd
onExit
` );
2023-06-16 21:30:55 -07:00
} ) ;
} ) ;
2024-01-16 13:32:47 -08:00
}
test ( 'should report a stable test.id' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : `
class Reporter {
onTestBegin ( test ) {
console . log ( '\\n%%testbegin-' + test . id ) ;
}
}
export default Reporter ;
` ,
'playwright.config.ts' : `
module . exports = { reporter : [ [ './reporter.ts' ] ] } ;
` ,
'a.test.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'example test' , async ( { } ) = > {
} ) ;
`
} , { reporter : '' , workers : 1 } ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . outputLines ) . toEqual ( [
'testbegin-20289bcdad95a5e18c38-8b63c3695b9c8bd62d98' ,
] ) ;
} ) ;
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
test ( 'should report annotations from test declaration' , async ( { runInlineTest } ) = > {
const result = await runInlineTest ( {
'reporter.ts' : `
export default class Reporter {
onBegin ( config , suite ) {
const visit = suite = > {
for ( const test of suite . tests || [ ] ) {
const annotations = test . annotations . map ( a = > {
2025-04-15 11:17:21 -07:00
return a . description ? a . type + '=' + a.description : a.type ;
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
} ) ;
console . log ( '\\n%%title=' + test . title + ', annotations=' + annotations . join ( ',' ) ) ;
}
for ( const child of suite . suites || [ ] )
visit ( child ) ;
} ;
visit ( suite ) ;
}
onError ( error ) {
console . log ( error ) ;
}
}
` ,
'playwright.config.ts' : `
module . exports = {
reporter : './reporter' ,
} ;
` ,
'stdio.spec.js' : `
import { test , expect } from '@playwright/test' ;
test ( 'none' , ( ) = > {
expect ( test . info ( ) . annotations ) . toEqual ( [ ] ) ;
} ) ;
test ( 'foo' , { annotation : { type : 'foo' } } , ( ) = > {
2025-04-15 11:17:21 -07:00
expect ( test . info ( ) . annotations ) . toEqual ( [ { type : 'foo' } ] ) ;
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
} ) ;
test ( 'foo-bar' , {
annotation : [
{ type : 'foo' , description : 'desc' } ,
{ type : 'bar' } ,
] ,
} , ( ) = > {
expect ( test . info ( ) . annotations ) . toEqual ( [
2025-04-15 11:17:21 -07:00
{ type : 'foo' , description : 'desc' } ,
{ type : 'bar' } ,
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
] ) ;
} ) ;
test . skip ( 'skip-foo' , { annotation : { type : 'foo' } } , ( ) = > {
} ) ;
test . fixme ( 'fixme-bar' , { annotation : { type : 'bar' } } , ( ) = > {
} ) ;
test . fail ( 'fail-foo-bar' , {
annotation : [
{ type : 'foo' } ,
{ type : 'bar' , description : 'desc' } ,
] ,
} , ( ) = > {
expect ( 1 ) . toBe ( 2 ) ;
} ) ;
test . describe ( 'suite' , { annotation : { type : 'foo' } } , ( ) = > {
test ( 'foo-suite' , ( ) = > {
2025-04-15 11:17:21 -07:00
expect ( test . info ( ) . annotations ) . toEqual ( [ { type : 'foo' } ] ) ;
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
} ) ;
test . describe ( 'inner' , { annotation : { type : 'bar' } } , ( ) = > {
test ( 'foo-bar-suite' , ( ) = > {
2025-04-15 11:17:21 -07:00
expect ( test . info ( ) . annotations ) . toEqual ( [ { type : 'foo' } , { type : 'bar' } ] ) ;
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
} ) ;
} ) ;
} ) ;
test . describe . skip ( 'skip-foo-suite' , { annotation : { type : 'foo' } } , ( ) = > {
test ( 'skip-foo-suite' , ( ) = > {
} ) ;
} ) ;
test . describe . fixme ( 'fixme-bar-suite' , { annotation : { type : 'bar' } } , ( ) = > {
test ( 'fixme-bar-suite' , ( ) = > {
} ) ;
} ) ;
`
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . outputLines ) . toEqual ( [
` title=none, annotations= ` ,
2025-04-15 11:17:21 -07:00
` title=foo, annotations=foo ` ,
` title=foo-bar, annotations=foo=desc,bar ` ,
` title=skip-foo, annotations=foo,skip ` ,
` title=fixme-bar, annotations=bar,fixme ` ,
` title=fail-foo-bar, annotations=foo,bar=desc,fail ` ,
` title=foo-suite, annotations=foo ` ,
` title=foo-bar-suite, annotations=foo,bar ` ,
` title=skip-foo-suite, annotations=foo,skip ` ,
` title=fixme-bar-suite, annotations=bar,fixme ` ,
feat(test runner): tags/annotations (#29248)
API changes:
- `test(title, details, body)` where details contain `tag` and
`annotation`.
- similar `details` property added to `test.skip`, `test.fail`,
`test.fixme`, `test.only`, `test.describe` and other `test.describe.*`
variations.
- `TestProject.tagFilter`/`TestConfig.tagFilter` that supports logical
tag expressions with `(`, `)`, `and`, `or` and `not`.
- `--tag` CLI option to filter by tags.
- New annotations are available in `TestInfo.annotations` and
`TestCase.annotations`.
- New tags are available in `TestCase.tags`.
Reporter changes:
- `json` reporter includes new tags in addition to old `@smoke`-style
tags. **Breaking**: tags are now listed with the leading `@` symbol.
- `html` reporter filters by old and new tags with the same `@smoke`
token.
Fixes #29229, fixes #23180.
2024-02-07 16:31:25 -08:00
] ) ;
} ) ;
2024-04-23 10:51:27 -07:00
test ( 'tests skipped in serial mode receive onTestBegin/onTestEnd' , async ( { runInlineTest } ) = > {
test . info ( ) . annotations . push ( { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/28321' } ) ;
const result = await runInlineTest ( {
'reporter.ts' : smallReporterJS ,
'playwright.config.ts' : ` module.exports = { reporter: [['./reporter', { printTestStatus: true }]] }; ` ,
'a.spec.ts' : `
import { test , expect } from '@playwright/test' ;
test . describe . configure ( { mode : 'serial' , retries : 1 } ) ;
test ( 'test1' , ( ) = > {
expect ( test . info ( ) . retry ) . toBe ( 1 ) ;
} ) ;
test ( 'test2' , ( ) = > {
} ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
expect ( result . output ) . toBe ( `
onBegin : 2 tests total
onTestBegin : > a . spec . ts > test1 ; retry # 0
onTestEnd : [ failed ] > a . spec . ts > test1 ; retry # 0
error : Error : expect ( received ) . toBe ( expected ) // Object.is equality @ a.spec.ts:7
onTestBegin : > a . spec . ts > test2 ; retry # 0
onTestEnd : [ skipped ] > a . spec . ts > test2 ; retry # 0
onTestBegin : > a . spec . ts > test1 ; retry # 1
onTestEnd : [ passed ] > a . spec . ts > test1 ; retry # 1
onTestBegin : > a . spec . ts > test2 ; retry # 1
onTestEnd : [ passed ] > a . spec . ts > test2 ; retry # 1
onEnd
onExit
` );
} ) ;
2025-01-02 17:48:59 +01:00
test ( 'step attachments are referentially equal to result attachments' , async ( { runInlineTest } ) = > {
class TestReporter implements Reporter {
onStepEnd ( test : TestCase , result : TestResult , step : TestStep ) {
console . log ( '%%%' , JSON . stringify ( {
title : step.title ,
attachments : step.attachments.map ( a = > result . attachments . indexOf ( a ) ) ,
} ) ) ;
}
}
const result = await runInlineTest ( {
'reporter.ts' : ` module.exports = ${ TestReporter . toString ( ) } ` ,
'playwright.config.ts' : ` module.exports = { reporter: './reporter' }; ` ,
'a.spec.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'test' , async ( { } , testInfo ) = > {
await test . step ( 'step' , async ( ) = > {
testInfo . attachments . push ( { name : 'attachment' , body : Buffer.from ( 'content' ) } ) ;
} ) ;
} ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
const steps = result . outputLines . map ( line = > JSON . parse ( line ) ) ;
expect ( steps ) . toEqual ( [
{ title : 'Before Hooks' , attachments : [ ] } ,
{ title : 'step' , attachments : [ 0 ] } ,
{ title : 'After Hooks' , attachments : [ ] } ,
] ) ;
} ) ;
2025-02-03 13:54:28 +01:00
2025-02-05 12:27:44 -08:00
test ( 'step.attach attachments are reported on right steps' , async ( { runInlineTest } ) = > {
class TestReporter implements Reporter {
onStepEnd ( test : TestCase , result : TestResult , step : TestStep ) {
console . log ( '%%%' , JSON . stringify ( {
title : step.title ,
attachments : step.attachments.map ( a = > ( { . . . a , body : a.body.toString ( 'utf8' ) } ) ) ,
} ) ) ;
}
}
const result = await runInlineTest ( {
'reporter.ts' : ` module.exports = ${ TestReporter . toString ( ) } ` ,
'playwright.config.ts' : ` module.exports = { reporter: './reporter' }; ` ,
'a.spec.ts' : `
import { test , expect } from '@playwright/test' ;
test . beforeAll ( async ( ) = > {
await test . step ( 'step in beforeAll' , async ( step ) = > {
await step . attach ( 'attachment1' , { body : 'content1' } ) ;
} ) ;
} ) ;
test ( 'test' , async ( ) = > {
await test . step ( 'step' , async ( step ) = > {
await step . attach ( 'attachment2' , { body : 'content2' } ) ;
} ) ;
} ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
const steps = result . outputLines . map ( line = > JSON . parse ( line ) ) ;
expect ( steps ) . toEqual ( [
{ title : 'step in beforeAll' , attachments : [ { body : 'content1' , contentType : 'text/plain' , name : 'attachment1' } ] } ,
{ title : 'beforeAll hook' , attachments : [ ] } ,
{ title : 'Before Hooks' , attachments : [ ] } ,
{ title : 'step' , attachments : [ { body : 'content2' , contentType : 'text/plain' , name : 'attachment2' } ] } ,
{ title : 'After Hooks' , attachments : [ ] } ,
] ) ;
} ) ;
2025-02-03 13:54:28 +01:00
test ( 'attachments are reported in onStepEnd' , { annotation : { type : 'issue' , description : 'https://github.com/microsoft/playwright/issues/14364' } } , async ( { runInlineTest } ) = > {
class TestReporter implements Reporter {
onStepEnd ( test : TestCase , result : TestResult , step : TestStep ) {
2025-05-23 16:03:50 -07:00
console . log ( ` %%[ ${ step . category } ] ${ step . title } : ${ result . attachments . length } attachments in result ` ) ;
2025-02-03 13:54:28 +01:00
}
}
const result = await runInlineTest ( {
'reporter.ts' : ` module.exports = ${ TestReporter . toString ( ) } ` ,
'playwright.config.ts' : ` module.exports = { reporter: './reporter' }; ` ,
'a.spec.ts' : `
import { test , expect } from '@playwright/test' ;
test ( 'test' , async ( { } , testInfo ) = > {
await test . step ( 'step' , async ( ) = > {
testInfo . attachments . push ( { name : 'attachment' , body : Buffer.from ( 'content' ) } ) ;
} ) ;
await testInfo . attach ( '4' , { body : '444' } )
} ) ;
` ,
} , { 'reporter' : '' , 'workers' : 1 } ) ;
expect ( result . outputLines ) . toEqual ( [
2025-05-23 16:03:50 -07:00
'[hook] Before Hooks: 0 attachments in result' ,
'[test.step] step: 1 attachments in result' ,
'[test.attach] 4: 2 attachments in result' ,
'[hook] After Hooks: 2 attachments in result' ,
2025-02-03 13:54:28 +01:00
] ) ;
} ) ;