Skip to content

Commit

Permalink
chore: various roll fixes for .NET (#31914)
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt authored Jul 30, 2024
1 parent b8b5628 commit 5518720
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 9 deletions.
6 changes: 6 additions & 0 deletions docs/src/api/class-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,12 @@ If set changes the request URL. New URL must have same protocol as original one.
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
Defaults to `20`. Pass `0` to not follow redirects.

### option: Route.fetch.maxRetries
* since: v1.46
- `maxRetries` <[int]>

Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.

### option: Route.fetch.timeout
* since: v1.33
- `timeout` <[float]>
Expand Down
6 changes: 3 additions & 3 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,9 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
## context-option-clientCertificates
- `clientCertificates` <[Array]<[Object]>>
- `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port.
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
- `certPath` ?<[path]> Path to the file with the certificate in PEM format.
- `keyPath` ?<[path]> Path to the file with the private key in PEM format.
- `pfxPath` ?<[path]> Path to the PFX or PKCS12 encoded private key and certificate chain.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).

TLS Client Authentication allows the server to request a client certificate and verify it.
Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19570,6 +19570,12 @@ export interface Route {
*/
maxRedirects?: number;

/**
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
*/
maxRetries?: number;

/**
* If set changes the request method (e.g. GET or POST).
*/
Expand Down
36 changes: 32 additions & 4 deletions tests/library/global-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@
import os from 'os';
import * as util from 'util';
import { getPlaywrightVersion } from '../../packages/playwright-core/lib/utils/userAgent';
import { expect, playwrightTest as it } from '../config/browserTest';
import { expect, playwrightTest as base } from '../config/browserTest';
import { kTargetClosedErrorMessage } from 'tests/config/errors';

const it = base.extend({
context: async () => {
throw new Error('global fetch tests should not use context');
}
});

it.skip(({ mode }) => mode !== 'default');

for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put'] as const) {
Expand All @@ -33,16 +39,19 @@ for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put']
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
expect(await response.text()).toBe('head' === method ? '' : '{"foo": "bar"}\n');
await request.dispose();
});
}


it(`should dispose global request`, async function({ playwright, server }) {
const request = await playwright.request.newContext();
const response = await request.get(server.PREFIX + '/simple.json');
expect(await response.json()).toEqual({ foo: 'bar' });
await request.dispose();
const error = await response.body().catch(e => e);
expect(error.message).toContain('Response has been disposed');
await request.dispose();
});

it('should support global userAgent option', async ({ playwright, server }) => {
Expand All @@ -54,13 +63,15 @@ it('should support global userAgent option', async ({ playwright, server }) => {
expect(response.ok()).toBeTruthy();
expect(response.url()).toBe(server.EMPTY_PAGE);
expect(serverRequest.headers['user-agent']).toBe('My Agent');
await request.dispose();
});

it('should support global timeout option', async ({ playwright, server }) => {
const request = await playwright.request.newContext({ timeout: 100 });
server.setRoute('/empty.html', (req, res) => {});
const error = await request.get(server.EMPTY_PAGE).catch(e => e);
expect(error.message).toContain('Request timed out after 100ms');
await request.dispose();
});

it('should propagate extra http headers with redirects', async ({ playwright, server }) => {
Expand All @@ -76,6 +87,7 @@ it('should propagate extra http headers with redirects', async ({ playwright, se
expect(req1.headers['my-secret']).toBe('Value');
expect(req2.headers['my-secret']).toBe('Value');
expect(req3.headers['my-secret']).toBe('Value');
await request.dispose();
});

it('should support global httpCredentials option', async ({ playwright, server }) => {
Expand All @@ -96,27 +108,31 @@ it('should return error with wrong credentials', async ({ playwright, server })
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'wrong' } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
await request.dispose();
});

it('should work with correct credentials and matching origin', async ({ playwright, server }) => {
server.setAuth('/empty.html', 'user', 'pass');
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
await request.dispose();
});

it('should work with correct credentials and matching origin case insensitive', async ({ playwright, server }) => {
server.setAuth('/empty.html', 'user', 'pass');
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase() } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
await request.dispose();
});

it('should return error with correct credentials and mismatching scheme', async ({ playwright, server }) => {
server.setAuth('/empty.html', 'user', 'pass');
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.replace('http://', 'https://') } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
await request.dispose();
});

it('should return error with correct credentials and mismatching hostname', async ({ playwright, server }) => {
Expand All @@ -126,6 +142,7 @@ it('should return error with correct credentials and mismatching hostname', asyn
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
await request.dispose();
});

it('should return error with correct credentials and mismatching port', async ({ playwright, server }) => {
Expand All @@ -134,6 +151,7 @@ it('should return error with correct credentials and mismatching port', async ({
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(401);
await request.dispose();
});

it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
Expand All @@ -152,6 +170,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
const response = await request.get(server.EMPTY_PAGE);
expect(response.status()).toBe(200);
expect(credentials).toBe('user:pass');
await request.dispose();
});

it('should support HTTPCredentials.send', async ({ playwright, server }) => {
Expand All @@ -176,25 +195,29 @@ it('should support HTTPCredentials.send', async ({ playwright, server }) => {
expect(serverRequest.headers.authorization).toBe(undefined);
expect(response.status()).toBe(200);
}
await request.dispose();
});

it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => {
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
const response = await request.get(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200);
await request.dispose();
});

it('should propagate ignoreHTTPSErrors on redirects', async ({ playwright, httpsServer }) => {
httpsServer.setRedirect('/redir', '/empty.html');
const request = await playwright.request.newContext();
const response = await request.get(httpsServer.PREFIX + '/redir', { ignoreHTTPSErrors: true });
expect(response.status()).toBe(200);
await request.dispose();
});

it('should resolve url relative to global baseURL option', async ({ playwright, server }) => {
const request = await playwright.request.newContext({ baseURL: server.PREFIX });
const response = await request.get('/empty.html');
expect(response.url()).toBe(server.EMPTY_PAGE);
await request.dispose();
});

it('should set playwright as user-agent', async ({ playwright, server, isWindows, isLinux, isMac }) => {
Expand All @@ -221,12 +244,14 @@ it('should set playwright as user-agent', async ({ playwright, server, isWindows
expect(userAgentMasked.replace(/<ARCH>; \w+ [^)]+/, '<ARCH>; distro version')).toBe('Playwright/X.X.X (<ARCH>; distro version) node/X.X' + suffix);
else if (isMac)
expect(userAgentMasked).toBe('Playwright/X.X.X (<ARCH>; macOS X.X) node/X.X' + suffix);
await request.dispose();
});

it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
const request = await playwright.request.newContext((browserType as any)._defaultContextOptions);
const response = await request.get(server.EMPTY_PAGE);
expect(response.ok()).toBeTruthy();
await request.dispose();
});

it('should return empty body', async ({ playwright, server }) => {
Expand Down Expand Up @@ -254,6 +279,7 @@ it('should abort requests when context is disposed', async ({ playwright, server
expect(result.message).toContain(kTargetClosedErrorMessage);
}
await connectionClosed;
await request.dispose();
});

it('should abort redirected requests when context is disposed', async ({ playwright, server }) => {
Expand All @@ -269,6 +295,7 @@ it('should abort redirected requests when context is disposed', async ({ playwri
expect(result instanceof Error).toBeTruthy();
expect(result.message).toContain(kTargetClosedErrorMessage);
await connectionClosed;
await request.dispose();
});

it('should remove content-length from redirected post requests', async ({ playwright, server }) => {
Expand Down Expand Up @@ -473,7 +500,6 @@ it('should serialize post data on the client', async ({ playwright, server }) =>
await postReq;
const body = await (await serverReq).postBody;
expect(body.toString()).toBe('{"foo":"bar"}');
// expect(serverRequest.rawHeaders).toContain('vaLUE');
await request.dispose();
});

Expand All @@ -486,7 +512,8 @@ it('should throw after dispose', async ({ playwright, server }) => {

it('should retry ECONNRESET', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
}, async ({ context, server }) => {
}, async ({ playwright, server }) => {
const request = await playwright.request.newContext();
let requestCount = 0;
server.setRoute('/test', (req, res) => {
if (requestCount++ < 3) {
Expand All @@ -496,8 +523,9 @@ it('should retry ECONNRESET', {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('Hello!');
});
const response = await context.request.fetch(server.PREFIX + '/test', { maxRetries: 3 });
const response = await request.fetch(server.PREFIX + '/test', { maxRetries: 3 });
expect(response.status()).toBe(200);
expect(await response.text()).toBe('Hello!');
expect(requestCount).toBe(4);
await request.dispose();
});
12 changes: 10 additions & 2 deletions utils/doclint/generateDotnetApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,18 @@ function generateNameDefault(member, name, t, parent) {
if (names[2] === names[1])
names.pop(); // get rid of duplicates, cheaply
let attemptedName = names.pop();
const typesDiffer = function(left, right) {
const typesDiffer = function(/** @type {Documentation.Type} */ left, /** @type {Documentation.Type} */ right) {
if (left.expression && right.expression)
return left.expression !== right.expression;
return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
const toExpression = (/** @type {Documentation.Member} */ t) => t.name + t.type?.expression;
const leftOverRightProperties = new Set(left.properties?.map(toExpression) ?? []);
for (const prop of right.properties ?? []) {
const expression = toExpression(prop);
if (!leftOverRightProperties.has(expression))
return true;
leftOverRightProperties.delete(expression);
}
return leftOverRightProperties.size > 0;
};
while (true) {
// crude attempt at removing plurality
Expand Down

0 comments on commit 5518720

Please sign in to comment.