-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add functions and documentation to testing/e2e
- Loading branch information
Svana
committed
Nov 1, 2024
1 parent
d0c9471
commit f723214
Showing
20 changed files
with
2,593 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,3 +98,8 @@ apps/**/index.html | |
|
||
.nx/ | ||
.zed/ | ||
|
||
# E2E outputs | ||
test-results/ | ||
playwright-report/ | ||
tmp-sessions/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,48 @@ | ||
# libs/testing/e2e | ||
# E2E Testing | ||
|
||
This library was generated with [Nx](https://nx.dev). | ||
This library was generated with [Nx](https://nx.dev). It contains utility functions and configuration files that assist with end-to-end (E2E) testing in Playwright for various apps. | ||
|
||
## Running unit tests | ||
## Overview | ||
|
||
Run `nx test libs/testing/e2e` to execute the unit tests via [Jest](https://jestjs.io). | ||
This library includes: | ||
|
||
- **Helper Functions:** Utility functions designed to streamline E2E testing with Playwright. These functions cater to different applications across the project and help automate common testing workflows. | ||
- **Global Playwright Configuration:** The `createGlobalConfig` function provides a shared Playwright configuration used across multiple applications. It standardizes the testing environment. | ||
|
||
## Mockoon Usage Guide for E2E Tests | ||
|
||
This section explains how to use [Mockoon](https://mockoon.com/) to set up mock APIs for end-to-end (e2e) testing. | ||
|
||
### What is Mockoon? | ||
|
||
[Mockoon](https://mockoon.com/) is an open-source tool for creating mock APIs quickly and easily. It allows developers to simulate backend servers without relying on live backend services. This is especially useful for e2e testing, where consistency and repeatability of backend responses are important. | ||
|
||
Mockoon provides both a graphical user interface (GUI) for managing API mock files and a command-line interface (CLI) for running these mocks in various environments, such as pipelines. | ||
|
||
### Opening an Existing Mock File in Mockoon | ||
|
||
To view or modify an existing mock file: | ||
|
||
1. Open Mockoon. | ||
2. Click on **+** and then click on **Open Local Environment**. | ||
3. Choose the desired mock file, such as `apps/<my-app>/e2e/mocks/<my-app-mock>.json`. | ||
|
||
This will load the mock configuration into the Mockoon UI, allowing you to inspect and edit the mock endpoints. | ||
|
||
### Creating a Mock File with Mockoon UI | ||
|
||
To create or modify a mock file: | ||
|
||
1. Download and install [Mockoon](https://mockoon.com/download/) if you haven't already. | ||
2. Open Mockoon and create a new environment: | ||
- Click on **+** and then click on **New Local Environment**. | ||
- Nema your mock file and choose a location for it e.g. `apps/<my-app>/e2e/mocks/<my-app-mock>.json`. | ||
- Add endpoints, routes, and response details as needed. | ||
|
||
### Running a Mockoon Server with the CLI | ||
|
||
To run a mock server with the cli, use the following command: | ||
|
||
```bash | ||
yarn mockoon-cli start --data ./apps/<my-app>/e2e/mocks/<my-app-mock>.json --port <port> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,13 @@ | ||
export * from './lib/libs/testing/e2e' | ||
export * from './lib/support/api-tools' | ||
export { createApplication } from './lib/support/application' | ||
export * from './lib/support/disablers' | ||
export * from './lib/support/email-account' | ||
export * from './lib/support/i18n' | ||
export * from './lib/support/locator-helpers' | ||
export * from './lib/support/login' | ||
export * from './lib/support/session' | ||
export * from './lib/support/urls' | ||
export * from './lib/support/utils' | ||
export * from './lib/utils/pageHelpers' | ||
export * from './lib/utils/playwright-config' | ||
export * from '@playwright/test' |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Page } from '@playwright/test' | ||
|
||
export const graphqlSpy = async ( | ||
page: Page, | ||
url: string, | ||
operation: string, | ||
) => { | ||
const data: { | ||
request: Record<string, unknown> | ||
response: Record<string, unknown> | ||
}[] = [] | ||
await page.route(url, async (route, req) => { | ||
const response = await page.request.fetch(req) | ||
if ( | ||
req.method() === 'POST' && | ||
req.postDataJSON().operationName === operation | ||
) { | ||
data.push({ | ||
request: req.postDataJSON(), | ||
response: await response.json(), | ||
}) | ||
} | ||
await route.fulfill({ response }) | ||
}) | ||
return { | ||
extractor: | ||
( | ||
fieldExtractor: (op: { | ||
request: Record<string, unknown> | ||
response: Record<string, unknown> | ||
}) => string, | ||
) => | ||
() => { | ||
const op = data[0] | ||
return op ? fieldExtractor(op) : '' | ||
}, | ||
data: ( | ||
fieldExtractor: (op: { | ||
request: Record<string, unknown> | ||
response: unknown | ||
}) => string, | ||
) => { | ||
const op = data[0] | ||
return op ? fieldExtractor(op) : '' | ||
}, | ||
} | ||
} | ||
|
||
export const mockApi = async ( | ||
page: Page, | ||
url: string, | ||
response: Record<string, unknown>, | ||
) => { | ||
await page.route(url, async (route, _req) => { | ||
await route.fulfill({ | ||
status: 200, | ||
body: JSON.stringify(response), | ||
contentType: 'application/json', | ||
}) | ||
}) | ||
} | ||
|
||
export const verifyRequestCompletion = async ( | ||
page: Page, | ||
url: string, | ||
op: string, | ||
) => { | ||
const response = await page.waitForResponse( | ||
(resp) => | ||
resp.url().includes(url) && | ||
resp.request().postDataJSON().operationName === op, | ||
) | ||
|
||
return await response.json() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Page } from '@playwright/test' | ||
|
||
/** | ||
Creates a new application and returns the number of applications before creation. | ||
@async | ||
@function | ||
@param {Page} page - Playwright Page object representing the current page. | ||
@returns {Promise<number>} - The number of applications before the new application is created. | ||
This function waits for the applications to load on the overview page and | ||
counts the number of applications. If there is an existing application, the | ||
overview page will not redirect to a new application. In this case, the function | ||
clicks the 'create-new-application' button to create a new application. | ||
*/ | ||
export const createApplication = async (page: Page) => { | ||
// Wait for the applications to load on the overview and count the number of applications | ||
const responsePromise = await page.waitForResponse( | ||
'**/api/graphql?op=ApplicationApplications', | ||
) | ||
const response = await responsePromise | ||
const responseData = await response.json() | ||
const numberOfApplications = | ||
responseData.data.applicationApplications.length || 0 | ||
// if there is an application, the overview won't redirect to a new application and we need | ||
// to click the button to create a new application | ||
if (numberOfApplications > 0) { | ||
await page.getByTestId('create-new-application').click() | ||
} | ||
return numberOfApplications | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { Page } from '@playwright/test' | ||
import mergeWith from 'lodash/merge' | ||
import camelCase from 'lodash/camelCase' | ||
import { debug } from './utils' | ||
|
||
const mergeOverwrite = (_: unknown, source: unknown) => { | ||
source | ||
} | ||
|
||
type Matchable = string | RegExp | ||
type MockGQLOptions = { | ||
responseKey?: string | ||
camelCaseResponseKey?: boolean | ||
patchResponse?: boolean | ||
deepMockKey?: Matchable // TODO type for this: | Matchable[] | ||
useResponseKey?: boolean | ||
pattern?: string | ||
} | ||
|
||
type Dict<T = unknown> = Record<string, T> | ||
/** | ||
* Return a copy of the `eroginal` object with any sub-objects mocked as `mockData` | ||
*/ | ||
const deepMock = <T = Dict>( | ||
original: T | T[], | ||
mockKey: Matchable, | ||
mockData: unknown = {}, | ||
{ exactMatch = false, deepPath = 'data' } = {}, | ||
): T | T[] | Dict | Dict[] => { | ||
if (Array.isArray(original)) { | ||
debug('Deep mocking array:', original) | ||
// Should do the typing properly here :/ | ||
return original.map( | ||
(item: T) => deepMock(item, mockKey, mockData, { exactMatch }) as T, | ||
) | ||
} | ||
if (typeof original != 'object') { | ||
return String(original).match(mockKey) ? (mockData as T) : original | ||
} | ||
|
||
if (typeof mockKey == 'string') | ||
mockKey = new RegExp(exactMatch ? `^${mockKey}$` : `${mockKey}`) | ||
const mocked: Dict = {} | ||
for (const key in original) { | ||
if (key.match('currenLic')) debug('Mocking currentLic', original) | ||
const updatedDeepPath = `${deepPath}.${key}` | ||
if (key.match(mockKey)) { | ||
mocked.isMocked = true | ||
mocked[key] = mockData | ||
debug(`Found deepMock match `, { | ||
mockKey, | ||
key, | ||
updatedDeepPath, | ||
mockData, | ||
}) | ||
} else | ||
mocked[key] = deepMock(original[key], mockKey, mockData, { | ||
deepPath: updatedDeepPath, | ||
}) | ||
} | ||
if (mocked.isMocked) { | ||
debug(`Deep mocking mocked data:`, mocked) | ||
debug(`Deep mocking original data:`, original) | ||
} | ||
return mocked | ||
} | ||
|
||
/** | ||
* Mock any graphql operation, returning the given mockData | ||
* | ||
* Optionally, define a different data key in the response or turn off the default camelCasing | ||
* of the operation. | ||
*/ | ||
export const mockQGL = async <T>( | ||
page: Page, | ||
op: string, | ||
mockData: T, | ||
{ | ||
responseKey = undefined, | ||
camelCaseResponseKey = !responseKey, | ||
patchResponse = false, | ||
deepMockKey = undefined, | ||
pattern = `**/graphql?op=${op}`, | ||
}: MockGQLOptions = {}, | ||
) => { | ||
debug(`Setting up mock for ${pattern} `, { | ||
op, | ||
responseKey, | ||
deepMockKey, | ||
}) | ||
|
||
await page.route(pattern, async (route) => { | ||
// Setup | ||
const routeUrl = route.request().url() | ||
const routeOp = routeUrl.split('op=')[1] | ||
const casedRouteOp = camelCaseResponseKey ? camelCase(routeOp) : routeOp | ||
debug(`Got route `, { routeUrl, routeOp, casedRouteOp, op }) | ||
|
||
// Get original | ||
const response = patchResponse | ||
? await ( | ||
await route.fetch({ | ||
headers: { ...route.request().headers(), MOCKED_PATCH: 'yes' }, | ||
}) | ||
).json() | ||
: {} | ||
const originalResponse = { ...response?.data } | ||
|
||
// Set mock | ||
const mockKey = responseKey ?? casedRouteOp | ||
if (!mockKey) | ||
throw Error( | ||
`Invalid key for mock (mockKey=${mockKey}, responseKey=${responseKey}, op=${op})!\nYou probably need to change the 'op' or add 'responseKey'`, | ||
) | ||
const mockResponse: Dict = deepMockKey | ||
? deepMock(originalResponse, deepMockKey, mockData) | ||
: Object.fromEntries([[mockKey, mockData]]) | ||
mockResponse.deepMocked = !!deepMockKey | ||
mockResponse.mocked = true | ||
|
||
// Debug logging | ||
debug(`Got a mock-match for > ${route.request().url()} < `, { | ||
mockKey, | ||
patchResponse, | ||
}) | ||
debug('(original):', originalResponse) | ||
|
||
const patchedData = mergeWith( | ||
{ ...originalResponse }, | ||
mockResponse, | ||
mergeOverwrite, | ||
) | ||
const data: Dict<Dict> = { data: {} } | ||
data.data = patchedData | ||
|
||
// Debug logging | ||
debug('(mocked): ', mockResponse) | ||
debug('(merged): ', patchedData) | ||
|
||
// Mock injection | ||
const body = JSON.stringify(data) | ||
debug('Body:', body) | ||
route.fulfill({ | ||
body, | ||
headers: { MOCKED: 'yes', DEEP_MOCKED: deepMockKey ? 'yes' : 'no' }, | ||
}) | ||
}) | ||
} | ||
|
||
export const disableObjectKey = async <T>( | ||
page: Page, | ||
key: Matchable, | ||
mockData?: T, | ||
) => { | ||
return await mockQGL(page, '**', mockData ?? `MOCKED-${key}`, { | ||
deepMockKey: key, | ||
patchResponse: true, | ||
}) | ||
} | ||
|
||
export const disablePreviousApplications = async (page: Page) => { | ||
await mockQGL(page, 'ApplicationApplications', []) | ||
//syslumennOnEntry.data.estates | ||
/* | ||
await mockQGL( | ||
page, | ||
'UpdateApplication', | ||
{ | ||
externalData: { | ||
existingApplication: { data: [] }, | ||
syslumennOnEntry: { data: {} }, | ||
}, | ||
}, | ||
{ patchResponse: true }, | ||
) | ||
*/ | ||
} | ||
|
||
export const disableI18n = async (page: Page) => { | ||
return await mockQGL(page, 'GetTranslations', { | ||
'mock.translation': 'YES-mocked', | ||
}) | ||
} | ||
|
||
export const disableDelegations = async (page: Page) => { | ||
return await mockQGL(page, 'ActorDelegations', []) | ||
} |
Oops, something went wrong.