Skip to content

Commit

Permalink
Add functions and documentation to testing/e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
Svana committed Nov 1, 2024
1 parent d0c9471 commit f723214
Show file tree
Hide file tree
Showing 20 changed files with 2,593 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,8 @@ apps/**/index.html

.nx/
.zed/

# E2E outputs
test-results/
playwright-report/
tmp-sessions/
49 changes: 45 additions & 4 deletions libs/testing/e2e/README.md
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>
```
14 changes: 13 additions & 1 deletion libs/testing/e2e/src/index.ts
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'
7 changes: 0 additions & 7 deletions libs/testing/e2e/src/lib/libs/testing/e2e.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions libs/testing/e2e/src/lib/libs/testing/e2e.ts

This file was deleted.

75 changes: 75 additions & 0 deletions libs/testing/e2e/src/lib/support/api-tools.ts
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()
}
29 changes: 29 additions & 0 deletions libs/testing/e2e/src/lib/support/application.ts
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
}
187 changes: 187 additions & 0 deletions libs/testing/e2e/src/lib/support/disablers.ts
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', [])
}
Loading

0 comments on commit f723214

Please sign in to comment.