Skip to content

Commit

Permalink
Add dynamic mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
danielstclair committed Jan 17, 2024
1 parent 1d5d447 commit b7fd6c7
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 111 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Playwright Tests

on:
push:
branches:
- 08-dynamic-mocking
schedule:
- cron: "* 8-15/4 * * *"
workflow_dispatch:
Expand Down
135 changes: 134 additions & 1 deletion playwright/helpers/mock.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,134 @@
export const MOCK_DIR = './playwright/mocks'
import { Page } from '@playwright/test'
import fs from 'fs'
import path from 'path'

export const MOCK_DIR = './playwright/mocks'

export const fileExists = (filePath: string): boolean => fs.existsSync(filePath)

type Content = {
size: number
mimeType: string
_file?: string
}
type HAREntry = {
[key: string]: {
status: number
content: Content
}
}
export class Mock {
private page: Page
private basePath: string
private subDirectory: string
private routeMap: HAREntry

constructor(page: Page, basePath: string) {
this.page = page
this.basePath = basePath
this.subDirectory = ''
this.routeMap = {}
}

private getUrlEndpoint = (url: string): string => url.split('/api/')[1]
private readFile = (...paths: string[]): string => fs.readFileSync(path.join(process.cwd(), ...paths), 'utf8')

private getHAREntries = (harPath: string) => {
const har = this.readFile(this.basePath, harPath)
const harJson = JSON.parse(har)
const harEntries = harJson.log.entries
this.routeMap = harEntries.reduce((acc: any, entry: any) => {
const url = this.getUrlEndpoint(entry.request.url)
const method = entry.request.method
const requestKey = `${method}-${url}`
if (!acc[requestKey]) {
const { status, content } = entry.response
const response = {
status: status || 200,
content
}
acc[requestKey] = response
}
return acc
}, {})
}

private getContent = (content: Content): string | undefined => {
if (content?._file) {
const contentString = this.readFile(this.basePath, this.subDirectory, content._file)
return JSON.parse(contentString)
}
}

private getDataFromHAR = () => {
const routes = Object.keys(this.routeMap).map((entryKey: string) => {
const [method, url] = entryKey.split('-')
const { status, content } = this.routeMap[entryKey]
return {
method,
url,
status,
content: this.getContent(content)
}
})
return routes
}

private convertHarToRouteMock = async (harPath: string) => {
// create routeMap from HAR entries
this.getHAREntries(harPath)

// create routes from routeMap
await Promise.all(
this.getDataFromHAR().map(async route => {
if (route.status && route.content) {
await this.page.route(`**/api/${route.url}`, async api => {
console.log('route status: ', route.status)
if (route.status === -1) return
console.log('*************** hitting route: ', api.request().url())
await api.fulfill({
status: route.status,
body: JSON.stringify(route.content)
})
})
}
})
)
}

private getSubDirectory = (outputFile: string): string => {
const subDirectory = outputFile.split('/').slice(0, -1).join('/')
return subDirectory
}

route = async (config: { outputFile: string; url: string; forceUpdate?: boolean }) => {
const { outputFile, url, forceUpdate } = config

if (!process.env.CI) {
console.log('running locally')
const harPath = path.join(this.basePath, outputFile)
return this.page.routeFromHAR(harPath, {
url,
update: forceUpdate || !fileExists(harPath)
})
}

console.log('running in CI')
this.subDirectory = this.getSubDirectory(outputFile)
await this.convertHarToRouteMock(outputFile)
}

unroute = async (url: string) => {
if (process.env.CI) {
await Promise.all(
Object.keys(this.routeMap).map(async (route: string) => {
console.log('unrouting: ', route)
const [_, url] = route.split('-')
await this.page.unroute(url)
})
)
} else {
await this.page.unroute(url)
}
}
}
10 changes: 8 additions & 2 deletions playwright/helpers/test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { test as base, expect as baseExpect, Page } from '@playwright/test';
import { Mock, MOCK_DIR } from './mock';

export const test = base.extend<{ rootURL: string }>({
rootURL: process.env.CI ? '/playwright-workshop' : '/'
export const test = base.extend<{ rootURL: string, mock: Mock, MOCK_DIR: string }>({
rootURL: process.env.CI ? '/playwright-workshop' : '/',
MOCK_DIR,
mock: async ({ page, MOCK_DIR }, use) => {
const mock = new Mock(page, MOCK_DIR);
await use(mock);
}
});
114 changes: 102 additions & 12 deletions playwright/mocks/home/auth.har
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,131 @@
},
"entries": [
{
"startedDateTime": "2024-01-17T21:02:00.406Z",
"time": -1,
"startedDateTime": "2024-01-17T21:35:05.524Z",
"time": 1.46,
"request": {
"method": "GET",
"url": "https://theofficeapi.dev/api/character/55",
"httpVersion": "HTTP/1.1",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{ "name": ":authority", "value": "theofficeapi.dev" },
{ "name": ":method", "value": "GET" },
{ "name": ":path", "value": "/api/character/55" },
{ "name": ":scheme", "value": "https" },
{ "name": "accept", "value": "*/*" },
{ "name": "accept-encoding", "value": "gzip, deflate, br" },
{ "name": "accept-language", "value": "en-US" },
{ "name": "origin", "value": "http://localhost:3000" },
{ "name": "referer", "value": "http://localhost:3000/" },
{ "name": "sec-ch-ua", "value": "\"Not A(Brand\";v=\"99\", \"HeadlessChrome\";v=\"121\", \"Chromium\";v=\"121\"" },
{ "name": "Referer", "value": "http://localhost:3000/" },
{ "name": "Accept-Language", "value": "en-US" },
{ "name": "sec-ch-ua-mobile", "value": "?0" },
{ "name": "User-Agent", "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/121.0.6167.57 Safari/537.36" },
{ "name": "sec-ch-ua-platform", "value": "\"macOS\"" }
{ "name": "sec-ch-ua-platform", "value": "\"macOS\"" },
{ "name": "sec-fetch-dest", "value": "empty" },
{ "name": "sec-fetch-mode", "value": "cors" },
{ "name": "sec-fetch-site", "value": "cross-site" },
{ "name": "user-agent", "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/121.0.6167.57 Safari/537.36" }
],
"queryString": [],
"headersSize": -1,
"bodySize": -1
},
"response": {
"status": -1,
"status": 200,
"statusText": "",
"httpVersion": "HTTP/1.1",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [],
"headers": [
{ "name": "access-control-allow-origin", "value": "*" },
{ "name": "age", "value": "1" },
{ "name": "cache-status", "value": "\"Netlify Edge\"; fwd=miss" },
{ "name": "content-encoding", "value": "br" },
{ "name": "content-type", "value": "application/json; charset=UTF-8" },
{ "name": "date", "value": "Wed, 17 Jan 2024 21:35:06 GMT" },
{ "name": "fly-request-id", "value": "01HMCNJSFF075G3SHYKMA7A5SE-iad" },
{ "name": "netlify-vary", "value": "query,cookie=__next_preview_data:presence|__prerender_bypass:presence" },
{ "name": "server", "value": "Netlify" },
{ "name": "strict-transport-security", "value": "max-age=31536000" },
{ "name": "via", "value": "2 fly.io" },
{ "name": "x-nf-request-id", "value": "01HMCNJSFB9A4Z5HZ0ESQ5Y1XP" }
],
"content": {
"size": -1,
"mimeType": "application/json; charset=UTF-8",
"_file": "b379fca3e30868ae8a40f234e257fa0046c39b0f.json"
},
"headersSize": -1,
"bodySize": -1,
"redirectURL": ""
},
"cache": {},
"timings": { "send": -1, "wait": -1, "receive": 1.46 }
},
{
"startedDateTime": "2024-01-17T21:35:06.029Z",
"time": 6.632,
"request": {
"method": "GET",
"url": "https://theofficeapi.dev/api/characters?limit=100",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{ "name": ":authority", "value": "theofficeapi.dev" },
{ "name": ":method", "value": "GET" },
{ "name": ":path", "value": "/api/characters?limit=100" },
{ "name": ":scheme", "value": "https" },
{ "name": "accept", "value": "*/*" },
{ "name": "accept-encoding", "value": "gzip, deflate, br" },
{ "name": "accept-language", "value": "en-US" },
{ "name": "origin", "value": "http://localhost:3000" },
{ "name": "referer", "value": "http://localhost:3000/" },
{ "name": "sec-ch-ua", "value": "\"Not A(Brand\";v=\"99\", \"HeadlessChrome\";v=\"121\", \"Chromium\";v=\"121\"" },
{ "name": "sec-ch-ua-mobile", "value": "?0" },
{ "name": "sec-ch-ua-platform", "value": "\"macOS\"" },
{ "name": "sec-fetch-dest", "value": "empty" },
{ "name": "sec-fetch-mode", "value": "cors" },
{ "name": "sec-fetch-site", "value": "cross-site" },
{ "name": "user-agent", "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/121.0.6167.57 Safari/537.36" }
],
"queryString": [
{
"name": "limit",
"value": "100"
}
],
"headersSize": -1,
"bodySize": -1
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{ "name": "access-control-allow-origin", "value": "*" },
{ "name": "age", "value": "0" },
{ "name": "cache-status", "value": "\"Netlify Edge\"; fwd=miss" },
{ "name": "content-encoding", "value": "br" },
{ "name": "content-type", "value": "application/json; charset=UTF-8" },
{ "name": "date", "value": "Wed, 17 Jan 2024 21:35:06 GMT" },
{ "name": "fly-request-id", "value": "01HMCNJSVX7CYYY1XNT7PDG26Y-iad" },
{ "name": "netlify-vary", "value": "query,cookie=__next_preview_data:presence|__prerender_bypass:presence" },
{ "name": "server", "value": "Netlify" },
{ "name": "strict-transport-security", "value": "max-age=31536000" },
{ "name": "via", "value": "2 fly.io" },
{ "name": "x-nf-request-id", "value": "01HMCNJSVWQ883YJS6068W6THZ" }
],
"content": {
"size": -1,
"mimeType": "x-unknown"
"mimeType": "application/json; charset=UTF-8",
"_file": "b83db4f6ed6b666278056edde75228a88e1ffc4e.json"
},
"headersSize": -1,
"bodySize": -1,
"redirectURL": ""
},
"cache": {},
"timings": { "send": -1, "wait": -1, "receive": -1 }
"timings": { "send": -1, "wait": -1, "receive": 6.632 }
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":55,"name":"Michael Scott","gender":"Male","marital":"Holly Flax","job":["Regional Manager","Co-Regional Manager (former)","Salesman (former)","Founder and CEO (former)","Shareholder (former)","Cutlery Salesman (former)","Greeter (former)","Telemarketer (former)"],"workplace":["Dunder Mifflin Scranton","The Michael Scott Paper Company","WUPHF.com (Website)","Arby's","Men's Warehouse","Lipophedrine Diet Pill Company"],"firstAppearance":"Pilot","lastAppearance":"Finale","actor":"Steve Carell"}
Loading

0 comments on commit b7fd6c7

Please sign in to comment.