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

Clean up integration tests and add listeners for backend calls #11847

Merged
merged 29 commits into from
Dec 12, 2024
Merged
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1a64456
Remove section headers from integration tests
somebody1234 Nov 28, 2024
d5eee57
Add `calls` to E2E tests
somebody1234 Nov 29, 2024
3a22990
Adjust tests
somebody1234 Dec 2, 2024
7f9e042
Fix type errors
somebody1234 Dec 2, 2024
0072570
Use named imports in Dashboard integration tests
somebody1234 Dec 2, 2024
490602e
WIP: `goToSettingsTabActions`
somebody1234 Dec 2, 2024
c7f4ea0
Update `userSettings.spec` to use new API
somebody1234 Dec 2, 2024
b7daa29
Finish updating `userSettings.spec` to use new API
somebody1234 Dec 3, 2024
62a9427
Update `organizationSettings.spec` to use new API
somebody1234 Dec 3, 2024
75efa0a
Switch integration tests to use named imports
somebody1234 Dec 3, 2024
554e174
Add `DrivePageActions.withSearchBar` to switch `assetSearchBar.spec` …
somebody1234 Dec 3, 2024
e049e33
Basic conversion of E2E tests to new structure
somebody1234 Dec 11, 2024
3538721
Fix circular imports in integration tests
somebody1234 Dec 11, 2024
e1cbb0e
Merge branch 'develop' into wip/sb/more-integration-tests
somebody1234 Dec 11, 2024
2e086f0
Fix Playwright imports
somebody1234 Dec 11, 2024
20af42c
Formatting
somebody1234 Dec 11, 2024
9b2d078
Use `page` from `actions.step`
somebody1234 Dec 11, 2024
8179f17
Fix broken integration tests
somebody1234 Dec 11, 2024
abcb91b
Fix integration tests
somebody1234 Dec 11, 2024
78a515a
Merge branch 'develop' into wip/sb/more-integration-tests
somebody1234 Dec 12, 2024
37edeb0
Fix more integration tests
somebody1234 Dec 12, 2024
9f9982a
Fix more integration tests
somebody1234 Dec 12, 2024
cea2aeb
Fix test errors
somebody1234 Dec 12, 2024
a9f45ea
Consistent formatting
somebody1234 Dec 12, 2024
8581f73
Fix lat integration test
somebody1234 Dec 12, 2024
f10d637
Fix
somebody1234 Dec 12, 2024
c796cde
Track backend calls in `organizationSettings.spec`
somebody1234 Dec 12, 2024
10ecb07
Add more docs for integration tests
somebody1234 Dec 12, 2024
6c0db07
Uncomment tests and skip them instead
somebody1234 Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 43 additions & 37 deletions app/gui/integration-test/dashboard/actions/BaseActions.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
/** @file The base class from which all `Actions` classes are derived. */
import * as test from '@playwright/test'
import { expect, test, type Locator, type Page } from '@playwright/test'

import type * as inputBindings from '#/utilities/inputBindings'
import type { AutocompleteKeybind } from '#/utilities/inputBindings'

import { modModifier } from '.'

// ====================
// === PageCallback ===
// ====================

/** A callback that performs actions on a {@link test.Page}. */
export interface PageCallback {
(input: test.Page): Promise<void> | void
/** `Meta` (`Cmd`) on macOS, and `Control` on all other platforms. */
async function modModifier(page: Page) {
let userAgent = ''
await test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
return /\bMac OS\b/i.test(userAgent) ? 'Meta' : 'Control'
}

// =======================
// === LocatorCallback ===
// =======================
/** A callback that performs actions on a {@link Page}. */
export interface PageCallback<Context> {
(input: Page, context: Context): Promise<void> | void
}

/** A callback that performs actions on a {@link test.Locator}. */
/** A callback that performs actions on a {@link Locator}. */
export interface LocatorCallback {
(input: test.Locator): Promise<void> | void
(input: Locator): Promise<void> | void
}

// ===================
// === BaseActions ===
// ===================
export interface BaseActionsClass<Context, Args extends readonly unknown[] = []> {
// The return type should be `InstanceType<this>`, but that results in a circular reference error.
new (page: Page, context: Context, promise: Promise<void>, ...args: Args): any
}

/**
* The base class from which all `Actions` classes are derived.
@@ -34,10 +34,11 @@ export interface LocatorCallback {
*
* [`thenable`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
*/
export default class BaseActions implements Promise<void> {
export default class BaseActions<Context> implements Promise<void> {
/** Create a {@link BaseActions}. */
constructor(
protected readonly page: test.Page,
protected readonly page: Page,
protected readonly context: Context,
private readonly promise = Promise.resolve(),
) {}

@@ -53,11 +54,11 @@ export default class BaseActions implements Promise<void> {
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
static press(page: test.Page, keyOrShortcut: string): Promise<void> {
return test.test.step(`Press '${keyOrShortcut}'`, async () => {
static press(page: Page, keyOrShortcut: string): Promise<void> {
return test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
await test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
const isMacOS = /\bMac OS\b/i.test(userAgent)
@@ -99,43 +100,48 @@ export default class BaseActions implements Promise<void> {

/** Return a {@link BaseActions} with the same {@link Promise} but a different type. */
into<
T extends new (page: test.Page, promise: Promise<void>, ...args: Args) => InstanceType<T>,
T extends new (
page: Page,
context: Context,
promise: Promise<void>,
...args: Args
) => InstanceType<T>,
Args extends readonly unknown[],
>(clazz: T, ...args: Args): InstanceType<T> {
return new clazz(this.page, this.promise, ...args)
return new clazz(this.page, this.context, this.promise, ...args)
}

/**
* Perform an action on the current page. This should generally be avoided in favor of using
* Perform an action. This should generally be avoided in favor of using
* specific methods; this is more or less an escape hatch used ONLY when the methods do not
* support desired functionality.
*/
do(callback: PageCallback): this {
do(callback: PageCallback<Context>): this {
// @ts-expect-error This is SAFE, but only when the constructor of this class has the exact
// same parameters as `BaseActions`.
return new this.constructor(
this.page,
this.then(() => callback(this.page)),
this.then(() => callback(this.page, this.context)),
)
}

/** Perform an action on the current page. */
step(name: string, callback: PageCallback) {
return this.do(() => test.test.step(name, () => callback(this.page)))
/** Perform an action. */
step(name: string, callback: PageCallback<Context>) {
return this.do(() => test.step(name, () => callback(this.page, this.context)))
}

/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
press<Key extends string>(keyOrShortcut: inputBindings.AutocompleteKeybind<Key>) {
press<Key extends string>(keyOrShortcut: AutocompleteKeybind<Key>) {
return this.do((page) => BaseActions.press(page, keyOrShortcut))
}

/** Perform actions until a predicate passes. */
retry(
callback: (actions: this) => this,
predicate: (page: test.Page) => Promise<boolean>,
predicate: (page: Page) => Promise<boolean>,
options: { retries?: number; delay?: number } = {},
) {
const { retries = 3, delay = 1_000 } = options
@@ -152,7 +158,7 @@ export default class BaseActions implements Promise<void> {
}

/** Perform actions with the "Mod" modifier key pressed. */
withModPressed<R extends BaseActions>(callback: (actions: this) => R) {
withModPressed<R extends BaseActions<Context>>(callback: (actions: this) => R) {
return callback(
this.step('Press "Mod"', async (page) => {
await page.keyboard.down(await modModifier(page))
@@ -171,11 +177,11 @@ export default class BaseActions implements Promise<void> {
return this
} else if (expected != null) {
return this.step(`Expect ${description} error to be '${expected}'`, async (page) => {
await test.expect(page.getByTestId(testId).getByTestId('error')).toHaveText(expected)
await expect(page.getByTestId(testId).getByTestId('error')).toHaveText(expected)
})
} else {
return this.step(`Expect no ${description} error`, async (page) => {
await test.expect(page.getByTestId(testId).getByTestId('error')).not.toBeVisible()
await expect(page.getByTestId(testId).getByTestId('error')).not.toBeVisible()
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @file Actions for the "user" tab of the "settings" page. */
import { goToPageActions, type GoToPageActions } from './goToPageActions'
import PageActions from './PageActions'

/** Actions common to all settings pages. */
export default class BaseSettingsTabActions<Context> extends PageActions<Context> {
/** Actions for navigating to another page. */
get goToPage(): Omit<GoToPageActions<Context>, 'settings'> {
return goToPageActions(this.step.bind(this))
}
}
Loading