feat(expext): toBeOK for APIResponse (#10596)
This commit is contained in:
parent
729da65eba
commit
d66b7aab3b
|
|
@ -268,6 +268,11 @@ export class APIResponse implements api.APIResponse {
|
|||
_fetchUid(): string {
|
||||
return this._initializer.fetchUid;
|
||||
}
|
||||
|
||||
async _fetchLog(): Promise<string[]> {
|
||||
const { log } = await this._request._channel.fetchLog({ fetchUid: this._fetchUid() });
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
||||
type ServerFilePayload = NonNullable<channels.FormField['file']>;
|
||||
|
|
|
|||
|
|
@ -178,8 +178,8 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
|
|||
this._object.dispose();
|
||||
}
|
||||
|
||||
async fetch(params: channels.APIRequestContextFetchParams, metadata?: channels.Metadata): Promise<channels.APIRequestContextFetchResult> {
|
||||
const fetchResponse = await this._object.fetch(params);
|
||||
async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise<channels.APIRequestContextFetchResult> {
|
||||
const fetchResponse = await this._object.fetch(params, metadata);
|
||||
return {
|
||||
response: {
|
||||
url: fetchResponse.url,
|
||||
|
|
@ -196,7 +196,12 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
|
|||
return { binary: buffer ? buffer.toString('base64') : undefined };
|
||||
}
|
||||
|
||||
async fetchLog(params: channels.APIRequestContextFetchLogParams, metadata?: channels.Metadata): Promise<channels.APIRequestContextFetchLogResult> {
|
||||
const log = this._object.fetchLog.get(params.fetchUid) || [];
|
||||
return { log };
|
||||
}
|
||||
|
||||
async disposeAPIResponse(params: channels.APIRequestContextDisposeAPIResponseParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._object.fetchResponses.delete(params.fetchUid);
|
||||
this._object.disposeResponse(params.fetchUid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ export interface APIRequestContextChannel extends APIRequestContextEventTarget,
|
|||
_type_APIRequestContext: boolean;
|
||||
fetch(params: APIRequestContextFetchParams, metadata?: Metadata): Promise<APIRequestContextFetchResult>;
|
||||
fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, metadata?: Metadata): Promise<APIRequestContextFetchResponseBodyResult>;
|
||||
fetchLog(params: APIRequestContextFetchLogParams, metadata?: Metadata): Promise<APIRequestContextFetchLogResult>;
|
||||
storageState(params?: APIRequestContextStorageStateParams, metadata?: Metadata): Promise<APIRequestContextStorageStateResult>;
|
||||
disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: Metadata): Promise<APIRequestContextDisposeAPIResponseResult>;
|
||||
dispose(params?: APIRequestContextDisposeParams, metadata?: Metadata): Promise<APIRequestContextDisposeResult>;
|
||||
|
|
@ -307,6 +308,15 @@ export type APIRequestContextFetchResponseBodyOptions = {
|
|||
export type APIRequestContextFetchResponseBodyResult = {
|
||||
binary?: Binary,
|
||||
};
|
||||
export type APIRequestContextFetchLogParams = {
|
||||
fetchUid: string,
|
||||
};
|
||||
export type APIRequestContextFetchLogOptions = {
|
||||
|
||||
};
|
||||
export type APIRequestContextFetchLogResult = {
|
||||
log: string[],
|
||||
};
|
||||
export type APIRequestContextStorageStateParams = {};
|
||||
export type APIRequestContextStorageStateOptions = {};
|
||||
export type APIRequestContextStorageStateResult = {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,14 @@ APIRequestContext:
|
|||
returns:
|
||||
binary?: binary
|
||||
|
||||
fetchLog:
|
||||
parameters:
|
||||
fetchUid: string
|
||||
returns:
|
||||
log:
|
||||
type: array
|
||||
items: string
|
||||
|
||||
storageState:
|
||||
returns:
|
||||
cookies:
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.APIRequestContextFetchResponseBodyParams = tObject({
|
||||
fetchUid: tString,
|
||||
});
|
||||
scheme.APIRequestContextFetchLogParams = tObject({
|
||||
fetchUid: tString,
|
||||
});
|
||||
scheme.APIRequestContextStorageStateParams = tOptional(tObject({}));
|
||||
scheme.APIRequestContextDisposeAPIResponseParams = tObject({
|
||||
fetchUid: tString,
|
||||
|
|
|
|||
|
|
@ -17,19 +17,19 @@
|
|||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { Progress, ProgressController } from './progress';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import { pipeline, Readable, Transform } from 'stream';
|
||||
import url from 'url';
|
||||
import zlib from 'zlib';
|
||||
import { HTTPCredentials } from '../../types/types';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||
import { assert, createGuid, getPlaywrightVersion, monotonicTime } from '../utils/utils';
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { CookieStore, domainMatches } from './cookieStore';
|
||||
import { MultipartFormData } from './formData';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { CallMetadata, SdkObject } from './instrumentation';
|
||||
import { Playwright } from './playwright';
|
||||
import * as types from './types';
|
||||
import { HeadersArray, ProxySettings } from './types';
|
||||
|
|
@ -50,6 +50,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
};
|
||||
|
||||
readonly fetchResponses: Map<string, Buffer> = new Map();
|
||||
readonly fetchLog: Map<string, string[]> = new Map();
|
||||
protected static allInstances: Set<APIRequestContext> = new Set();
|
||||
|
||||
static findResponseBody(guid: string): Buffer | undefined {
|
||||
|
|
@ -69,9 +70,15 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
protected _disposeImpl() {
|
||||
APIRequestContext.allInstances.delete(this);
|
||||
this.fetchResponses.clear();
|
||||
this.fetchLog.clear();
|
||||
this.emit(APIRequestContext.Events.Dispose);
|
||||
}
|
||||
|
||||
disposeResponse(fetchUid: string) {
|
||||
this.fetchResponses.delete(fetchUid);
|
||||
this.fetchLog.delete(fetchUid);
|
||||
}
|
||||
|
||||
abstract dispose(): void;
|
||||
|
||||
abstract _defaultOptions(): FetchRequestOptions;
|
||||
|
|
@ -85,7 +92,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
return uid;
|
||||
}
|
||||
|
||||
async fetch(params: channels.APIRequestContextFetchParams): Promise<Omit<types.APIResponse, 'body'> & { fetchUid: string }> {
|
||||
async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise<Omit<types.APIResponse, 'body'> & { fetchUid: string }> {
|
||||
const headers: { [name: string]: string } = {};
|
||||
const defaults = this._defaultOptions();
|
||||
headers['user-agent'] = defaults.userAgent;
|
||||
|
|
@ -141,15 +148,19 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
requestUrl.searchParams.set(name, value);
|
||||
}
|
||||
|
||||
let postData;
|
||||
let postData: Buffer | undefined;
|
||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
|
||||
postData = serializePostData(params, headers);
|
||||
else if (params.postData || params.jsonData || params.formData || params.multipartData)
|
||||
throw new Error(`Method ${method} does not accept post data`);
|
||||
if (postData)
|
||||
headers['content-length'] = String(postData.byteLength);
|
||||
const fetchResponse = await this._sendRequest(requestUrl, options, postData);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const fetchResponse = await controller.run(progress => {
|
||||
return this._sendRequest(progress, requestUrl, options, postData);
|
||||
});
|
||||
const fetchUid = this._storeResponseBody(fetchResponse.body);
|
||||
this.fetchLog.set(fetchUid, controller.metadata.log);
|
||||
if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400))
|
||||
throw new Error(`${fetchResponse.status} ${fetchResponse.statusText}`);
|
||||
return { ...fetchResponse, fetchUid };
|
||||
|
|
@ -191,17 +202,15 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
}
|
||||
}
|
||||
|
||||
private async _sendRequest(url: URL, options: https.RequestOptions & { maxRedirects: number, deadline: number }, postData?: Buffer): Promise<types.APIResponse>{
|
||||
private async _sendRequest(progress: Progress, url: URL, options: https.RequestOptions & { maxRedirects: number, deadline: number }, postData?: Buffer): Promise<types.APIResponse>{
|
||||
await this._updateRequestCookieHeader(url, options);
|
||||
return new Promise<types.APIResponse>((fulfill, reject) => {
|
||||
const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
|
||||
= (url.protocol === 'https:' ? https : http).request;
|
||||
const request = requestConstructor(url, options, async response => {
|
||||
if (debugLogger.isEnabled('api')) {
|
||||
debugLogger.log('api', `← ${response.statusCode} ${response.statusMessage}`);
|
||||
for (const [name, value] of Object.entries(response.headers))
|
||||
debugLogger.log('api', ` ${name}: ${value}`);
|
||||
}
|
||||
progress.log(`← ${response.statusCode} ${response.statusMessage}`);
|
||||
for (const [name, value] of Object.entries(response.headers))
|
||||
progress.log(` ${name}: ${value}`);
|
||||
if (response.headers['set-cookie'])
|
||||
await this._updateCookiesFromHeader(response.url || url.toString(), response.headers['set-cookie']);
|
||||
if (redirectStatus.includes(response.statusCode!)) {
|
||||
|
|
@ -242,7 +251,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
// HTTP-redirect fetch step 4: If locationURL is null, then return response.
|
||||
if (response.headers.location) {
|
||||
const locationURL = new URL(response.headers.location, url);
|
||||
fulfill(this._sendRequest(locationURL, redirectOptions, postData));
|
||||
fulfill(this._sendRequest(progress, locationURL, redirectOptions, postData));
|
||||
request.destroy();
|
||||
return;
|
||||
}
|
||||
|
|
@ -254,7 +263,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
const { username, password } = credentials;
|
||||
const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64');
|
||||
options.headers!['authorization'] = `Basic ${encoded}`;
|
||||
fulfill(this._sendRequest(url, options, postData));
|
||||
fulfill(this._sendRequest(progress, url, options, postData));
|
||||
request.destroy();
|
||||
return;
|
||||
}
|
||||
|
|
@ -304,12 +313,10 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
this.on(APIRequestContext.Events.Dispose, disposeListener);
|
||||
request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener));
|
||||
|
||||
if (debugLogger.isEnabled('api')) {
|
||||
debugLogger.log('api', `→ ${options.method} ${url.toString()}`);
|
||||
if (options.headers) {
|
||||
for (const [name, value] of Object.entries(options.headers))
|
||||
debugLogger.log('api', ` ${name}: ${value}`);
|
||||
}
|
||||
progress.log(`→ ${options.method} ${url.toString()}`);
|
||||
if (options.headers) {
|
||||
for (const [name, value] of Object.entries(options.headers))
|
||||
progress.log(` ${name}: ${value}`);
|
||||
}
|
||||
|
||||
if (options.deadline) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
toBeEnabled,
|
||||
toBeFocused,
|
||||
toBeHidden,
|
||||
toBeOK,
|
||||
toBeVisible,
|
||||
toContainText,
|
||||
toHaveAttribute,
|
||||
|
|
@ -101,6 +102,7 @@ const customMatchers = {
|
|||
toBeEnabled,
|
||||
toBeFocused,
|
||||
toBeHidden,
|
||||
toBeOK,
|
||||
toBeVisible,
|
||||
toContainText,
|
||||
toHaveAttribute,
|
||||
|
|
|
|||
|
|
@ -14,18 +14,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from 'playwright-core';
|
||||
import { Locator, Page, APIResponse } from 'playwright-core';
|
||||
import { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
||||
import { constructURLBasedOnBaseURL } from 'playwright-core/lib/utils/utils';
|
||||
import type { Expect } from '../types';
|
||||
import { expectType } from '../util';
|
||||
import { toBeTruthy } from './toBeTruthy';
|
||||
import { toEqual } from './toEqual';
|
||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
import { callLogText, toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
|
||||
interface LocatorEx extends Locator {
|
||||
_expect(expression: string, options: FrameExpectOptions): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
||||
}
|
||||
|
||||
interface APIResponseEx extends APIResponse {
|
||||
_fetchLog(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export function toBeChecked(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: LocatorEx,
|
||||
|
|
@ -263,3 +268,15 @@ export function toHaveURL(
|
|||
return await locator._expect('to.have.url', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
export async function toBeOK(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
response: APIResponseEx
|
||||
) {
|
||||
const matcherName = 'toBeOK';
|
||||
expectType(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 pass = response.ok();
|
||||
return { message, pass };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ declare global {
|
|||
*/
|
||||
toBeHidden(options?: { timeout?: number }): Promise<R>;
|
||||
|
||||
/**
|
||||
* Asserts given APIResponse's status is between 200 and 299.
|
||||
*/
|
||||
toBeOK(): Promise<R>;
|
||||
|
||||
/**
|
||||
* Asserts given DOM node visible on the screen.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ it('should throw on network error', async ({ context, server }) => {
|
|||
req.socket.destroy();
|
||||
});
|
||||
const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
|
||||
expect(error.message).toBe('apiRequestContext.get: socket hang up');
|
||||
expect(error.message).toContain('apiRequestContext.get: socket hang up');
|
||||
});
|
||||
|
||||
it('should throw on network error after redirect', async ({ context, server }) => {
|
||||
|
|
@ -78,7 +78,7 @@ it('should throw on network error after redirect', async ({ context, server }) =
|
|||
req.socket.destroy();
|
||||
});
|
||||
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
|
||||
expect(error.message).toBe('apiRequestContext.get: socket hang up');
|
||||
expect(error.message).toContain('apiRequestContext.get: socket hang up');
|
||||
});
|
||||
|
||||
it('should throw on network error when sending body', async ({ context, server }) => {
|
||||
|
|
@ -92,7 +92,7 @@ it('should throw on network error when sending body', async ({ context, server }
|
|||
req.socket.destroy();
|
||||
});
|
||||
const error = await context.request.get(server.PREFIX + '/test').catch(e => e);
|
||||
expect(error.message).toBe('apiRequestContext.get: aborted');
|
||||
expect(error.message).toContain('apiRequestContext.get: aborted');
|
||||
});
|
||||
|
||||
it('should throw on network error when sending body after redirect', async ({ context, server }) => {
|
||||
|
|
@ -107,7 +107,7 @@ it('should throw on network error when sending body after redirect', async ({ co
|
|||
req.socket.destroy();
|
||||
});
|
||||
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
|
||||
expect(error.message).toBe('apiRequestContext.get: aborted');
|
||||
expect(error.message).toContain('apiRequestContext.get: aborted');
|
||||
});
|
||||
|
||||
it('should add session cookies to request', async ({ context, server }) => {
|
||||
|
|
|
|||
|
|
@ -322,3 +322,41 @@ test('should print syntax error', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain(`Unexpected token "]" while parsing selector "row]"`);
|
||||
});
|
||||
|
||||
test('should support toBeOK', async ({ runInlineTest, server }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass with response', async ({ page }) => {
|
||||
const res = await page.request.get('${server.EMPTY_PAGE}');
|
||||
await expect(res).toBeOK();
|
||||
});
|
||||
|
||||
test('pass with not', async ({ page }) => {
|
||||
const res = await page.request.get('${server.PREFIX}/unknown');
|
||||
await expect(res).not.toBeOK();
|
||||
});
|
||||
|
||||
test('fail with invalid argument', async ({ page }) => {
|
||||
await expect(page).toBeOK();
|
||||
});
|
||||
|
||||
test('fail with promise', async ({ page }) => {
|
||||
const res = page.request.get('${server.EMPTY_PAGE}').catch(e => {});
|
||||
await expect(res).toBeOK();
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
const res = await page.request.get('${server.PREFIX}/unknown');
|
||||
await expect(res).toBeOK();
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.passed).toBe(2);
|
||||
expect(result.failed).toBe(3);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain(`→ GET ${server.PREFIX}/unknown`);
|
||||
expect(result.output).toContain(`← 404 Not Found`);
|
||||
expect(result.output).toContain(`Error: toBeOK can be only used with APIResponse object`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue