enso/app/ide-desktop/lib/dashboard/e2e/actions.ts
somebody1234 83ec24da59
Refactor E2E tests for Dashboard; add E2E tests for User and Organization settings pages (#10031)
- PARTIALLY implements https://github.com/enso-org/cloud-v2/issues/1232
- Partially refactor E2E test actions into a state machine.
- The main goal of this is to _disallow_ invalid actions - for example going from a page to itself, which will fail at runtime, or trying to create a new Data Link on a page where that button is not accessible.
- An auxiliary goal is to have better namespacing of actions and better clarity:
- Previously, everything was a locator at the top level of a single module. This makes it very difficult to comprehend what kinds of actions are available.
- Note: There is also older `namespace`-based namespacing for the User and Organization settings pages, which were added before this refactor. They SHOULD be refactored to the new API, but I'm not sure whether it's worth spending the time right now.
- Add E2E tests for every input on the "user" settings page and the "organization" settings page.
- A skeletal E2E test for the Datalink modal has also been added - it does not actually test anything currently but should be sufficient for building upon.

# Important Notes
None
2024-06-20 16:19:01 +00:00

857 lines
31 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-redeclare */
/** @file Various actions, locators, and constants used in end-to-end tests. */
import * as test from '@playwright/test'
import DrivePageActions from './actions/DrivePageActions'
import LoginPageActions from './actions/LoginPageActions'
import * as apiModule from './api'
/* eslint-disable @typescript-eslint/no-namespace */
// =================
// === Constants ===
// =================
/** An example password that does not meet validation requirements. */
export const INVALID_PASSWORD = 'password'
/** An example password that meets validation requirements. */
export const VALID_PASSWORD = 'Password0!'
/** An example valid email address. */
export const VALID_EMAIL = 'email@example.com'
// ================
// === Locators ===
// ================
// === Input locators ===
/** Find an email input (if any) on the current page. */
export function locateEmailInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your email')
}
/** Find a password input (if any) on the current page. */
export function locatePasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your password')
}
/** Find a "confirm password" input (if any) on the current page. */
export function locateConfirmPasswordInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Confirm your password')
}
/** Find a "username" input (if any) on the current page. */
export function locateUsernameInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Enter your username')
}
/** Find a "name" input for a "new label" modal (if any) on the current page. */
export function locateNewLabelModalNameInput(page: test.Page) {
return locateNewLabelModal(page).getByLabel('Name')
}
/** Find all color radio button inputs for a "new label" modal (if any) on the current page. */
export function locateNewLabelModalColorButtons(page: test.Page) {
return (
locateNewLabelModal(page)
.filter({ has: page.getByText('Color') })
// The `radio` inputs are invisible, so they cannot be used in the locator.
.locator('label[data-rac]')
)
}
/** Find a "name" input for an "upsert secret" modal (if any) on the current page. */
export function locateSecretNameInput(page: test.Page) {
return locateUpsertSecretModal(page).getByPlaceholder('Enter the name of the secret')
}
/** Find a "value" input for an "upsert secret" modal (if any) on the current page. */
export function locateSecretValueInput(page: test.Page) {
return locateUpsertSecretModal(page).getByPlaceholder('Enter the value of the secret')
}
/** Find a search bar input (if any) on the current page. */
export function locateSearchBarInput(page: test.Page) {
return locateSearchBar(page).getByPlaceholder(/(?:)/)
}
/** Find the name column of the given assets table row. */
export function locateAssetRowName(locator: test.Locator) {
return locator.getByTestId('asset-row-name')
}
// === Button locators ===
/** Find a toast close button (if any) on the current locator. */
export function locateToastCloseButton(page: test.Locator | test.Page) {
// There is no other simple way to uniquely identify this element.
// eslint-disable-next-line no-restricted-properties
return page.locator('.Toastify__close-button')
}
/** Find a "login" button (if any) on the current locator. */
export function locateLoginButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Login', exact: true }).getByText('Login')
}
/** Find a "register" button (if any) on the current locator. */
export function locateRegisterButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Register' }).getByText('Register')
}
/** Find a "set username" button (if any) on the current page. */
export function locateSetUsernameButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Set Username' }).getByText('Set Username')
}
/** Find a "delete" button (if any) on the current page. */
export function locateDeleteButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Delete' }).getByText('Delete')
}
/** Find a button to delete something (if any) on the current page. */
export function locateDeleteIcon(page: test.Locator | test.Page) {
return page.getByAltText('Delete')
}
/** Find a "create" button (if any) on the current page. */
export function locateCreateButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'Create' }).getByText('Create')
}
/** Find a button to open the editor (if any) on the current page. */
export function locatePlayOrOpenProjectButton(page: test.Locator | test.Page) {
return page.getByLabel('Open in editor')
}
/** Find a button to close the project (if any) on the current page. */
export function locateStopProjectButton(page: test.Locator | test.Page) {
return page.getByLabel('Stop execution')
}
/** Find all labels in the labels panel (if any) on the current page. */
export function locateLabelsPanelLabels(page: test.Page) {
return (
locateLabelsPanel(page)
.getByRole('button')
// The delete button is also a `button`.
// eslint-disable-next-line no-restricted-properties
.and(page.locator(':nth-child(1)'))
)
}
/** Find a tick button (if any) on the current page. */
export function locateEditingTick(page: test.Locator | test.Page) {
return page.getByAltText('Confirm Edit')
}
/** Find a cross button (if any) on the current page. */
export function locateEditingCross(page: test.Locator | test.Page) {
return page.getByAltText('Cancel Edit')
}
/** Find labels in the "Labels" column of the assets table (if any) on the current page. */
export function locateAssetLabels(page: test.Locator | test.Page) {
return page.getByTestId('asset-label')
}
/** Find a toggle for the "Name" column (if any) on the current page. */
export function locateNameColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Name')
}
/** Find a toggle for the "Modified" column (if any) on the current page. */
export function locateModifiedColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Modified')
}
/** Find a toggle for the "Shared with" column (if any) on the current page. */
export function locateSharedWithColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Shared With')
}
/** Find a toggle for the "Labels" column (if any) on the current page. */
export function locateLabelsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Labels')
}
/** Find a toggle for the "Accessed by projects" column (if any) on the current page. */
export function locateAccessedByProjectsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Accessed By Projects')
}
/** Find a toggle for the "Accessed data" column (if any) on the current page. */
export function locateAccessedDataColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Accessed Data')
}
/** Find a toggle for the "Docs" column (if any) on the current page. */
export function locateDocsColumnToggle(page: test.Locator | test.Page) {
return page.getByAltText('Docs')
}
/** Find a button for the "Recent" category (if any) on the current page. */
export function locateRecentCategory(page: test.Locator | test.Page) {
return page.getByLabel('Recent').locator('visible=true')
}
/** Find a button for the "Home" category (if any) on the current page. */
export function locateHomeCategory(page: test.Locator | test.Page) {
return page.getByLabel('Home').locator('visible=true')
}
/** Find a button for the "Trash" category (if any) on the current page. */
export function locateTrashCategory(page: test.Locator | test.Page) {
return page.getByLabel('Trash').locator('visible=true')
}
// === Other buttons ===
/** Find a "new label" button (if any) on the current page. */
export function locateNewLabelButton(page: test.Locator | test.Page) {
return page.getByRole('button', { name: 'new label' }).getByText('new label')
}
/** Find an "upgrade" button (if any) on the current page. */
export function locateUpgradeButton(page: test.Locator | test.Page) {
return page.getByRole('link', { name: 'Upgrade', exact: true }).getByText('Upgrade').first()
}
/** Find a not enabled stub view (if any) on the current page. */
export function locateNotEnabledStub(page: test.Locator | test.Page) {
return page.getByTestId('not-enabled-stub')
}
/** Find a "new folder" icon (if any) on the current page. */
export function locateNewFolderIcon(page: test.Locator | test.Page) {
return page.getByRole('button').filter({ has: page.getByAltText('New Folder') })
}
/** Find a "new secret" icon (if any) on the current page. */
export function locateNewSecretIcon(page: test.Locator | test.Page) {
return page.getByRole('button').filter({ has: page.getByAltText('New Secret') })
}
/** Find a "download files" icon (if any) on the current page. */
export function locateDownloadFilesIcon(page: test.Locator | test.Page) {
return page.getByRole('button').filter({ has: page.getByAltText('Export') })
}
/** Find a list of tags in the search bar (if any) on the current page. */
export function locateSearchBarTags(page: test.Page) {
return locateSearchBar(page).getByTestId('asset-search-tag-names').getByRole('button')
}
/** Find a list of labels in the search bar (if any) on the current page. */
export function locateSearchBarLabels(page: test.Page) {
return locateSearchBar(page).getByTestId('asset-search-labels').getByRole('button')
}
/** Find a list of labels in the search bar (if any) on the current page. */
export function locateSearchBarSuggestions(page: test.Page) {
return locateSearchBar(page).getByTestId('asset-search-suggestion')
}
/** Find a "home page" icon (if any) on the current page. */
export function locateHomePageIcon(page: test.Locator | test.Page) {
return page.getByRole('button').filter({ has: page.getByAltText('Home') })
}
// === Icon locators ===
// These are specifically icons that are not also buttons.
// Icons that *are* buttons belong in the "Button locators" section.
/** Find a "sort ascending" icon (if any) on the current page. */
export function locateSortAscendingIcon(page: test.Locator | test.Page) {
return page.getByAltText('Sort Ascending')
}
/** Find a "sort descending" icon (if any) on the current page. */
export function locateSortDescendingIcon(page: test.Locator | test.Page) {
return page.getByAltText('Sort Descending')
}
// === Heading locators ===
/** Find a "name" column heading (if any) on the current page. */
export function locateNameColumnHeading(page: test.Locator | test.Page) {
return page
.getByLabel('Sort by name')
.or(page.getByLabel('Stop sorting by name'))
.or(page.getByLabel('Sort by name descending'))
}
/** Find a "modified" column heading (if any) on the current page. */
export function locateModifiedColumnHeading(page: test.Locator | test.Page) {
return page
.getByLabel('Sort by modification date')
.or(page.getByLabel('Stop sorting by modification date'))
.or(page.getByLabel('Sort by modification date descending'))
}
// === Container locators ===
/** Find a drive view (if any) on the current page. */
export function locateDriveView(page: test.Locator | test.Page) {
// This has no identifying features.
return page.getByTestId('drive-view')
}
/** Find a samples list (if any) on the current page. */
export function locateSamplesList(page: test.Locator | test.Page) {
// This has no identifying features.
return page.getByTestId('samples')
}
/** Find all samples list (if any) on the current page. */
export function locateSamples(page: test.Locator | test.Page) {
// This has no identifying features.
return locateSamplesList(page).getByRole('button')
}
/** Find a modal background (if any) on the current page. */
export function locateModalBackground(page: test.Locator | test.Page) {
// This has no identifying features.
return page.getByTestId('modal-background')
}
/** Find an editor container (if any) on the current page. */
export function locateEditor(page: test.Page) {
// Test ID of a placeholder editor component used during testing.
return page.getByTestId('gui-editor-root')
}
/** Find an assets table (if any) on the current page. */
export function locateAssetsTable(page: test.Page) {
return locateDriveView(page).getByRole('table')
}
/** Find assets table rows (if any) on the current page. */
export function locateAssetRows(page: test.Page) {
return locateAssetsTable(page).locator('tbody').getByRole('row')
}
/** Find the name column of the given asset row. */
export function locateAssetName(locator: test.Locator) {
return locator.locator('> :nth-child(1)')
}
/** Find assets table rows that represent directories that can be expanded (if any)
* on the current page. */
export function locateExpandableDirectories(page: test.Page) {
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Expand]') })
}
/** Find assets table rows that represent directories that can be collapsed (if any)
* on the current page. */
export function locateCollapsibleDirectories(page: test.Page) {
// The icon is hidden when not hovered so `getByLabel` will not work.
// eslint-disable-next-line no-restricted-properties
return locateAssetRows(page).filter({ has: page.locator('[aria-label=Collapse]') })
}
/** Find a "confirm delete" modal (if any) on the current page. */
export function locateConfirmDeleteModal(page: test.Page) {
// This has no identifying features.
return page.getByTestId('confirm-delete-modal')
}
/** Find a "new label" modal (if any) on the current page. */
export function locateNewLabelModal(page: test.Page) {
// This has no identifying features.
return page.getByTestId('new-label-modal')
}
/** Find an "upsert secret" modal (if any) on the current page. */
export function locateUpsertSecretModal(page: test.Page) {
// This has no identifying features.
return page.getByTestId('upsert-secret-modal')
}
/** Find a "new user group" modal (if any) on the current page. */
export function locateNewUserGroupModal(page: test.Page) {
// This has no identifying features.
return page.getByTestId('new-user-group-modal')
}
/** Find a user menu (if any) on the current page. */
export function locateUserMenu(page: test.Page) {
// This has no identifying features.
return page.getByTestId('user-menu')
}
/** Find a "set username" panel (if any) on the current page. */
export function locateSetUsernamePanel(page: test.Page) {
// This has no identifying features.
return page.getByTestId('set-username-panel')
}
/** Find a set of context menus (if any) on the current page. */
export function locateContextMenus(page: test.Page) {
// This has no identifying features.
return page.getByTestId('context-menus')
}
/** Find a labels panel (if any) on the current page. */
export function locateLabelsPanel(page: test.Page) {
// This has no identifying features.
return page.getByTestId('labels')
}
/** Find a list of labels (if any) on the current page. */
export function locateLabelsList(page: test.Page) {
// This has no identifying features.
return page.getByTestId('labels-list')
}
/** Find an asset panel (if any) on the current page. */
export function locateAssetPanel(page: test.Page) {
// This has no identifying features.
return page.getByTestId('asset-panel').locator('visible=true')
}
/** Find a search bar (if any) on the current page. */
export function locateSearchBar(page: test.Page) {
// This has no identifying features.
return page.getByTestId('asset-search-bar')
}
/** Find an extra columns button panel (if any) on the current page. */
export function locateExtraColumns(page: test.Page) {
// This has no identifying features.
return page.getByTestId('extra-columns')
}
/** Find a root directory dropzone (if any) on the current page.
* This is the empty space below the assets table, if it doesn't take up the whole screen
* vertically. */
export function locateRootDirectoryDropzone(page: test.Page) {
// This has no identifying features.
return page.getByTestId('root-directory-dropzone')
}
// === Content locators ===
/** Find an asset description in an asset panel (if any) on the current page. */
export function locateAssetPanelDescription(page: test.Page) {
// This has no identifying features.
return locateAssetPanel(page).getByTestId('asset-panel-description')
}
/** Find asset permissions in an asset panel (if any) on the current page. */
export function locateAssetPanelPermissions(page: test.Page) {
// This has no identifying features.
return locateAssetPanel(page).getByTestId('asset-panel-permissions').getByRole('button')
}
export namespace settings {
export namespace tab {
export namespace organization {
/** Find an "organization" tab button. */
export function locate(page: test.Page) {
return page.getByRole('button', { name: 'Organization' }).getByText('Organization')
}
}
export namespace members {
/** Find a "members" tab button. */
export function locate(page: test.Page) {
return page.getByRole('button', { name: 'Members', exact: true }).getByText('Members')
}
}
}
export namespace userAccount {
/** Navigate so that the "user account" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "user account" settings section', async () => {
await press(page, 'Mod+,')
})
}
/** Find a "user account" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('User Account')).locator('..')
}
/** Find a "name" input in the "user account" settings section. */
export function locateNameInput(page: test.Page) {
return locate(page).getByLabel('Name')
}
}
export namespace changePassword {
/** Navigate so that the "change password" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "change password" settings section', async () => {
await press(page, 'Mod+,')
})
}
/** Find a "change password" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Change Password')).locator('..')
}
/** Find a "current password" input in the "user account" settings section. */
export function locateCurrentPasswordInput(page: test.Page) {
return locate(page).getByLabel('Current password')
}
/** Find a "new password" input in the "user account" settings section. */
export function locateNewPasswordInput(page: test.Page) {
return locate(page).getByLabel('New password', { exact: true })
}
/** Find a "confirm new password" input in the "user account" settings section. */
export function locateConfirmNewPasswordInput(page: test.Page) {
return locate(page).getByLabel('Confirm new password')
}
/** Find a "change" button. */
export function locateChangeButton(page: test.Page) {
return locate(page).getByRole('button', { name: 'Change' }).getByText('Change')
}
}
export namespace profilePicture {
/** Navigate so that the "profile picture" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "profile picture" settings section', async () => {
await press(page, 'Mod+,')
})
}
/** Find a "profile picture" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Profile Picture')).locator('..')
}
/** Find a "profile picture" input. */
export function locateInput(page: test.Page) {
return locate(page).locator('label')
}
}
export namespace organization {
/** Navigate so that the "organization" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "organization" settings section', async () => {
await press(page, 'Mod+,')
await settings.tab.organization.locate(page).click()
})
}
/** Find an "organization" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Organization')).locator('..')
}
/** Find a "name" input in the "organization" settings section. */
export function locateNameInput(page: test.Page) {
return locate(page).getByLabel('Organization display name')
}
/** Find an "email" input in the "organization" settings section. */
// eslint-disable-next-line @typescript-eslint/no-shadow
export function locateEmailInput(page: test.Page) {
return locate(page).getByLabel('Email')
}
/** Find an "website" input in the "organization" settings section. */
export function locateWebsiteInput(page: test.Page) {
return locate(page).getByLabel('Website')
}
/** Find an "location" input in the "organization" settings section. */
export function locateLocationInput(page: test.Page) {
return locate(page).getByLabel('Location')
}
}
export namespace organizationProfilePicture {
/** Navigate so that the "organization profile picture" settings section is visible. */
export async function go(page: test.Page) {
await test.test.step('Go to "organization profile picture" settings section', async () => {
await press(page, 'Mod+,')
await settings.tab.organization.locate(page).click()
})
}
/** Find an "organization profile picture" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Profile Picture')).locator('..')
}
/** Find a "profile picture" input. */
export function locateInput(page: test.Page) {
return locate(page).locator('label')
}
}
export namespace members {
/** Navigate so that the "members" settings section is visible. */
export async function go(page: test.Page, force = false) {
await test.test.step('Go to "members" settings section', async () => {
await press(page, 'Mod+,')
await settings.tab.members.locate(page).click({ force })
})
}
/** Find a "members" settings section. */
export function locate(page: test.Page) {
return page.getByRole('heading').and(page.getByText('Members')).locator('..')
}
/** Find all rows representing members of the current organization. */
export function locateMembersRows(page: test.Page) {
return locate(page).locator('tbody').getByRole('row')
}
}
}
// ===============================
// === Visual layout utilities ===
// ===============================
/** Get the left side of the bounding box of an asset row. The locator MUST be for an asset row.
* DO NOT assume the left side of the outer container will change. This means that it is NOT SAFE
* to do anything with the returned values other than comparing them. */
export function getAssetRowLeftPx(locator: test.Locator) {
return locator.evaluate(el => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
}
// ===================================
// === Expect functions for themes ===
// ===================================
/** A test assertion to confirm that the element has the class `selected`. */
export async function expectClassSelected(locator: test.Locator) {
await test.test.step('Expect `selected`', async () => {
await test.expect(locator).toHaveClass(/(?:^| )selected(?: |$)/)
})
}
// ==============================
// === Other expect functions ===
// ==============================
/** A test assertion to confirm that the element is fully transparent. */
export async function expectOpacity0(locator: test.Locator) {
await test.test.step('Expect `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).toBe('0')
})
.toPass()
})
}
/** A test assertion to confirm that the element is not fully transparent. */
export async function expectNotOpacity0(locator: test.Locator) {
await test.test.step('Expect not `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).not.toBe('0')
})
.toPass()
})
}
// =======================
// === Mouse utilities ===
// =======================
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
export const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 }
/** Click an asset row. The center must not be clicked as that is the button for adding a label. */
export async function clickAssetRow(assetRow: test.Locator) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
await assetRow.click({ position: ASSET_ROW_SAFE_POSITION })
}
/** Drag an asset row. The center must not be clicked as that is the button for adding a label. */
export async function dragAssetRowToAssetRow(from: test.Locator, to: test.Locator) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
await from.dragTo(to, {
sourcePosition: ASSET_ROW_SAFE_POSITION,
targetPosition: ASSET_ROW_SAFE_POSITION,
})
}
/** Drag an asset row. The center must not be clicked as that is the button for adding a label. */
export async function dragAssetRow(from: test.Locator, to: test.Locator) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
await from.dragTo(to, { sourcePosition: ASSET_ROW_SAFE_POSITION })
}
// ==========================
// === Keyboard utilities ===
// ==========================
/** `Meta` (`Cmd`) on macOS, and `Control` on all other platforms. */
export async function modModifier(page: test.Page) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
return /\bMac OS\b/i.test(userAgent) ? 'Meta' : 'Control'
}
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
export async function press(page: test.Page, keyOrShortcut: string) {
await test.test.step(`Press '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
})
const isMacOS = /\bMac OS\b/i.test(userAgent)
const ctrlKey = isMacOS ? 'Meta' : 'Control'
const deleteKey = isMacOS ? 'Backspace' : 'Delete'
const shortcut = keyOrShortcut.replace(/\bMod\b/, ctrlKey).replace(/\bDelete\b/, deleteKey)
await page.keyboard.press(shortcut)
} else {
await page.keyboard.press(keyOrShortcut)
}
})
}
// =============
// === login ===
// =============
/** Perform a successful login. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function login(
{ page }: MockParams,
email = 'email@example.com',
password = VALID_PASSWORD
) {
await test.test.step('Login', async () => {
await page.goto('/')
await locateEmailInput(page).fill(email)
await locatePasswordInput(page).fill(password)
await locateLoginButton(page).click()
await locateToastCloseButton(page).click()
await passTermsAndConditionsDialog({ page })
})
}
// ==============================
// === mockIsInPlaywrightTest ===
// ==============================
/** Inject `isInPlaywrightTest` into the page. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockIsInPlaywrightTest({ page }: MockParams) {
await test.test.step('Mock `isInPlaywrightTest`', async () => {
await page.evaluate(() => {
// @ts-expect-error This is SAFE - it is a mistake for this variable to be written to
// from anywhere else.
window.isInPlaywrightTest = true
})
})
}
// ================
// === mockDate ===
// ================
/** A placeholder date for visual regression testing. */
const MOCK_DATE = Number(new Date('01/23/45 01:23:45'))
/** Parameters for {@link mockDate}. */
interface MockParams {
readonly page: test.Page
}
/** Replace `Date` with a version that returns a fixed time. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
async function mockDate({ page }: MockParams) {
// https://github.com/microsoft/playwright/issues/6347#issuecomment-1085850728
await test.test.step('Mock Date', async () => {
await page.addInitScript(`{
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${MOCK_DATE});
} else {
super(...args);
}
}
}
const __DateNowOffset = ${MOCK_DATE} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`)
})
}
/** Pass the Terms and conditions dialog. */
export async function passTermsAndConditionsDialog({ page }: MockParams) {
// wait for terms and conditions dialog to appear
// but don't fail if it doesn't appear
try {
await test.test.step('Accept Terms and Conditions', async () => {
// wait for terms and conditions dialog to appear
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
await page.waitForSelector('#terms-of-service-modal', { timeout: 500 })
await page.getByRole('checkbox').click()
await page.getByRole('button', { name: 'Accept' }).click()
})
} catch (error) {
// do nothing
}
}
// ===============
// === mockApi ===
// ===============
// This is a function, even though it does not use function syntax.
// eslint-disable-next-line no-restricted-syntax
export const mockApi = apiModule.mockApi
// ===============
// === mockAll ===
// ===============
/** Set up all mocks, without logging in. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAll({ page }: MockParams) {
return await test.test.step('Execute all mocks', async () => {
const api = await mockApi({ page })
await mockIsInPlaywrightTest({ page })
await mockDate({ page })
return { api, pageActions: new LoginPageActions(page) }
})
}
// =======================
// === mockAllAndLogin ===
// =======================
/** Set up all mocks, and log in with dummy credentials. */
// This syntax is required for Playwright to work properly.
// eslint-disable-next-line no-restricted-syntax
export async function mockAllAndLogin({ page }: MockParams) {
return await test.test.step('Execute all mocks and login', async () => {
const mocks = await mockAll({ page })
await login({ page })
await passTermsAndConditionsDialog({ page })
// This MUST run after login because globals are reset when the browser
// is navigated to another page.
await mockIsInPlaywrightTest({ page })
return { ...mocks, pageActions: new DrivePageActions(page) }
})
}