fix(expect): beautiful expect stacks (#9204)

We now mark our wrapper as `__PWTRAP__[expect.toHaveText]` and
find it later in the stack trace.

Added trace/inspector tests to ensure this behavior in the future.
This commit is contained in:
Dmitry Gozman 2021-09-28 16:02:34 -07:00 committed by GitHub
parent b93718daea
commit 0a690778e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 25 deletions

View File

@ -74,6 +74,10 @@ function wrap(matcherName: string, matcher: any) {
return matcher.call(this, ...args);
const INTERNAL_STACK_LENGTH = 3;
// at Object.__PWTRAP__[expect.toHaveText] (...)
// at __EXTERNAL_MATCHER_TRAP__ (...)
// at Object.throwingMatcher [as toHaveText] (...)
// at <test function> (...)
const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1);
const step = testInfo._addStep({
category: 'expect',
@ -107,6 +111,7 @@ function wrap(matcherName: string, matcher: any) {
reportStepError(e);
}
};
result.displayName = '__PWTRAP__[expect.' + matcherName + ']';
return result;
}

View File

@ -33,8 +33,6 @@ export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string):
const ROOT_DIR = path.resolve(__dirname, '..', '..');
const CLIENT_LIB = path.join(ROOT_DIR, 'lib', 'client');
const CLIENT_SRC = path.join(ROOT_DIR, 'src', 'client');
const TEST_LIB = path.join(ROOT_DIR, 'lib', 'test');
const TEST_SRC = path.join(ROOT_DIR, 'src', 'test');
export type ParsedStackTrace = {
allFrames: StackFrame[];
@ -65,14 +63,7 @@ export function captureStackTrace(): ParsedStackTrace {
const fileName = path.resolve(process.cwd(), frame.file);
if (isTesting && fileName.includes(path.join('playwright', 'tests', 'config', 'coverage.js')))
return null;
const inClient =
// Allow fixtures in the reported stacks.
(!fileName.includes('test/index') && !fileName.includes('test\\index')) && (
frame.file.includes(path.join('node_modules', 'expect'))
|| fileName.startsWith(CLIENT_LIB)
|| fileName.startsWith(CLIENT_SRC)
|| fileName.startsWith(TEST_LIB)
|| fileName.startsWith(TEST_SRC));
const inClient = fileName.startsWith(CLIENT_LIB) || fileName.startsWith(CLIENT_SRC);
const parsed: ParsedFrame = {
frame: {
file: fileName,
@ -87,23 +78,29 @@ export function captureStackTrace(): ParsedStackTrace {
}).filter(Boolean) as ParsedFrame[];
let apiName = '';
// Deepest transition between non-client code calling into client code
// is the api entry.
const allFrames = parsedFrames;
for (let i = 0; i < parsedFrames.length - 1; i++) {
if (parsedFrames[i].inClient && !parsedFrames[i + 1].inClient) {
const frame = parsedFrames[i].frame;
const text = parsedFrames[i].frameText;
// expect matchers have the following stack structure:
// at __EXTERNAL_MATCHER_TRAP__ (.../index.js:342:30)
// at Object.throwingMatcher [as toBeChecked] (.../index.js:343:15)
const aliasIndex = text.indexOf('[as ');
if (aliasIndex !== -1)
apiName = 'expect.' + text.substring(aliasIndex + 4, text.indexOf(']'));
else
// expect matchers have the following stack structure:
// at Object.__PWTRAP__[expect.toHaveText] (...)
// at __EXTERNAL_MATCHER_TRAP__ (...)
// at Object.throwingMatcher [as toHaveText] (...)
const TRAP = '__PWTRAP__[';
const expectIndex = parsedFrames.findIndex(f => f.frameText.includes(TRAP));
if (expectIndex !== -1) {
const text = parsedFrames[expectIndex].frameText;
const aliasIndex = text.indexOf(TRAP);
apiName = text.substring(aliasIndex + TRAP.length, text.indexOf(']'));
parsedFrames = parsedFrames.slice(expectIndex + 3);
} else {
// Deepest transition between non-client code calling into client code
// is the api entry.
for (let i = 0; i < parsedFrames.length - 1; i++) {
if (parsedFrames[i].inClient && !parsedFrames[i + 1].inClient) {
const frame = parsedFrames[i].frame;
apiName = frame.function ? frame.function[0].toLowerCase() + frame.function.slice(1) : '';
parsedFrames = parsedFrames.slice(i + 1);
break;
parsedFrames = parsedFrames.slice(i + 1);
break;
}
}
}

View File

@ -173,6 +173,25 @@ it.describe('pause', () => {
await scriptPromise;
});
it('should show expect.toHaveText', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await expect(page.locator('button')).toHaveText('Submit');
await page.pause(); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'page.pause- XXms',
'expect.toHaveText(button)- XXms',
'page.pause',
]);
await recorderPage.click('[title="Resume"]');
await scriptPromise;
});
it('should highlight waitForEvent', async ({ page, recorderPageGetter }) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {

View File

@ -105,6 +105,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
const page = await context.newPage();
await page.goto('data:text/html,<html>Hello world</html>');
await page.setContent('<button>Click</button>');
await expect(page.locator('button')).toHaveText('Click');
await page.evaluate(({ a }) => {
console.log('Info');
console.warn('Warning');
@ -158,6 +159,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
await expect(traceViewer.actionTitles).toHaveText([
/page.gotodata:text\/html,<html>Hello world<\/html>— [\d.ms]+/,
/page.setContent— [\d.ms]+/,
/expect.toHaveTextbutton— [\d.ms]+/,
/page.evaluate— [\d.ms]+/,
/page.click"Click"— [\d.ms]+/,
/page.waitForEvent— [\d.ms]+/,