Skip to content

Commit

Permalink
Merge pull request #10 from cloudnc/feat/mock-route-at-context-level
Browse files Browse the repository at this point in the history
feat(playwright): mock route at context level
  • Loading branch information
maxime1992 authored Jun 28, 2023
2 parents 7efc91d + 9606674 commit fed66fe
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 91 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
build:
# Machine environment:
# https://help.github.com/en/articles/software-in-virtual-environments-for-github-actions#ubuntu-1804-lts
# We specify the Node.js version manually below, and use versioned Chrome from Puppeteer.
runs-on: ubuntu-18.04
# We specify the Node.js version manually below
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v1
- name: Use Node.js 16.14.2
uses: actions/setup-node@v1
- uses: actions/checkout@v3
- name: Use Node.js 18.4.0
uses: actions/setup-node@v3
with:
node-version: 16.14.2
node-version: 18.4.0
- name: Install dependencies
run: yarn --frozen-lockfile --non-interactive --no-progress
- name: Format check
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16.14.2
v18.4.0
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"htmlWhitespaceSensitivity": "ignore",
"tabWidth": 2
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
"dependencies": {
"@grpc/grpc-js": "1.6.10",
"@improbable-eng/grpc-web": "0.15.0",
"@playwright/test": "1.22.2",
"@playwright/test": "1.35.1",
"google-protobuf": "3.14.0",
"playwright-core": "1.22.2"
"playwright-core": "1.35.1"
},
"devDependencies": {
"prettier": "^2.7.1",
"prettier": "2.8.8",
"semantic-release": "19.0.2",
"typescript": "4.7.2"
}
Expand Down
22 changes: 10 additions & 12 deletions src/base/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Metadata, status as Status } from "@grpc/grpc-js";
import { Metadata, status as Status } from '@grpc/grpc-js';

export interface GrpcErrorResponse {
status: Status;
Expand All @@ -22,7 +22,7 @@ function fourBytesLength(sized: { length: number }): Uint8Array {

export function decodeGrpcWebBody(bodyBuffer: Buffer): GrpcResponse {
if (bodyBuffer.length === 0) {
throw new Error("Body has zero length, cannot decode!");
throw new Error('Body has zero length, cannot decode!');
}

const bodyRaw = new Uint8Array(bodyBuffer);
Expand Down Expand Up @@ -53,7 +53,7 @@ export function decodeGrpcWebBody(bodyBuffer: Buffer): GrpcResponse {
const trailersHeader = 0x80;

if (bodyRaw.at(offset++) !== trailersHeader) {
throw new Error("Expected trailers header 0x80");
throw new Error('Expected trailers header 0x80');
}

const trailersLength = readInt32Length(bodyRaw, offset);
Expand All @@ -66,16 +66,16 @@ export function decodeGrpcWebBody(bodyBuffer: Buffer): GrpcResponse {

const trailers = new Metadata();

trailersString.split("\r\n").forEach((trailer) => {
const [key, value] = trailer.split(":", 2);
trailersString.split('\r\n').forEach((trailer) => {
const [key, value] = trailer.split(':', 2);
trailers.set(key, value);
});

if (status !== Status.OK) {
return {
status,
trailers,
detail: trailers.get("grpc-message")[0] as string | undefined,
detail: trailers.get('grpc-message')[0] as string | undefined,
};
}

Expand All @@ -99,19 +99,17 @@ export class GrpcUnknownStatus extends Error {

export function grpcResponseToBuffer(response: GrpcResponse): Buffer {
// error messages need to have a zero length message field to be considered valid
const message = "message" in response ? response.message : new Uint8Array();
const message = 'message' in response ? response.message : new Uint8Array();

// all success responses have status OK
const status = "status" in response ? response.status : Status.OK;
const status = 'status' in response ? response.status : Status.OK;
// error statuses may the detail field to denote a custom error message, otherwise use the string version of the status
let grpcMessage: string | undefined;

if ("detail" in response) {
if ('detail' in response) {
grpcMessage = response.detail;
} else {
const currentStatus = Object.entries(Status).find(
([, code]) => code === status
);
const currentStatus = Object.entries(Status).find(([, code]) => code === status);

if (!currentStatus) {
throw new GrpcUnknownStatus(status);
Expand Down
8 changes: 4 additions & 4 deletions src/playwright/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { observeGrpcUnary } from "./observe-grpc-unary";
export { mockGrpcUnary } from "./mock-grpc-unary";
export { readGrpcRequest } from "./read-grpc-request";
export * from "./interfaces";
export { observeGrpcUnary } from './observe-grpc-unary';
export { mockGrpcUnary } from './mock-grpc-unary';
export { readGrpcRequest } from './read-grpc-request';
export * from './interfaces';
22 changes: 7 additions & 15 deletions src/playwright/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { grpc } from "@improbable-eng/grpc-web";
import { Request } from "playwright-core";
import { status as Status, Metadata } from "@grpc/grpc-js";
import { grpc } from '@improbable-eng/grpc-web';
import { Request } from 'playwright-core';
import { status as Status, Metadata } from '@grpc/grpc-js';

export interface UnaryMethodDefinitionish
extends grpc.UnaryMethodDefinition<any, any> {
export interface UnaryMethodDefinitionish extends grpc.UnaryMethodDefinition<any, any> {
requestStream: any;
responseStream: any;
}

export type RequestPredicate = (
requestMessage: Uint8Array | null,
request: Request
) => boolean | Promise<boolean>;
export type RequestPredicate = (requestMessage: Uint8Array | null, request: Request) => boolean | Promise<boolean>;

export interface MockedGrpcCall {
/**
Expand All @@ -21,9 +17,7 @@ export interface MockedGrpcCall {
* The request message argument to the optional predicate should be used to match the request payload.
* Note the requestMessage objects need to be decoded using a protobuf decoder for the specific expected message.
*/
waitForMock(
requestPredicate?: RequestPredicate
): Promise<{ requestMessage: Uint8Array | null }>;
waitForMock(requestPredicate?: RequestPredicate): Promise<{ requestMessage: Uint8Array | null }>;
}

export interface ObservedGrpcCallResponse {
Expand All @@ -34,7 +28,5 @@ export interface ObservedGrpcCallResponse {
}

export interface ObservedGrpcCall {
waitForResponse: (
requestPredicate?: RequestPredicate
) => Promise<ObservedGrpcCallResponse>;
waitForResponse: (requestPredicate?: RequestPredicate) => Promise<ObservedGrpcCallResponse>;
}
27 changes: 11 additions & 16 deletions src/playwright/mock-grpc-unary.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
import { expect, Page } from "@playwright/test";
import { GrpcResponse, grpcResponseToBuffer } from "../base";
import { MockedGrpcCall, UnaryMethodDefinitionish } from "./interfaces";
import { readGrpcRequest } from "./read-grpc-request";
import { expect, Page } from '@playwright/test';
import { GrpcResponse, grpcResponseToBuffer } from '../base';
import { MockedGrpcCall, UnaryMethodDefinitionish } from './interfaces';
import { readGrpcRequest } from './read-grpc-request';

export async function mockGrpcUnary(
page: Page,
rpc: UnaryMethodDefinitionish,
response: GrpcResponse | ((request: Uint8Array | null) => GrpcResponse)
response: GrpcResponse | ((request: Uint8Array | null) => GrpcResponse),
mockAtContextLevel: boolean = false,
): Promise<MockedGrpcCall> {
const url = `/${rpc.service.serviceName}/${rpc.methodName}`;

// note this wildcard route url base is done in order to match both localhost and deployed service usages.
await page.route("**" + url, (route) => {
expect(
route.request().method(),
"ALL gRPC requests should be a POST request"
).toBe("POST");
await (mockAtContextLevel ? page.context() : page).route('**' + url, (route) => {
expect(route.request().method(), 'ALL gRPC requests should be a POST request').toBe('POST');

const grpcResponse =
typeof response === "function"
? response(readGrpcRequest(route.request()))
: response;
const grpcResponse = typeof response === 'function' ? response(readGrpcRequest(route.request())) : response;

const grpcResponseBody = grpcResponseToBuffer(grpcResponse);

return route.fulfill({
body: grpcResponseBody,
contentType: "application/grpc-web+proto",
contentType: 'application/grpc-web+proto',
headers: {
"Access-Control-Allow-Origin": "*",
'Access-Control-Allow-Origin': '*',
},
});
});
Expand Down
29 changes: 10 additions & 19 deletions src/playwright/observe-grpc-unary.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { Page } from "@playwright/test";
import {
ObservedGrpcCall,
RequestPredicate,
UnaryMethodDefinitionish,
} from "./interfaces";
import { decodeGrpcWebBody } from "../base";
import { status as Status } from "@grpc/grpc-js";
import { readGrpcRequest } from "./read-grpc-request";

export async function observeGrpcUnary(
page: Page,
rpc: UnaryMethodDefinitionish
): Promise<ObservedGrpcCall> {
import { Page } from '@playwright/test';
import { ObservedGrpcCall, RequestPredicate, UnaryMethodDefinitionish } from './interfaces';
import { decodeGrpcWebBody } from '../base';
import { status as Status } from '@grpc/grpc-js';
import { readGrpcRequest } from './read-grpc-request';

export async function observeGrpcUnary(page: Page, rpc: UnaryMethodDefinitionish): Promise<ObservedGrpcCall> {
const url = `/${rpc.service.serviceName}/${rpc.methodName}`;

// note this wildcard route url base is done in order to match both localhost and deployed service usages.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
await page.route("**" + url, async (route) => await route.continue());
await page.route('**' + url, async (route) => await route.continue());

return {
async waitForResponse(requestPredicate?: RequestPredicate) {
Expand All @@ -33,17 +26,15 @@ export async function observeGrpcUnary(
return true;
});

const response = await page.waitForResponse(
(resp) => resp.request() === request
);
const response = await page.waitForResponse((resp) => resp.request() === request);

const requestMessage = readGrpcRequest(request);

const responseParsed = await decodeGrpcWebBody(await response.body());

const trailers = responseParsed.trailers ?? null;

if ("status" in responseParsed) {
if ('status' in responseParsed) {
return {
requestMessage,
responseMessage: null,
Expand Down
4 changes: 2 additions & 2 deletions src/playwright/read-grpc-request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request } from "playwright-core";
import { unframeRequest } from "../base";
import { Request } from 'playwright-core';
import { unframeRequest } from '../base';

export function readGrpcRequest(request: Request): Uint8Array | null {
const requestBody = request.postDataBuffer();
Expand Down
33 changes: 20 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,15 @@
dependencies:
"@octokit/openapi-types" "^11.2.0"

"@playwright/test@1.22.2":
version "1.22.2"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.22.2.tgz#b848f25f8918140c2d0bae8e9227a40198f2dd4a"
integrity sha512-cCl96BEBGPtptFz7C2FOSN3PrTnJ3rPpENe+gYCMx4GNNDlN4tmo2D89y13feGKTMMAIVrXfSQ/UmaQKLy1XLA==
"@playwright/test@1.35.1":
version "1.35.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c"
integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==
dependencies:
"@types/node" "*"
playwright-core "1.22.2"
playwright-core "1.35.1"
optionalDependencies:
fsevents "2.3.2"

"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
Expand Down Expand Up @@ -1282,6 +1284,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
Expand Down Expand Up @@ -2689,15 +2696,15 @@ pkg-conf@^2.1.0:
find-up "^2.0.0"
load-json-file "^4.0.0"

playwright-core@1.22.2:
version "1.22.2"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a"
integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q==
playwright-core@1.35.1:
version "1.35.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d"
integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==

prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
prettier@2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==

proc-log@^2.0.0, proc-log@^2.0.1:
version "2.0.1"
Expand Down

0 comments on commit fed66fe

Please sign in to comment.