Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tests): Add functions and documentation to testing/e2e #16694

Merged
merged 10 commits into from
Nov 12, 2024
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 mock APIs 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 * from './lib/support/application'
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
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 { test, expect, Page, Locator, BrowserContext } from '@playwright/test'
54 changes: 54 additions & 0 deletions libs/testing/e2e/src/lib/config/playwright-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { defineConfig, devices } from '@playwright/test'

interface GlobalConfigParams {
webServerUrl: string
port?: number
command: string
cwd?: string
timeoutMs?: number
}

export const createGlobalConfig = ({
webServerUrl,
port,
command,
cwd = '../../../',
timeoutMs = 5 * 60 * 1000,
}: GlobalConfigParams) => {
return defineConfig({
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
testDir: 'e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved

use: {
baseURL: webServerUrl,
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
stdout: 'pipe',
port: port ? port : undefined,
url: port ? undefined : webServerUrl,
command,
reuseExistingServer: !process.env.CI,
timeout: timeoutMs,
cwd,
},
})
}
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.

50 changes: 50 additions & 0 deletions libs/testing/e2e/src/lib/support/addons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, Locator, Page } from '@playwright/test'
import { sleep } from './utils'

expect.extend({
async toHaveCountGreaterThan(
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
received: Locator,
value: number,
options: { timeout: number; sleepTime: number } = {
timeout: 10000,
sleepTime: 100,
},
) {
const initialTime = Date.now()
let count = -1
while (count <= value) {
count = await received.count()
if (Date.now() > initialTime + options.timeout)
return {
message: () =>
`Timeout waiting for element count to exceed ${value}. Current count: ${count}`,
pass: false,
}
await sleep(options.sleepTime)
}
return {
message: () => `Found ${count} elements`,
pass: true,
}
},
async toBeApplication(
received: string | Page,
applicationType = '[a-zA-Z0-9_-]+',
) {
const url: string = typeof received == 'string' ? received : received.url()
const protocol = 'https?://'
const host = '[^/]+'
const applicationId = '(/[a-zA-Z0-9_-]*)?'
const applicationRegExp = new RegExp(
`^${protocol}${host}/umsoknir/${applicationType}${applicationId}$`,
)
const pass = applicationRegExp.test(url)
const message = () =>
[
`Current page is ${pass ? '' : 'not '}an application`,
`Expected pattern: ${applicationRegExp}`,
`Actual URL: ${url}`,
].join('\n')
return { message, pass }
},
})
75 changes: 75 additions & 0 deletions libs/testing/e2e/src/lib/support/api-tools.ts
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
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) : ''
},
}
}
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved

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',
})
})
}
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved

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()
}
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
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
}
svanaeinars marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading