From af8e3e7afa142975181153a0d86b602821249500 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 29 Jul 2022 11:46:48 -0700 Subject: [PATCH] feat: print response text when toBeOK fails (#16040) --- packages/playwright-core/package.json | 1 + .../src/server/har/harTracer.ts | 5 +- .../playwright-core/src/utils/mimeType.ts | 19 ++++++ .../playwright-test/src/matchers/matchers.ts | 16 ++++- .../playwright.expect.true.spec.ts | 62 +++++++++++++++++++ utils/testserver/index.js | 1 + 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 packages/playwright-core/src/utils/mimeType.ts diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 9a539409bb..db29ba485f 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -28,6 +28,7 @@ "./lib/utils/httpServer": "./lib/utils/httpServer.js", "./lib/utils/hostPlatform": "./lib/utils/hostPlatform.js", "./lib/utils/manualPromise": "./lib/utils/manualPromise.js", + "./lib/utils/mimeType": "./lib/utils/mimeType.js", "./lib/utils/multimap": "./lib/utils/multimap.js", "./lib/utils/processLauncher": "./lib/utils/processLauncher.js", "./lib/utils/processLauncherCleanupEntrypoint": "./lib/utils/processLauncherCleanupEntrypoint.js", diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index ab196e1197..ce1ba85ff8 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -31,6 +31,7 @@ import { getPlaywrightVersion } from '../../common/userAgent'; import { urlMatches } from '../../common/netUtils'; import { Frame } from '../frames'; import type { LifecycleEvent } from '../types'; +import { isTextualMimeType } from '../../utils/mimeType'; const FALLBACK_HTTP_VERSION = 'HTTP/1.1'; @@ -599,7 +600,3 @@ function parseCookie(c: string): har.Cookie { } return cookie; } - -function isTextualMimeType(mimeType: string) { - return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); -} diff --git a/packages/playwright-core/src/utils/mimeType.ts b/packages/playwright-core/src/utils/mimeType.ts new file mode 100644 index 0000000000..388bd6cb2e --- /dev/null +++ b/packages/playwright-core/src/utils/mimeType.ts @@ -0,0 +1,19 @@ +/** + * 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. + */ + +export function isTextualMimeType(mimeType: string) { + return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); +} \ No newline at end of file diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index c45f4b4ad8..2441058ca3 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -16,6 +16,7 @@ import type { Locator, Page, APIResponse } from 'playwright-core'; import type { FrameExpectOptions } from 'playwright-core/lib/client/types'; +import { colors } from 'playwright-core/lib/utilsBundle'; import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils'; import type { Expect } from '../types'; import { expectTypes, callLogText } from '../util'; @@ -23,6 +24,7 @@ import { toBeTruthy } from './toBeTruthy'; import { toEqual } from './toEqual'; import { toExpectedTextValues, toMatchText } from './toMatchText'; import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace'; +import { isTextualMimeType } from 'playwright-core/lib/utils/mimeType'; interface LocatorEx extends Locator { _expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>; @@ -289,8 +291,18 @@ export async function toBeOK( ) { const matcherName = 'toBeOK'; expectTypes(response, ['APIResponse'], matcherName); - const log = (this.isNot === response.ok()) ? await response._fetchLog() : []; - const message = () => this.utils.matcherHint(matcherName, undefined, '', { isNot: this.isNot }) + callLogText(log); + + const contentType = response.headers()['content-type']; + const isTextEncoding = contentType && isTextualMimeType(contentType); + const [log, text] = (this.isNot === response.ok()) ? await Promise.all([ + response._fetchLog(), + isTextEncoding ? response.text() : null + ]) : []; + + const message = () => this.utils.matcherHint(matcherName, undefined, '', { isNot: this.isNot }) + + callLogText(log) + + (text === null ? '' : `\nResponse text:\n${colors.dim(text?.substring(0, 1000) || '')}`); + const pass = response.ok(); return { message, pass }; } diff --git a/tests/playwright-test/playwright.expect.true.spec.ts b/tests/playwright-test/playwright.expect.true.spec.ts index 7401ca03ec..8b6b892a6f 100644 --- a/tests/playwright-test/playwright.expect.true.spec.ts +++ b/tests/playwright-test/playwright.expect.true.spec.ts @@ -417,3 +417,65 @@ test('should support toBeOK', async ({ runInlineTest, server }) => { expect(result.output).toContain(`← 404 Not Found`); expect(result.output).toContain(`Error: toBeOK can be only used with APIResponse object`); }); + +test('should print response text if toBeOK fails', async ({ runInlineTest, server }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + + test('fail', async ({ page }) => { + const res = await page.request.get('${server.PREFIX}/unknown'); + await expect(res).toBeOK(); + }); + `, + }, { workers: 1 }); + expect(result.failed).toBe(1); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`→ GET ${server.PREFIX}/unknown`); + expect(result.output).toContain(`← 404 Not Found`); + expect(result.output).toContain(`Response text:`); + expect(result.output).toContain(`File not found`); +}); + +test('should only print response with text content type if toBeOK fails', async ({ runInlineTest, server }) => { + server.setRoute('/text-content-type', (req, res) => { + res.statusCode = 404; + res.setHeader('Content-type', 'text/plain'); + res.end('Text error'); + }); + server.setRoute('/no-content-type', (req, res) => { + res.statusCode = 404; + res.end('No content type error'); + }); + server.setRoute('/binary-content-type', (req, res) => { + res.statusCode = 404; + res.setHeader('Content-type', 'image/bmp'); + res.end('Image content type error'); + }); + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + + test('text content type', async ({ page }) => { + const res = await page.request.get('${server.PREFIX}/text-content-type'); + await expect(res).toBeOK(); + }); + + test('no content type', async ({ page }) => { + const res = await page.request.get('${server.PREFIX}/no-content-type'); + await expect(res).toBeOK(); + }); + + test('image content type', async ({ page }) => { + const res = await page.request.get('${server.PREFIX}/image-content-type'); + await expect(res).toBeOK(); + }); + `, + }, { workers: 1 }); + expect(result.failed).toBe(3); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`← 404 Not Found`); + expect(result.output).toContain(`Text error`); + expect(result.output).not.toContain(`No content type error`); + expect(result.output).not.toContain(`Image content type error`); +}); diff --git a/utils/testserver/index.js b/utils/testserver/index.js index 81fc385c0d..7fd92731d5 100644 --- a/utils/testserver/index.js +++ b/utils/testserver/index.js @@ -307,6 +307,7 @@ class TestServer { return; if (err) { response.statusCode = 404; + response.setHeader('Content-Type', 'text/plain'); response.end(`File not found: ${filePath}`); return; }