Skip to content

Commit

Permalink
feat: QA-126 Add compability with playwright
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszfiszer committed Apr 17, 2020
1 parent b247ea5 commit 9970eaf
Show file tree
Hide file tree
Showing 25 changed files with 745 additions and 345 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist
node_modules
reports
coverage
*.log
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
puppeteer_download_host=https://nexus.hltech.dev/repository/binaries
playwright_download_host=https://nexus.hltech.dev/repository/binaries
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mocketeer

Mocketeer is HTTP request mocking library for [Puppeteer](http://pptr.dev/). It was created to enable effective testing of Single Page Apps in isolation and independently from API services.
Mocketeer is HTTP request mocking library for [Puppeteer](http://pptr.dev/) and [Playwright](https://github.com/microsoft/playwright/). It was created to enable effective testing of Single Page Apps in isolation and independently from API services.

## Main features

Expand Down Expand Up @@ -38,12 +38,12 @@ Mocketeer is HTTP request mocking library for [Puppeteer](http://pptr.dev/). It
yarn add @hltech/mocketeer
```

- Mocketeer requires [Puppeteer](https://pptr.dev/) which need to be installed separately
- If you're using [jest](jestjs.io/) we also recommend to install [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer)
- Mocketeer requires [Puppeteer](https://pptr.dev/) or [Playwright](https://www.npmjs.com/package/playwright/) which need to be installed separately
- If you're using [jest](jestjs.io/) we also recommend to install [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) or [jest-playwright](https://www.npmjs.com/package/jest-playwright-preset)

## Getting started <a name="getting-started"/>

To start using Mocketeer, you need to instantiate `Mocketeer` class by providing it an instance of [Puppeteer Page](https://pptr.dev/#?product=Puppeteer&show=api-class-page).
To start using Mocketeer, you need to instantiate `Mocketeer` class by providing it an instance of [Puppeteer Page](https://pptr.dev/#?product=Puppeteer&show=api-class-page) or [Playwright Page](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-page)

```typescript
const { Mocketeer } = require('@hltech/mocketeer');
Expand Down Expand Up @@ -294,15 +294,15 @@ await Mocketeer.setup(page, {debug: true});

#### `Mocketeer.setup(page, options): Promise<Mocketeer>`

Factory method used to set-up request mocking on provided Puppeteer Page. It creates and returns an instance of Mocketeer
Factory method used to set-up request mocking on provided Puppeteer or Playwright Page. It creates and returns an instance of Mocketeer

Once created, mocketeer will intercept all requests made by the page and match them with defined mocks.

If request does not match any mocks, it will be responded with `404 Not Found`.

###### Arguments

- `page` _(Page)_ instance of Puppeteer [Page](https://pptr.dev/#?product=Puppeteer&show=api-class-page)
- `page` _(Page)_ instance of [Puppeteer Page](https://pptr.dev/#?product=Puppeteer&show=api-class-page) or [Playwright Page](https://github.com/microsoft/playwright/blob/master/docs/api.md#class-page)
- `options` _(object)_ configuration options
- `debug: boolean` turns debug mode with logging to console (default: `false`)

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@
"@types/jest": "^24.0.11",
"@types/lodash.isequal": "^4.5.5",
"@types/puppeteer": "^1.12.3",
"factory.ts": "^0.5.1",
"husky": "^1.3.1",
"jest": "^24.5.0",
"jest-junit": "^6.3.0",
"playwright": "0.11",
"prettier": "^1.16.4",
"pretty-quick": "^1.10.0",
"puppeteer": "1.13",
"semantic-release": "^15.13.3",
"ts-jest": "^24.0.1",
"typescript": "^3.6.3"
"typescript": "^3.8.3"
},
"files": [
"dist"
Expand Down
51 changes: 51 additions & 0 deletions src/controllers/BrowserController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Interface through with Mocketeer communicates with different browser automation libraries
*/
export interface BrowserController {
startInterception(onRequest: BrowserRequestHandler): Promise<void>;
}

/**
* Callback function called whenever a request is intercepted in the browser
*/
export type BrowserRequestHandler = (
request: BrowserRequest,
respond: (response: ResponseData) => Promise<void>,
skip: () => void
) => void;

/**
* Data of intercepted browser request
*/
export interface BrowserRequest {
type: BrowserRequestType;
method: string;
url: string;
headers: Record<string, string>;
body: any;
path: string;
hostname: string;
query: Record<string, string | string[]>;
sourceOrigin: string;
}

export interface ResponseData {
status: number;
body?: Buffer | string;
headers?: Record<string, string>;
}

export type BrowserRequestType =
| 'document'
| 'stylesheet'
| 'image'
| 'media'
| 'font'
| 'script'
| 'texttrack'
| 'xhr'
| 'fetch'
| 'eventsource'
| 'websocket'
| 'manifest'
| 'other';
34 changes: 34 additions & 0 deletions src/controllers/BrowserControllerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as puppeteer from 'puppeteer';
import playwright from 'playwright';
import { PlaywrightController } from './PlaywrightController';
import { PuppeteerController } from './PuppeteerController';
import { BrowserController } from './BrowserController';

export type BrowserPage = puppeteer.Page | playwright.Page;

export class BrowserControllerFactory {
/**
* Return instantiated Playwright or Puppeteer controller
* corresponding to provided page object
*/
public static getForPage(page: BrowserPage): BrowserController {
if (this.isPlaywrightPage(page)) {
return new PlaywrightController(page);
} else if (this.isPuppeteerPage(page)) {
return new PuppeteerController(page);
} else {
throw new Error(
'Expected instance of Puppeteer or Playwright Page. Got: ' +
page
);
}
}

private static isPlaywrightPage(page: any): page is playwright.Page {
return typeof page['route'] === 'function';
}

private static isPuppeteerPage(page: any): page is puppeteer.Page {
return typeof page['setRequestInterception'] === 'function';
}
}
65 changes: 65 additions & 0 deletions src/controllers/PlaywrightController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import playwright from 'playwright';
import {
BrowserController,
BrowserRequestHandler,
ResponseData,
BrowserRequest,
BrowserRequestType,
} from './BrowserController';
import { tryJsonParse } from '../utils';
import { parse } from 'url';

export class PlaywrightController implements BrowserController {
constructor(private readonly page: playwright.Page) {}

async startInterception(onRequest: BrowserRequestHandler) {
this.page.route('**/*', (request: playwright.Request) => {
onRequest(
this.toBrowserRequest(request),
data => this.respond(request, data),
() => request.continue()
);
});
}

private toBrowserRequest(request: playwright.Request): BrowserRequest {
// TODO find a better alternative for url.parse
const { pathname = '', query, protocol, host } = parse(
request.url(),
true
);

return {
type: request.resourceType() as BrowserRequestType,
url: request.url(),
body: tryJsonParse(request.postData()),
method: request.method(),
headers: request.headers() || {},
path: pathname,
hostname: `${protocol}//${host}`,
query: query,
sourceOrigin: this.getRequestOrigin(request),
};
}

private async respond(request: playwright.Request, response: ResponseData) {
await request.fulfill({
headers: response.headers || {},
status: response.status,
body: response.body ? response.body : '',
contentType: response.headers
? response.headers['content-type']
: 'application/json',
});
}

/**
* Obtain request origin url from originating frame url
*/
private getRequestOrigin(request: playwright.Request) {
const originFrame = request.frame();
const originFrameUrl = originFrame ? originFrame.url() : '';
const { protocol, host } = parse(originFrameUrl);
return `${protocol}//${host}`;
}
}
53 changes: 53 additions & 0 deletions src/controllers/PuppeteerController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Page, Request } from 'puppeteer';
import { parse } from 'url';
import {
BrowserController,
BrowserRequest,
BrowserRequestHandler,
} from './BrowserController';
import { tryJsonParse } from '../utils';

export class PuppeteerController implements BrowserController {
constructor(private readonly page: Page) {}

async startInterception(onRequest: BrowserRequestHandler) {
await this.page.setRequestInterception(true);
this.page.on('request', request => {
onRequest(
this.getRequestData(request),
response => request.respond(response),
() => request.continue()
);
});
}

private getRequestData(request: Request): BrowserRequest {
// TODO find a better alternative for url.parse
const { pathname = '', query, protocol, host } = parse(
request.url(),
true
);

return {
type: request.resourceType(),
url: request.url(),
body: tryJsonParse(request.postData()),
method: request.method(),
headers: request.headers() || {},
path: pathname,
hostname: `${protocol}//${host}`,
query: query,
sourceOrigin: this.getRequestOrigin(request),
};
}

/**
* Obtain request origin url from originating frame url
*/
private getRequestOrigin(request: Request) {
const originFrame = request.frame();
const originFrameUrl = originFrame ? originFrame.url() : '';
const { protocol, host } = parse(originFrameUrl);
return `${protocol}//${host}`;
}
}
18 changes: 5 additions & 13 deletions src/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,12 @@ import {
RequestMatcher,
PathParameters,
} from './types';
import {
waitFor,
TimeoutError,
nth,
requestToPlainObject,
getRequestOrigin,
} from './utils';
import { waitFor, TimeoutError, nth } from './utils';
import isEqual from 'lodash.isequal';
import { parse } from 'url';
import { stringify } from 'querystring';
import { Request } from 'puppeteer';
import { match, MatchFunction } from 'path-to-regexp';
import { BrowserRequest } from './controllers/BrowserController';

const debug = dbg('mocketeer:rest');

Expand Down Expand Up @@ -117,7 +111,7 @@ export class Mock {
}

public getResponseForRequest(
request: Request
request: BrowserRequest
): MockedResponseObject | null {
if (this.options.once && this.requests.length > 0) {
this.debug('once', 'Request already matched');
Expand All @@ -140,16 +134,14 @@ export class Mock {
return response;
}

private getRequestMatch(rawRequest: Request): MatchedRequest | null {
const request = requestToPlainObject(rawRequest);
const pageOrigin = getRequestOrigin(rawRequest);
private getRequestMatch(request: BrowserRequest): MatchedRequest | null {

if (request.method !== this.matcher.method) {
this.debugMiss('method', request.method, this.matcher.method);
return null;
}

const matcherOrigin = this.matcher.hostname || pageOrigin;
const matcherOrigin = this.matcher.hostname || request.sourceOrigin;

if (matcherOrigin !== request.hostname) {
this.debugMiss(
Expand Down
Loading

0 comments on commit 9970eaf

Please sign in to comment.