feat: print response text when toBeOK fails (#16040)
This commit is contained in:
parent
4a47e275c8
commit
af8e3e7afa
|
|
@ -28,6 +28,7 @@
|
||||||
"./lib/utils/httpServer": "./lib/utils/httpServer.js",
|
"./lib/utils/httpServer": "./lib/utils/httpServer.js",
|
||||||
"./lib/utils/hostPlatform": "./lib/utils/hostPlatform.js",
|
"./lib/utils/hostPlatform": "./lib/utils/hostPlatform.js",
|
||||||
"./lib/utils/manualPromise": "./lib/utils/manualPromise.js",
|
"./lib/utils/manualPromise": "./lib/utils/manualPromise.js",
|
||||||
|
"./lib/utils/mimeType": "./lib/utils/mimeType.js",
|
||||||
"./lib/utils/multimap": "./lib/utils/multimap.js",
|
"./lib/utils/multimap": "./lib/utils/multimap.js",
|
||||||
"./lib/utils/processLauncher": "./lib/utils/processLauncher.js",
|
"./lib/utils/processLauncher": "./lib/utils/processLauncher.js",
|
||||||
"./lib/utils/processLauncherCleanupEntrypoint": "./lib/utils/processLauncherCleanupEntrypoint.js",
|
"./lib/utils/processLauncherCleanupEntrypoint": "./lib/utils/processLauncherCleanupEntrypoint.js",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { getPlaywrightVersion } from '../../common/userAgent';
|
||||||
import { urlMatches } from '../../common/netUtils';
|
import { urlMatches } from '../../common/netUtils';
|
||||||
import { Frame } from '../frames';
|
import { Frame } from '../frames';
|
||||||
import type { LifecycleEvent } from '../types';
|
import type { LifecycleEvent } from '../types';
|
||||||
|
import { isTextualMimeType } from '../../utils/mimeType';
|
||||||
|
|
||||||
const FALLBACK_HTTP_VERSION = 'HTTP/1.1';
|
const FALLBACK_HTTP_VERSION = 'HTTP/1.1';
|
||||||
|
|
||||||
|
|
@ -599,7 +600,3 @@ function parseCookie(c: string): har.Cookie {
|
||||||
}
|
}
|
||||||
return 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=.*)?$/);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
19
packages/playwright-core/src/utils/mimeType.ts
Normal file
19
packages/playwright-core/src/utils/mimeType.ts
Normal file
|
|
@ -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=.*)?$/);
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import type { Locator, Page, APIResponse } from 'playwright-core';
|
import type { Locator, Page, APIResponse } from 'playwright-core';
|
||||||
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
||||||
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils';
|
import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { expectTypes, callLogText } from '../util';
|
import { expectTypes, callLogText } from '../util';
|
||||||
|
|
@ -23,6 +24,7 @@ import { toBeTruthy } from './toBeTruthy';
|
||||||
import { toEqual } from './toEqual';
|
import { toEqual } from './toEqual';
|
||||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||||
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||||
|
import { isTextualMimeType } from 'playwright-core/lib/utils/mimeType';
|
||||||
|
|
||||||
interface LocatorEx extends Locator {
|
interface LocatorEx extends Locator {
|
||||||
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
||||||
|
|
@ -289,8 +291,18 @@ export async function toBeOK(
|
||||||
) {
|
) {
|
||||||
const matcherName = 'toBeOK';
|
const matcherName = 'toBeOK';
|
||||||
expectTypes(response, ['APIResponse'], matcherName);
|
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();
|
const pass = response.ok();
|
||||||
return { message, pass };
|
return { message, pass };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -417,3 +417,65 @@ test('should support toBeOK', async ({ runInlineTest, server }) => {
|
||||||
expect(result.output).toContain(`← 404 Not Found`);
|
expect(result.output).toContain(`← 404 Not Found`);
|
||||||
expect(result.output).toContain(`Error: toBeOK can be only used with APIResponse object`);
|
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`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,7 @@ class TestServer {
|
||||||
return;
|
return;
|
||||||
if (err) {
|
if (err) {
|
||||||
response.statusCode = 404;
|
response.statusCode = 404;
|
||||||
|
response.setHeader('Content-Type', 'text/plain');
|
||||||
response.end(`File not found: ${filePath}`);
|
response.end(`File not found: ${filePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue