From a553d635ba15c0f688b1ac6e61e0f29086edecf8 Mon Sep 17 00:00:00 2001 From: Maxime Robert Date: Wed, 28 Jun 2023 10:40:09 +0200 Subject: [PATCH 1/4] feat(playwright): mock route at context level This is useful to start mocking before the page is loaded --- src/playwright/mock-grpc-unary.ts | 56 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/playwright/mock-grpc-unary.ts b/src/playwright/mock-grpc-unary.ts index 4725543..3aabfbb 100644 --- a/src/playwright/mock-grpc-unary.ts +++ b/src/playwright/mock-grpc-unary.ts @@ -1,37 +1,41 @@ -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 { 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"); - - const grpcResponse = - typeof response === "function" - ? response(readGrpcRequest(route.request())) - : response; - - const grpcResponseBody = grpcResponseToBuffer(grpcResponse); - - return route.fulfill({ - body: grpcResponseBody, - contentType: "application/grpc-web+proto", - headers: { - "Access-Control-Allow-Origin": "*", - }, - }); - }); + 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 grpcResponseBody = grpcResponseToBuffer(grpcResponse); + + return route.fulfill({ + body: grpcResponseBody, + contentType: 'application/grpc-web+proto', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }); + } + ); return { async waitForMock(requestPredicate?) { From 542a7c76b9da8579d9d20425edf809f6966f03fd Mon Sep 17 00:00:00 2001 From: Maxime Robert Date: Wed, 28 Jun 2023 10:46:53 +0200 Subject: [PATCH 2/4] chore(CI): fix usage of old node version resulting in infinitely pending pipeline --- .github/workflows/main.yml | 12 ++++++------ .nvmrc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cd67bf..cc17406 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.nvmrc b/.nvmrc index 6276cf1..e8d9540 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.14.2 +v18.4.0 \ No newline at end of file From d827fa16b42c04bfcdd6b6b1884e831892f09889 Mon Sep 17 00:00:00 2001 From: Maxime Robert Date: Wed, 28 Jun 2023 10:50:20 +0200 Subject: [PATCH 3/4] chore: upgrade playwright as it's not compatible with the upgrade of nodejs --- package.json | 4 ++-- yarn.lock | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index e0bdca8..7348cb1 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "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", diff --git a/yarn.lock b/yarn.lock index 23779ed..88ef869 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -2689,10 +2696,10 @@ 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" From 960667474109ad4ac7c1b20f04b698caa6112237 Mon Sep 17 00:00:00 2001 From: Maxime Robert Date: Wed, 28 Jun 2023 11:00:01 +0200 Subject: [PATCH 4/4] chore: upgrade prettier --- .prettierrc | 7 +++++ package.json | 2 +- src/base/index.ts | 22 +++++++-------- src/playwright/index.ts | 8 +++--- src/playwright/interfaces.ts | 22 +++++---------- src/playwright/mock-grpc-unary.ts | 41 +++++++++++----------------- src/playwright/observe-grpc-unary.ts | 29 +++++++------------- src/playwright/read-grpc-request.ts | 4 +-- yarn.lock | 8 +++--- 9 files changed, 61 insertions(+), 82 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5af9608 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "htmlWhitespaceSensitivity": "ignore", + "tabWidth": 2 +} diff --git a/package.json b/package.json index 7348cb1..44730b4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "playwright-core": "1.35.1" }, "devDependencies": { - "prettier": "^2.7.1", + "prettier": "2.8.8", "semantic-release": "19.0.2", "typescript": "4.7.2" } diff --git a/src/base/index.ts b/src/base/index.ts index 96af9ec..3d63777 100644 --- a/src/base/index.ts +++ b/src/base/index.ts @@ -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; @@ -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); @@ -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); @@ -66,8 +66,8 @@ 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); }); @@ -75,7 +75,7 @@ export function decodeGrpcWebBody(bodyBuffer: Buffer): GrpcResponse { return { status, trailers, - detail: trailers.get("grpc-message")[0] as string | undefined, + detail: trailers.get('grpc-message')[0] as string | undefined, }; } @@ -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); diff --git a/src/playwright/index.ts b/src/playwright/index.ts index ec3764f..6b0de7e 100644 --- a/src/playwright/index.ts +++ b/src/playwright/index.ts @@ -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'; diff --git a/src/playwright/interfaces.ts b/src/playwright/interfaces.ts index 776d51c..bd95110 100644 --- a/src/playwright/interfaces.ts +++ b/src/playwright/interfaces.ts @@ -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 { +export interface UnaryMethodDefinitionish extends grpc.UnaryMethodDefinition { requestStream: any; responseStream: any; } -export type RequestPredicate = ( - requestMessage: Uint8Array | null, - request: Request -) => boolean | Promise; +export type RequestPredicate = (requestMessage: Uint8Array | null, request: Request) => boolean | Promise; export interface MockedGrpcCall { /** @@ -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 { @@ -34,7 +28,5 @@ export interface ObservedGrpcCallResponse { } export interface ObservedGrpcCall { - waitForResponse: ( - requestPredicate?: RequestPredicate - ) => Promise; + waitForResponse: (requestPredicate?: RequestPredicate) => Promise; } diff --git a/src/playwright/mock-grpc-unary.ts b/src/playwright/mock-grpc-unary.ts index 3aabfbb..60d663b 100644 --- a/src/playwright/mock-grpc-unary.ts +++ b/src/playwright/mock-grpc-unary.ts @@ -7,35 +7,26 @@ export async function mockGrpcUnary( page: Page, rpc: UnaryMethodDefinitionish, response: GrpcResponse | ((request: Uint8Array | null) => GrpcResponse), - mockAtContextLevel: boolean = false + mockAtContextLevel: boolean = false, ): Promise { 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 (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 grpcResponseBody = grpcResponseToBuffer(grpcResponse); - - return route.fulfill({ - body: grpcResponseBody, - contentType: 'application/grpc-web+proto', - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - } - ); + 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 grpcResponseBody = grpcResponseToBuffer(grpcResponse); + + return route.fulfill({ + body: grpcResponseBody, + contentType: 'application/grpc-web+proto', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }); + }); return { async waitForMock(requestPredicate?) { diff --git a/src/playwright/observe-grpc-unary.ts b/src/playwright/observe-grpc-unary.ts index 4e6267b..e6edf3e 100644 --- a/src/playwright/observe-grpc-unary.ts +++ b/src/playwright/observe-grpc-unary.ts @@ -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 { +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 { 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) { @@ -33,9 +26,7 @@ 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); @@ -43,7 +34,7 @@ export async function observeGrpcUnary( const trailers = responseParsed.trailers ?? null; - if ("status" in responseParsed) { + if ('status' in responseParsed) { return { requestMessage, responseMessage: null, diff --git a/src/playwright/read-grpc-request.ts b/src/playwright/read-grpc-request.ts index c9efb54..a40e971 100644 --- a/src/playwright/read-grpc-request.ts +++ b/src/playwright/read-grpc-request.ts @@ -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(); diff --git a/yarn.lock b/yarn.lock index 88ef869..9d4b677 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2701,10 +2701,10 @@ playwright-core@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"