interface.ts (5623B)
1 /** 2 * @license 3 * Copyright 2022 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import Mocha from 'mocha'; 8 import commonInterface from 'mocha/lib/interfaces/common'; 9 import { 10 setLogCapture, 11 getCapturedLogs, 12 } from 'puppeteer-core/internal/common/Debug.js'; 13 14 import {testIdMatchesExpectationPattern} from './utils.js'; 15 16 type SuiteFunction = ((this: Mocha.Suite) => void) | undefined; 17 type ExclusiveSuiteFunction = (this: Mocha.Suite) => void; 18 19 const skippedTests: Array<{testIdPattern: string; skip: true}> = process.env[ 20 'PUPPETEER_SKIPPED_TEST_CONFIG' 21 ] 22 ? JSON.parse(process.env['PUPPETEER_SKIPPED_TEST_CONFIG']) 23 : []; 24 25 const deflakeRetries = Number( 26 process.env['PUPPETEER_DEFLAKE_RETRIES'] 27 ? process.env['PUPPETEER_DEFLAKE_RETRIES'] 28 : 100, 29 ); 30 const deflakeTestPattern: string | undefined = 31 process.env['PUPPETEER_DEFLAKE_TESTS']; 32 33 function shouldSkipTest(test: Mocha.Test): boolean { 34 // TODO: more efficient lookup. 35 const definition = skippedTests.find(skippedTest => { 36 return testIdMatchesExpectationPattern(test, skippedTest.testIdPattern); 37 }); 38 if (definition && definition.skip) { 39 return true; 40 } 41 return false; 42 } 43 44 function shouldDeflakeTest(test: Mocha.Test): boolean { 45 if (deflakeTestPattern) { 46 // TODO: cache if we have seen it already 47 return testIdMatchesExpectationPattern(test, deflakeTestPattern); 48 } 49 return false; 50 } 51 52 function dumpLogsIfFail(this: Mocha.Context) { 53 if (this.currentTest?.state === 'failed') { 54 console.log( 55 `\n"${this.currentTest.fullTitle()}" failed. Here is a debug log:`, 56 ); 57 console.log(getCapturedLogs().join('\n') + '\n'); 58 } 59 setLogCapture(false); 60 } 61 62 function customBDDInterface(suite: Mocha.Suite) { 63 const suites: [Mocha.Suite] = [suite]; 64 65 suite.on( 66 Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, 67 function (context, file, mocha) { 68 const common = commonInterface(suites, context, mocha); 69 70 context['before'] = common.before; 71 context['after'] = common.after; 72 context['beforeEach'] = common.beforeEach; 73 context['afterEach'] = common.afterEach; 74 if (mocha.options.delay) { 75 context['run'] = common.runWithSuite(suite); 76 } 77 function describe(title: string, fn: SuiteFunction) { 78 return common.suite.create({ 79 title: title, 80 file: file, 81 fn: fn, 82 }); 83 } 84 describe.only = function (title: string, fn: ExclusiveSuiteFunction) { 85 return common.suite.only({ 86 title: title, 87 file: file, 88 fn: fn, 89 isOnly: true, 90 }); 91 }; 92 93 describe.skip = function (title: string, fn: SuiteFunction) { 94 return common.suite.skip({ 95 title: title, 96 file: file, 97 fn: fn, 98 }); 99 }; 100 101 describe.withDebugLogs = function ( 102 description: string, 103 body: (this: Mocha.Suite) => void, 104 ): void { 105 context['describe']('with Debug Logs', () => { 106 context['beforeEach'](() => { 107 setLogCapture(true); 108 }); 109 context['afterEach'](dumpLogsIfFail); 110 context['describe'](description, body); 111 }); 112 }; 113 114 // @ts-expect-error override the method to support custom functionality 115 context['describe'] = describe; 116 117 function it(title: string, fn: Mocha.TestFunction, itOnly = false) { 118 const suite = suites[0]! as Mocha.Suite; 119 const test = new Mocha.Test(title, suite.isPending() ? undefined : fn); 120 test.file = file; 121 test.parent = suite; 122 123 const describeOnly = Boolean( 124 // @ts-expect-error pokes at internal methods 125 suite.parent?._onlySuites.find(child => { 126 return child === suite; 127 }), 128 ); 129 if (shouldDeflakeTest(test)) { 130 const deflakeSuit = Mocha.Suite.create(suite, 'with Debug Logs'); 131 test.file = file; 132 deflakeSuit.beforeEach(function () { 133 setLogCapture(true); 134 }); 135 deflakeSuit.afterEach(dumpLogsIfFail); 136 for (let i = 0; i < deflakeRetries; i++) { 137 deflakeSuit.addTest(test.clone()); 138 } 139 return test; 140 } else if (!(itOnly || describeOnly) && shouldSkipTest(test)) { 141 const test = new Mocha.Test(title); 142 test.file = file; 143 suite.addTest(test); 144 return test; 145 } else { 146 suite.addTest(test); 147 return test; 148 } 149 } 150 151 it.only = function (title: string, fn: Mocha.TestFunction) { 152 return common.test.only( 153 mocha, 154 (context['it'] as unknown as typeof it)(title, fn, true), 155 ); 156 }; 157 158 it.skip = function (title: string) { 159 return context['it'](title); 160 }; 161 162 function wrapDeflake( 163 // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 164 func: Function, 165 ): (repeats: number, title: string, fn: Mocha.AsyncFunc) => void { 166 return (repeats: number, title: string, fn: Mocha.AsyncFunc): void => { 167 (context['describe'] as unknown as typeof describe).withDebugLogs( 168 'with Debug Logs', 169 () => { 170 for (let i = 1; i <= repeats; i++) { 171 func(`${i}/${title}`, fn); 172 } 173 }, 174 ); 175 }; 176 } 177 178 it.deflake = wrapDeflake(it); 179 it.deflakeOnly = wrapDeflake(it.only); 180 181 // @ts-expect-error override the method to support custom functionality 182 context.it = it; 183 }, 184 ); 185 } 186 187 customBDDInterface.description = 'Custom BDD'; 188 189 module.exports = customBDDInterface;