From e3c50c6ae4bcde1b6dab5fbc49aeb5b710d679f7 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Tue, 29 Oct 2024 11:38:27 -0700 Subject: [PATCH] test: refactor all fixtures into boxed steps --- .changeset/stupid-jars-thank.md | 5 + .../src/fixtures/configResetAllSettings.ts | 108 ++--- apps/hostd-e2e/src/fixtures/login.ts | 41 +- apps/hostd-e2e/src/fixtures/navigate.ts | 49 ++- apps/hostd-e2e/src/fixtures/volumes.ts | 131 ++++--- apps/renterd-e2e/src/fixtures/buckets.ts | 155 +++++--- .../src/fixtures/configResetSettings.ts | 187 ++++----- apps/renterd-e2e/src/fixtures/contracts.ts | 51 ++- apps/renterd-e2e/src/fixtures/files.ts | 370 ++++++++++-------- apps/renterd-e2e/src/fixtures/hosts.ts | 84 ++-- apps/renterd-e2e/src/fixtures/keys.ts | 91 +++-- apps/renterd-e2e/src/fixtures/login.ts | 41 +- apps/renterd-e2e/src/fixtures/navigate.ts | 81 ++-- apps/renterd-e2e/src/specs/files.spec.ts | 18 +- apps/renterd-e2e/src/specs/keys.spec.ts | 12 +- apps/walletd-e2e/src/fixtures/events.ts | 21 +- apps/walletd-e2e/src/fixtures/login.ts | 41 +- apps/walletd-e2e/src/fixtures/navigate.ts | 12 + .../src/fixtures/navigateToWallet.ts | 8 - .../src/fixtures/sendSiacoinDialog.ts | 39 +- apps/walletd-e2e/src/fixtures/wallet.ts | 277 +++++++------ .../src/specs/seedGenerateAddresses.spec.ts | 2 +- .../src/specs/seedSendSiacoin.spec.ts | 2 +- apps/walletd-e2e/src/specs/wallet.spec.ts | 8 +- libs/e2e/src/fixtures/clearToasts.ts | 24 +- libs/e2e/src/fixtures/click.ts | 60 +-- libs/e2e/src/fixtures/cmdk.ts | 5 +- libs/e2e/src/fixtures/configViewMode.ts | 37 +- libs/e2e/src/fixtures/expect.ts | 11 + libs/e2e/src/fixtures/preferences.ts | 67 ++-- libs/e2e/src/fixtures/selectInput.ts | 42 +- libs/e2e/src/fixtures/step.ts | 10 + libs/e2e/src/fixtures/switchValue.ts | 82 ++-- libs/e2e/src/fixtures/table.ts | 20 +- libs/e2e/src/fixtures/textInput.ts | 73 ++-- libs/e2e/src/fixtures/textarea.ts | 67 ++-- libs/e2e/src/index.ts | 2 + 37 files changed, 1328 insertions(+), 1006 deletions(-) create mode 100644 .changeset/stupid-jars-thank.md create mode 100644 apps/walletd-e2e/src/fixtures/navigate.ts delete mode 100644 apps/walletd-e2e/src/fixtures/navigateToWallet.ts create mode 100644 libs/e2e/src/fixtures/expect.ts create mode 100644 libs/e2e/src/fixtures/step.ts diff --git a/.changeset/stupid-jars-thank.md b/.changeset/stupid-jars-thank.md new file mode 100644 index 000000000..aa875e886 --- /dev/null +++ b/.changeset/stupid-jars-thank.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/e2e': minor +--- + +Refactored all fixtures into boxed steps. diff --git a/apps/hostd-e2e/src/fixtures/configResetAllSettings.ts b/apps/hostd-e2e/src/fixtures/configResetAllSettings.ts index 0a4ed5fd0..37ac09460 100644 --- a/apps/hostd-e2e/src/fixtures/configResetAllSettings.ts +++ b/apps/hostd-e2e/src/fixtures/configResetAllSettings.ts @@ -6,70 +6,74 @@ import { clearToasts, fillTextInputByName, fillSelectInputByName, + step, } from '@siafoundation/e2e' import { navigateToConfig } from './navigate' -export async function configResetAllSettings({ page }: { page: Page }) { - await navigateToConfig({ page }) - await setViewMode({ page, state: 'advanced' }) +export const configResetAllSettings = step( + 'config reset all settings', + async ({ page }: { page: Page }) => { + await navigateToConfig({ page }) + await setViewMode({ page, state: 'advanced' }) - // host - await setSwitchByLabel(page, 'acceptingContracts', false) - await fillTextInputByName(page, 'netAddress', 'foobar.com:9880') - await fillTextInputByName(page, 'maxContractDuration', '6') + // host + await setSwitchByLabel(page, 'acceptingContracts', false) + await fillTextInputByName(page, 'netAddress', 'foobar.com:9880') + await fillTextInputByName(page, 'maxContractDuration', '6') - // pricing - await fillSelectInputByName(page, 'pinnedCurrency', 'USD') - await fillTextInputByName(page, 'pinnedThreshold', '3') + // pricing + await fillSelectInputByName(page, 'pinnedCurrency', 'USD') + await fillTextInputByName(page, 'pinnedThreshold', '3') - await setSwitchByLabel(page, 'shouldPinStoragePrice', false) - await fillTextInputByName(page, 'storagePrice', '10') - await setSwitchByLabel(page, 'shouldPinStoragePrice', true) - await fillTextInputByName(page, 'storagePricePinned', '5') - await setSwitchByLabel(page, 'shouldPinStoragePrice', false) + await setSwitchByLabel(page, 'shouldPinStoragePrice', false) + await fillTextInputByName(page, 'storagePrice', '10') + await setSwitchByLabel(page, 'shouldPinStoragePrice', true) + await fillTextInputByName(page, 'storagePricePinned', '5') + await setSwitchByLabel(page, 'shouldPinStoragePrice', false) - await setSwitchByLabel(page, 'shouldPinEgressPrice', false) - await fillTextInputByName(page, 'egressPrice', '10') - await setSwitchByLabel(page, 'shouldPinEgressPrice', true) - await fillTextInputByName(page, 'egressPricePinned', '5') - await setSwitchByLabel(page, 'shouldPinEgressPrice', false) + await setSwitchByLabel(page, 'shouldPinEgressPrice', false) + await fillTextInputByName(page, 'egressPrice', '10') + await setSwitchByLabel(page, 'shouldPinEgressPrice', true) + await fillTextInputByName(page, 'egressPricePinned', '5') + await setSwitchByLabel(page, 'shouldPinEgressPrice', false) - await setSwitchByLabel(page, 'shouldPinIngressPrice', false) - await fillTextInputByName(page, 'ingressPrice', '10') - await setSwitchByLabel(page, 'shouldPinIngressPrice', true) - await fillTextInputByName(page, 'ingressPricePinned', '5') - await setSwitchByLabel(page, 'shouldPinIngressPrice', false) + await setSwitchByLabel(page, 'shouldPinIngressPrice', false) + await fillTextInputByName(page, 'ingressPrice', '10') + await setSwitchByLabel(page, 'shouldPinIngressPrice', true) + await fillTextInputByName(page, 'ingressPricePinned', '5') + await setSwitchByLabel(page, 'shouldPinIngressPrice', false) - await fillTextInputByName(page, 'collateralMultiplier', '2') + await fillTextInputByName(page, 'collateralMultiplier', '2') - await setSwitchByLabel(page, 'shouldPinMaxCollateral', false) - await fillTextInputByName(page, 'maxCollateral', '10') - await setSwitchByLabel(page, 'shouldPinMaxCollateral', true) - await fillTextInputByName(page, 'maxCollateralPinned', '5') - await setSwitchByLabel(page, 'shouldPinMaxCollateral', false) + await setSwitchByLabel(page, 'shouldPinMaxCollateral', false) + await fillTextInputByName(page, 'maxCollateral', '10') + await setSwitchByLabel(page, 'shouldPinMaxCollateral', true) + await fillTextInputByName(page, 'maxCollateralPinned', '5') + await setSwitchByLabel(page, 'shouldPinMaxCollateral', false) - await fillTextInputByName(page, 'contractPrice', '0.2') - await fillTextInputByName(page, 'baseRPCPrice', '1') - await fillTextInputByName(page, 'sectorAccessPrice', '1') - await fillTextInputByName(page, 'priceTableValidity', '30') + await fillTextInputByName(page, 'contractPrice', '0.2') + await fillTextInputByName(page, 'baseRPCPrice', '1') + await fillTextInputByName(page, 'sectorAccessPrice', '1') + await fillTextInputByName(page, 'priceTableValidity', '30') - // accounts - await fillTextInputByName(page, 'accountExpiry', '30') - await fillTextInputByName(page, 'maxAccountBalance', '10') + // accounts + await fillTextInputByName(page, 'accountExpiry', '30') + await fillTextInputByName(page, 'maxAccountBalance', '10') - // bandwidth - await fillTextInputByName(page, 'ingressLimit', '0') - await fillTextInputByName(page, 'egressLimit', '0') + // bandwidth + await fillTextInputByName(page, 'ingressLimit', '0') + await fillTextInputByName(page, 'egressLimit', '0') - // DNS - await fillSelectInputByName(page, 'dnsProvider', '') + // DNS + await fillSelectInputByName(page, 'dnsProvider', '') - // save - await clickIfEnabledAndWait( - page.getByText('Save changes'), - page.getByText('Settings have been saved') - ) - await clearToasts({ page }) - await setViewMode({ page, state: 'basic' }) - await navigateToConfig({ page }) -} + // save + await clickIfEnabledAndWait( + page.getByText('Save changes'), + page.getByText('Settings have been saved') + ) + await clearToasts({ page }) + await setViewMode({ page, state: 'basic' }) + await navigateToConfig({ page }) + } +) diff --git a/apps/hostd-e2e/src/fixtures/login.ts b/apps/hostd-e2e/src/fixtures/login.ts index 0d39e3493..dc0d4e1d7 100644 --- a/apps/hostd-e2e/src/fixtures/login.ts +++ b/apps/hostd-e2e/src/fixtures/login.ts @@ -1,21 +1,24 @@ import { Page, expect } from '@playwright/test' -import { fillTextInputByName } from '@siafoundation/e2e' +import { fillTextInputByName, step } from '@siafoundation/e2e' -export async function login({ - page, - address, - password, -}: { - page: Page - address: string - password: string -}) { - await page.goto('/login') - await expect(page).toHaveTitle('hostd') - await page.getByLabel('login settings').click() - await page.getByRole('menuitem', { name: 'Show custom API' }).click() - await fillTextInputByName(page, 'api', address) - await fillTextInputByName(page, 'password', password) - await page.locator('input[name=password]').press('Enter') - await expect(page.getByTestId('navbar').getByText('Overview')).toBeVisible() -} +export const login = step( + 'login', + async ({ + page, + address, + password, + }: { + page: Page + address: string + password: string + }) => { + await page.goto('/login') + await expect(page).toHaveTitle('hostd') + await page.getByLabel('login settings').click() + await page.getByRole('menuitem', { name: 'Show custom API' }).click() + await fillTextInputByName(page, 'api', address) + await fillTextInputByName(page, 'password', password) + await page.locator('input[name=password]').press('Enter') + await expect(page.getByTestId('navbar').getByText('Overview')).toBeVisible() + } +) diff --git a/apps/hostd-e2e/src/fixtures/navigate.ts b/apps/hostd-e2e/src/fixtures/navigate.ts index e954fddfa..2773f0dc5 100644 --- a/apps/hostd-e2e/src/fixtures/navigate.ts +++ b/apps/hostd-e2e/src/fixtures/navigate.ts @@ -1,23 +1,36 @@ import { Page, expect } from '@playwright/test' +import { step } from '@siafoundation/e2e' -export async function navigateToDashboard({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Overview').click() - await expect(page.getByTestId('navbar').getByText('Overview')).toBeVisible() -} +export const navigateToDashboard = step( + 'navigate to dashboard', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Overview').click() + await expect(page.getByTestId('navbar').getByText('Overview')).toBeVisible() + } +) -export async function navigateToConfig({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Configuration').click() - await expect( - page.getByTestId('navbar').getByText('Configuration') - ).toBeVisible() -} +export const navigateToConfig = step( + 'navigate to config', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Configuration').click() + await expect( + page.getByTestId('navbar').getByText('Configuration') + ).toBeVisible() + } +) -export async function navigateToVolumes({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Volumes').click() - await expect(page.getByTestId('navbar').getByText('Volumes')).toBeVisible() -} +export const navigateToVolumes = step( + 'navigate to volumes', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Volumes').click() + await expect(page.getByTestId('navbar').getByText('Volumes')).toBeVisible() + } +) -export async function navigateToWallet(page: Page) { - await page.getByTestId('sidenav').getByLabel('Wallet').click() - await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible() -} +export const navigateToWallet = step( + 'navigate to wallet', + async (page: Page) => { + await page.getByTestId('sidenav').getByLabel('Wallet').click() + await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible() + } +) diff --git a/apps/hostd-e2e/src/fixtures/volumes.ts b/apps/hostd-e2e/src/fixtures/volumes.ts index 5fb187a71..f958256fc 100644 --- a/apps/hostd-e2e/src/fixtures/volumes.ts +++ b/apps/hostd-e2e/src/fixtures/volumes.ts @@ -1,67 +1,82 @@ import { Page, expect } from '@playwright/test' import { navigateToVolumes } from './navigate' -import { fillTextInputByName } from '@siafoundation/e2e' +import { fillTextInputByName, step } from '@siafoundation/e2e' -export async function createVolume(page: Page, name: string, path: string) { - const fullPath = `${path}/${name}` - await navigateToVolumes({ page }) - await page.getByText('Create volume').click() - await fillTextInputByName(page, 'name', name) - await fillTextInputByName(page, 'immediatePath', path) - // immediatePath updates path after 500ms - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1000) - await fillTextInputByName(page, 'size', '11') - await expect( - page.getByRole('dialog').getByText('Must be between 10.00 GB') - ).toBeHidden() - await page.locator('input[name=size]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() - const row = page.getByRole('row', { name: fullPath }) - await expect(page.getByText('Volume created')).toBeVisible() - await expect(row.getByText('ready')).toBeVisible() - await expect(page.getByRole('cell', { name: fullPath })).toBeVisible() -} +export const createVolume = step( + 'create volume', + async (page: Page, name: string, path: string) => { + const fullPath = `${path}/${name}` + await navigateToVolumes({ page }) + await page.getByText('Create volume').click() + await fillTextInputByName(page, 'name', name) + await fillTextInputByName(page, 'immediatePath', path) + // immediatePath updates path after 500ms + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(1000) + await fillTextInputByName(page, 'size', '11') + await expect( + page.getByRole('dialog').getByText('Must be between 10.00 GB') + ).toBeHidden() + await page.locator('input[name=size]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + const row = page.getByRole('row', { name: fullPath }) + await expect(page.getByText('Volume created')).toBeVisible() + await expect(row.getByText('ready')).toBeVisible() + await expect(page.getByRole('cell', { name: fullPath })).toBeVisible() + } +) -export async function deleteVolume(page: Page, name: string, path: string) { - const fullPath = `${path}/${name}` - await openVolumeContextMenu(page, fullPath) - const menuDelete = page.getByRole('menuitem', { name: 'Delete' }) - await menuDelete.click({ timeout: 10_000 }) - await fillTextInputByName(page, 'path', fullPath) - await page.locator('input[name=path]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() - await expect( - page.getByText('Volume is now being permanently deleted') - ).toBeVisible() - await volumeNotInList(page, fullPath) -} +export const deleteVolume = step( + 'delete volume', + async (page: Page, name: string, path: string) => { + const fullPath = `${path}/${name}` + await openVolumeContextMenu(page, fullPath) + const menuDelete = page.getByRole('menuitem', { name: 'Delete' }) + await menuDelete.click({ timeout: 10_000 }) + await fillTextInputByName(page, 'path', fullPath) + await page.locator('input[name=path]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + await expect( + page.getByText('Volume is now being permanently deleted') + ).toBeVisible() + await volumeNotInList(page, fullPath) + } +) -export async function deleteVolumeIfExists( - page: Page, - name: string, - path: string -) { - const doesVolumeExist = await page - .getByRole('table') - .getByText(path + '/' + name) - .isVisible() - if (doesVolumeExist) { - await deleteVolume(page, name, path) +export const deleteVolumeIfExists = step( + 'delete volume if exists', + async (page: Page, name: string, path: string) => { + const doesVolumeExist = await page + .getByRole('table') + .getByText(path + '/' + name) + .isVisible() + if (doesVolumeExist) { + await deleteVolume(page, name, path) + } } -} +) -export async function openVolumeContextMenu(page: Page, name: string) { - await page - .getByRole('row', { name }) - .getByLabel('volume context menu') - .click() -} +export const openVolumeContextMenu = step( + 'open volume context menu', + async (page: Page, name: string) => { + const menu = page + .getByRole('row', { name }) + .getByLabel('volume context menu') + await expect(menu).toBeVisible() + await menu.click() + } +) -export async function volumeInList(page: Page, name: string) { - await expect(page.getByRole('table').getByText(name)).toBeVisible() -} +export const volumeInList = step( + 'volume in list', + async (page: Page, name: string) => { + await expect(page.getByRole('table').getByText(name)).toBeVisible() + } +) -export async function volumeNotInList(page: Page, name: string) { - await expect(page.getByRole('table').getByText(name)).toBeHidden() -} +export const volumeNotInList = step( + 'volume not in list', + async (page: Page, name: string) => { + await expect(page.getByRole('table').getByText(name)).toBeHidden() + } +) diff --git a/apps/renterd-e2e/src/fixtures/buckets.ts b/apps/renterd-e2e/src/fixtures/buckets.ts index 328d7f84c..0bfbf7ef9 100644 --- a/apps/renterd-e2e/src/fixtures/buckets.ts +++ b/apps/renterd-e2e/src/fixtures/buckets.ts @@ -1,78 +1,103 @@ import { Page, expect } from '@playwright/test' import { navigateToBuckets } from './navigate' -import { fillTextInputByName, clearToasts } from '@siafoundation/e2e' +import { fillTextInputByName, clearToasts, step } from '@siafoundation/e2e' import { deleteDirectory, deleteFile } from './files' -export async function createBucket(page: Page, name: string) { - await navigateToBuckets({ page }) - await page.getByText('Create bucket').first().click() - await fillTextInputByName(page, 'name', name) - await page.locator('input[name=name]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() - await expect(page.getByText('Bucket created')).toBeVisible() - await clearToasts({ page }) - await expect(page.getByRole('cell', { name })).toBeVisible() -} +export const createBucket = step( + 'create bucket', + async (page: Page, name: string) => { + await navigateToBuckets({ page }) + await page.getByText('Create bucket').first().click() + await fillTextInputByName(page, 'name', name) + await page.locator('input[name=name]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + await expect(page.getByText('Bucket created')).toBeVisible() + await clearToasts({ page }) + await expect(page.getByRole('cell', { name })).toBeVisible() + } +) -export async function deleteBucket(page: Page, name: string) { - await openBucketContextMenu(page, name) - await page.getByRole('menuitem', { name: 'Delete bucket' }).click() - await fillTextInputByName(page, 'name', name) - await page.locator('input[name=name]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() - await bucketNotInList(page, name) -} +export const deleteBucket = step( + 'delete bucket', + async (page: Page, name: string) => { + await openBucketContextMenu(page, name) + await page.getByRole('menuitem', { name: 'Delete bucket' }).click() + await fillTextInputByName(page, 'name', name) + await page.locator('input[name=name]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + await bucketNotInList(page, name) + } +) -export async function deleteBucketIfExists(page: Page, name: string) { - const doesBucketExist = await page - .getByTestId('bucketsTable') - .getByText(name) - .isVisible() - if (doesBucketExist) { - await openBucket(page, name) - // The list changes to filesTable and is still loading=false for a split second - // before the files start fetching - this is why we need to wait for 1000ms. - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1000) - await expect( - page.locator('[data-testid=filesTable][data-loading=false]') - ).toBeVisible() - const tableRows = await page - .getByTestId('filesTable') - .getByTestId(new RegExp(`${name}.*`)) - .all() - // First delete all top-level objects in the bucket, because a bucket - // can't be deleted if there are objects in it. - for (const row of tableRows) { - const id = await row.getAttribute('data-testid') - if (id === '..') { - continue - } - if (id?.endsWith('/')) { - await deleteDirectory(page, id) - } else { - await deleteFile(page, id) +export const deleteBucketIfExists = step( + 'delete bucket if exists', + async (page: Page, name: string) => { + const doesBucketExist = await page + .getByTestId('bucketsTable') + .getByText(name) + .isVisible() + if (doesBucketExist) { + await openBucket(page, name) + // The list changes to filesTable and is still loading=false for a split second + // before the files start fetching - this is why we need to wait for 1000ms. + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(1000) + await expect( + page.locator('[data-testid=filesTable][data-loading=false]') + ).toBeVisible() + const tableRows = await page + .getByTestId('filesTable') + .getByTestId(new RegExp(`${name}.*`)) + .all() + // First delete all top-level objects in the bucket, because a bucket + // can't be deleted if there are objects in it. + for (const row of tableRows) { + const id = await row.getAttribute('data-testid') + if (id === '..') { + continue + } + if (id?.endsWith('/')) { + await deleteDirectory(page, id) + } else { + await deleteFile(page, id) + } } + await navigateToBuckets({ page }) + await deleteBucket(page, name) } - await navigateToBuckets({ page }) - await deleteBucket(page, name) } -} +) -export async function openBucketContextMenu(page: Page, name: string) { - await page.getByRole('row', { name }).getByRole('button').first().click() -} +export const openBucketContextMenu = step( + 'open bucket context menu', + async (page: Page, name: string) => { + const menu = page.getByRole('row', { name }).getByRole('button').first() + await expect(menu).toBeVisible() + return menu.click() + } +) -export async function openBucket(page: Page, name: string) { - await page.getByRole('row').getByText(name).click() - await expect(page.getByTestId('navbar').getByText(name)).toBeVisible() - await expect(page.getByLabel('Upload files')).toBeVisible() -} +export const openBucket = step( + 'open bucket', + async (page: Page, name: string) => { + const row = page.getByRole('row').getByText(name) + await expect(row).toBeVisible() + await row.click() + await expect(page.getByTestId('navbar').getByText(name)).toBeVisible() + await expect(page.getByLabel('Upload files')).toBeVisible() + } +) -export async function bucketInList(page: Page, name: string) { - await expect(page.getByTestId('bucketsTable').getByText(name)).toBeVisible() -} +export const bucketInList = step( + 'expect bucket in list', + async (page: Page, name: string) => { + await expect(page.getByTestId('bucketsTable').getByText(name)).toBeVisible() + } +) -export async function bucketNotInList(page: Page, name: string) { - await expect(page.getByTestId('bucketsTable').getByText(name)).toBeHidden() -} +export const bucketNotInList = step( + 'expect bucket not in list', + async (page: Page, name: string) => { + await expect(page.getByTestId('bucketsTable').getByText(name)).toBeHidden() + } +) diff --git a/apps/renterd-e2e/src/fixtures/configResetSettings.ts b/apps/renterd-e2e/src/fixtures/configResetSettings.ts index cfdbb3507..03afd6f7c 100644 --- a/apps/renterd-e2e/src/fixtures/configResetSettings.ts +++ b/apps/renterd-e2e/src/fixtures/configResetSettings.ts @@ -6,96 +6,103 @@ import { clearToasts, clickIfEnabledAndWait, fillSelectInputByName, + step, } from '@siafoundation/e2e' import { navigateToConfig } from './navigate' -export async function configResetAllSettings({ page }: { page: Page }) { - await navigateToConfig({ page }) - await setViewMode({ page, state: 'advanced' }) - - // pinning - await fillSelectInputByName(page, 'pinnedCurrency', 'usd') - await fillTextInputByName(page, 'pinnedThreshold', '2') - - // storage - await fillTextInputByName(page, 'storageTB', '1') - await fillTextInputByName(page, 'uploadTBMonth', '1') - await fillTextInputByName(page, 'downloadTBMonth', '1') - await setSwitchByLabel(page, 'shouldPinAllowance', true) - await fillTextInputByName(page, 'allowanceMonthPinned', '10') - await setSwitchByLabel(page, 'shouldPinAllowance', false) - await fillTextInputByName(page, 'allowanceMonth', '21000') - await fillTextInputByName(page, 'periodWeeks', '6') - await fillTextInputByName(page, 'renewWindowWeeks', '2') - await fillTextInputByName(page, 'amountHosts', '3') - await fillTextInputByName(page, 'autopilotContractSet', 'autopilot') - await setSwitchByLabel(page, 'prune', false) - - // hosts - await setSwitchByLabel(page, 'allowRedundantIPs', false) - await fillTextInputByName(page, 'maxDowntimeHours', '330') - await fillTextInputByName(page, 'maxConsecutiveScanFailures', '10') - await fillTextInputByName(page, 'minProtocolVersion', '1.6.0') - - // contracts - await fillTextInputByName(page, 'defaultContractSet', 'autopilot') - await setSwitchByLabel(page, 'uploadPackingEnabled', true) - - // gouging - await setSwitchByLabel(page, 'shouldPinMaxStoragePrice', true) - await setSwitchByLabel(page, 'shouldPinMaxDownloadPrice', true) - await setSwitchByLabel(page, 'shouldPinMaxUploadPrice', true) - - await fillTextInputByName(page, 'maxStoragePriceTBMonthPinned', '5') - await fillTextInputByName(page, 'maxUploadPriceTBPinned', '5') - await fillTextInputByName(page, 'maxDownloadPriceTBPinned', '5') - - await setSwitchByLabel(page, 'shouldPinMaxStoragePrice', false) - await setSwitchByLabel(page, 'shouldPinMaxDownloadPrice', false) - await setSwitchByLabel(page, 'shouldPinMaxUploadPrice', false) - - await fillTextInputByName(page, 'maxStoragePriceTBMonth', '3000') - await fillTextInputByName(page, 'maxUploadPriceTB', '3000') - await fillTextInputByName(page, 'maxDownloadPriceTB', '3000') - await fillTextInputByName(page, 'maxRPCPriceMillion', '10') - - await fillTextInputByName(page, 'maxContractPrice', '1') - await fillTextInputByName(page, 'hostBlockHeightLeeway', '240') - await fillTextInputByName(page, 'minPriceTableValidityMinutes', '5') - await fillTextInputByName(page, 'minAccountExpiryDays', '1') - await fillTextInputByName(page, 'minMaxEphemeralAccountBalance', '1') - await fillTextInputByName(page, 'migrationSurchargeMultiplier', '1') - - // redundancy - await fillTextInputByName(page, 'minShards', '1') - await fillTextInputByName(page, 'totalShards', '3') - - // save - await clickIfEnabledAndWait( - page.getByText('Save changes'), - page.getByText('Configuration has been saved') - ) - await clearToasts({ page }) - await setViewMode({ page, state: 'basic' }) -} - -export async function configResetBasicSettings({ page }: { page: Page }) { - await navigateToConfig({ page }) - await setViewMode({ page, state: 'basic' }) - - await fillTextInputByName(page, 'storageTB', '7') - await fillTextInputByName(page, 'uploadTBMonth', '7') - await fillTextInputByName(page, 'downloadTBMonth', '7') - await fillTextInputByName(page, 'allowanceMonth', '1000') - - await fillTextInputByName(page, 'maxStoragePriceTBMonth', '3000') - await fillTextInputByName(page, 'maxUploadPriceTB', '3000') - await fillTextInputByName(page, 'maxDownloadPriceTB', '3000') - - // save - await clickIfEnabledAndWait( - page.getByText('Save changes'), - page.getByText('Configuration has been saved') - ) - await clearToasts({ page }) -} +export const configResetAllSettings = step( + 'config reset all settings', + async ({ page }: { page: Page }) => { + await navigateToConfig({ page }) + await setViewMode({ page, state: 'advanced' }) + + // pinning + await fillSelectInputByName(page, 'pinnedCurrency', 'usd') + await fillTextInputByName(page, 'pinnedThreshold', '2') + + // storage + await fillTextInputByName(page, 'storageTB', '1') + await fillTextInputByName(page, 'uploadTBMonth', '1') + await fillTextInputByName(page, 'downloadTBMonth', '1') + await setSwitchByLabel(page, 'shouldPinAllowance', true) + await fillTextInputByName(page, 'allowanceMonthPinned', '10') + await setSwitchByLabel(page, 'shouldPinAllowance', false) + await fillTextInputByName(page, 'allowanceMonth', '21000') + await fillTextInputByName(page, 'periodWeeks', '6') + await fillTextInputByName(page, 'renewWindowWeeks', '2') + await fillTextInputByName(page, 'amountHosts', '3') + await fillTextInputByName(page, 'autopilotContractSet', 'autopilot') + await setSwitchByLabel(page, 'prune', false) + + // hosts + await setSwitchByLabel(page, 'allowRedundantIPs', false) + await fillTextInputByName(page, 'maxDowntimeHours', '330') + await fillTextInputByName(page, 'maxConsecutiveScanFailures', '10') + await fillTextInputByName(page, 'minProtocolVersion', '1.6.0') + + // contracts + await fillTextInputByName(page, 'defaultContractSet', 'autopilot') + await setSwitchByLabel(page, 'uploadPackingEnabled', true) + + // gouging + await setSwitchByLabel(page, 'shouldPinMaxStoragePrice', true) + await setSwitchByLabel(page, 'shouldPinMaxDownloadPrice', true) + await setSwitchByLabel(page, 'shouldPinMaxUploadPrice', true) + + await fillTextInputByName(page, 'maxStoragePriceTBMonthPinned', '5') + await fillTextInputByName(page, 'maxUploadPriceTBPinned', '5') + await fillTextInputByName(page, 'maxDownloadPriceTBPinned', '5') + + await setSwitchByLabel(page, 'shouldPinMaxStoragePrice', false) + await setSwitchByLabel(page, 'shouldPinMaxDownloadPrice', false) + await setSwitchByLabel(page, 'shouldPinMaxUploadPrice', false) + + await fillTextInputByName(page, 'maxStoragePriceTBMonth', '3000') + await fillTextInputByName(page, 'maxUploadPriceTB', '3000') + await fillTextInputByName(page, 'maxDownloadPriceTB', '3000') + await fillTextInputByName(page, 'maxRPCPriceMillion', '10') + + await fillTextInputByName(page, 'maxContractPrice', '1') + await fillTextInputByName(page, 'hostBlockHeightLeeway', '240') + await fillTextInputByName(page, 'minPriceTableValidityMinutes', '5') + await fillTextInputByName(page, 'minAccountExpiryDays', '1') + await fillTextInputByName(page, 'minMaxEphemeralAccountBalance', '1') + await fillTextInputByName(page, 'migrationSurchargeMultiplier', '1') + + // redundancy + await fillTextInputByName(page, 'minShards', '1') + await fillTextInputByName(page, 'totalShards', '3') + + // save + await clickIfEnabledAndWait( + page.getByText('Save changes'), + page.getByText('Configuration has been saved') + ) + await clearToasts({ page }) + await setViewMode({ page, state: 'basic' }) + } +) + +export const configResetBasicSettings = step( + 'config reset basic settings', + async ({ page }: { page: Page }) => { + await navigateToConfig({ page }) + await setViewMode({ page, state: 'basic' }) + + await fillTextInputByName(page, 'storageTB', '7') + await fillTextInputByName(page, 'uploadTBMonth', '7') + await fillTextInputByName(page, 'downloadTBMonth', '7') + await fillTextInputByName(page, 'allowanceMonth', '1000') + + await fillTextInputByName(page, 'maxStoragePriceTBMonth', '3000') + await fillTextInputByName(page, 'maxUploadPriceTB', '3000') + await fillTextInputByName(page, 'maxDownloadPriceTB', '3000') + + // save + await clickIfEnabledAndWait( + page.getByText('Save changes'), + page.getByText('Configuration has been saved') + ) + await clearToasts({ page }) + } +) diff --git a/apps/renterd-e2e/src/fixtures/contracts.ts b/apps/renterd-e2e/src/fixtures/contracts.ts index 1ab72646d..ba2f158df 100644 --- a/apps/renterd-e2e/src/fixtures/contracts.ts +++ b/apps/renterd-e2e/src/fixtures/contracts.ts @@ -1,29 +1,42 @@ import { Page } from '@playwright/test' +import { maybeExpectAndReturn, step } from '@siafoundation/e2e' -export function getContractRowById(page: Page, id: string) { - return page.getByTestId('contractsTable').getByTestId(id) -} +export const getContractRowById = step( + 'get contract row by ID', + async (page: Page, id: string) => { + return page.getByTestId('contractsTable').getByTestId(id) + } +) -export function getContractsSummaryRow(page: Page) { - return page - .getByTestId('contractsTable') - .locator('thead') - .getByRole('row') - .nth(1) -} +export const getContractsSummaryRow = step( + 'get contracts summary row', + async (page: Page) => { + return page + .getByTestId('contractsTable') + .locator('thead') + .getByRole('row') + .nth(1) + } +) -export function getContractRowByIndex(page: Page, index: number) { - return page - .getByTestId('contractsTable') - .locator('tbody') - .getByRole('row') - .nth(index) -} +export const getContractRowByIndex = step( + 'get contract row by index', + async (page: Page, index: number, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page + .getByTestId('contractsTable') + .locator('tbody') + .getByRole('row') + .nth(index), + shouldExpect + ) + } +) -export async function getContractRows(page: Page) { +export const getContractRows = step('get contract rows', async (page: Page) => { return page .getByTestId('contractsTable') .locator('tbody') .getByRole('row') .all() -} +}) diff --git a/apps/renterd-e2e/src/fixtures/files.ts b/apps/renterd-e2e/src/fixtures/files.ts index 756edc244..f80020175 100644 --- a/apps/renterd-e2e/src/fixtures/files.ts +++ b/apps/renterd-e2e/src/fixtures/files.ts @@ -1,132 +1,187 @@ import { Page, expect } from '@playwright/test' import { readFileSync } from 'fs' -import { fillTextInputByName } from '@siafoundation/e2e' +import { + fillTextInputByName, + maybeExpectAndReturn, + step, +} from '@siafoundation/e2e' import { navigateToBuckets } from './navigate' import { openBucket } from './buckets' import { join } from 'path' -export async function deleteFile(page: Page, path: string) { - await openFileContextMenu(page, path) - await page.getByRole('menuitem', { name: 'Delete file' }).click() - await expect(page.getByRole('dialog').getByText('Delete file')).toBeVisible() - await page.locator('form button[type=submit]').click() - await expect(page.getByRole('dialog')).toBeHidden() - await fileNotInList(page, path) -} +export const deleteFile = step( + 'delete file', + async (page: Page, path: string) => { + await openFileContextMenu(page, path) + await page.getByRole('menuitem', { name: 'Delete file' }).click() + await expect( + page.getByRole('dialog').getByText('Delete file') + ).toBeVisible() + await page.locator('form button[type=submit]').click() + await expect(page.getByRole('dialog')).toBeHidden() + await fileNotInList(page, path) + } +) -export async function deleteFileIfExists(page: Page, path: string) { - const exists = await page - .getByTestId('filesTable') - .getByTestId(path) - .isVisible() - if (exists) { - await deleteFile(page, path) +export const deleteFileIfExists = step( + 'delete file if exists', + async (page: Page, path: string) => { + const exists = await page + .getByTestId('filesTable') + .getByTestId(path) + .isVisible() + if (exists) { + await deleteFile(page, path) + } } -} +) -export async function deleteDirectory(page: Page, path: string) { - await openDirectoryContextMenu(page, path) - const deleteDirectoryItem = page.getByRole('menuitem', { - name: 'Delete directory', - }) - await expect(deleteDirectoryItem).toBeVisible() - await deleteDirectoryItem.click() - await expect( - page.getByRole('dialog').getByText('Delete directory') - ).toBeVisible() - await page.locator('form button[type=submit]').click() - await expect(page.getByRole('dialog')).toBeHidden() - await fileNotInList(page, path) -} +export const deleteDirectory = step( + 'delete directory', + async (page: Page, path: string) => { + await openDirectoryContextMenu(page, path) + const deleteDirectoryItem = page.getByRole('menuitem', { + name: 'Delete directory', + }) + await expect(deleteDirectoryItem).toBeVisible() + await deleteDirectoryItem.click() + await expect( + page.getByRole('dialog').getByText('Delete directory') + ).toBeVisible() + await page.locator('form button[type=submit]').click() + await expect(page.getByRole('dialog')).toBeHidden() + await fileNotInList(page, path) + } +) -export async function deleteDirectoryIfExists(page: Page, path: string) { - const exists = await page - .getByTestId('filesTable') - .getByTestId(path) - .isVisible() - if (exists) { - await deleteDirectory(page, path) +export const deleteDirectoryIfExists = step( + 'delete directory if exists', + async (page: Page, path: string) => { + const exists = await page + .getByTestId('filesTable') + .getByTestId(path) + .isVisible() + if (exists) { + await deleteDirectory(page, path) + } } -} +) -export async function openDirectoryContextMenu(page: Page, path: string) { - const selector = page.getByTestId(path).getByLabel('Directory context menu') - // Click doesn't work until animation is finished. - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(100) - await expect(selector).toBeVisible() - await selector.click() -} +export const openDirectoryContextMenu = step( + 'open directory context menu', + async (page: Page, path: string) => { + const selector = page.getByTestId(path).getByLabel('Directory context menu') + // // Click doesn't work until animation is finished. + // // eslint-disable-next-line playwright/no-wait-for-timeout + // await page.waitForTimeout(100) + await expect(selector).toBeVisible() + await selector.click() + } +) -export async function openFileContextMenu(page: Page, path: string) { - const selector = page.getByTestId(path).getByLabel('File context menu') - await expect(selector).toBeVisible() - await selector.click() -} +export const openFileContextMenu = step( + 'open file context menu', + async (page: Page, path: string) => { + const selector = page.getByTestId(path).getByLabel('File context menu') + await expect(selector).toBeVisible() + await selector.click() + } +) -export async function openDirectory(page: Page, path: string) { - const parts = path.split('/') - const name = parts[parts.length - 2] + '/' - await page.getByTestId('filesTable').getByTestId(path).getByText(name).click() - for (const dir of path.split('/').slice(0, -1)) { - await expect(page.getByTestId('navbar').getByText(dir)).toBeVisible() +export const openDirectory = step( + 'open directory', + async (page: Page, path: string) => { + const parts = path.split('/') + const name = parts[parts.length - 2] + '/' + await page + .getByTestId('filesTable') + .getByTestId(path) + .getByText(name) + .click() + for (const dir of path.split('/').slice(0, -1)) { + await expect(page.getByTestId('navbar').getByText(dir)).toBeVisible() + } } -} +) -export async function openDirectoryFromAnywhere(page: Page, path: string) { - const bucket = path.split('/')[0] - const dirParts = path.split('/').slice(1) - await navigateToBuckets({ page }) - await openBucket(page, path.split('/')[0]) - let currentPath = bucket + '/' - for (const dir of dirParts) { - currentPath += dir + '/' - await openDirectory(page, currentPath) +export const openDirectoryFromAnywhere = step( + 'open directory from anywhere', + async (page: Page, path: string) => { + const bucket = path.split('/')[0] + const dirParts = path.split('/').slice(1) + await navigateToBuckets({ page }) + await openBucket(page, path.split('/')[0]) + let currentPath = bucket + '/' + for (const dir of dirParts) { + currentPath += dir + '/' + await openDirectory(page, currentPath) + } } -} +) -export async function navigateToParentDirectory(page: Page) { - const isEmpty = await page - .getByText('The current directory does not contain any files yet') - .isVisible() - if (isEmpty) { - await page.getByRole('button', { name: 'Back' }).click() - } else { - await page.getByRole('cell', { name: '..' }).click() +export const navigateToParentDirectory = step( + 'navigate to parent directory', + async (page: Page) => { + const isEmpty = await page + .getByText('The current directory does not contain any files yet') + .isVisible() + if (isEmpty) { + await page.getByRole('button', { name: 'Back' }).click() + } else { + await page.getByRole('cell', { name: '..' }).click() + } } -} +) -export async function createDirectory(page: Page, name: string) { - await expect(page.getByLabel('Create directory')).toBeVisible() - await page.getByLabel('Create directory').click() - await fillTextInputByName(page, 'name', name) - await page.locator('input[name=name]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() -} +export const createDirectory = step( + 'create directory', + async (page: Page, name: string) => { + await expect(page.getByLabel('Create directory')).toBeVisible() + await page.getByLabel('Create directory').click() + await fillTextInputByName(page, 'name', name) + await page.locator('input[name=name]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + } +) -export async function createDirectoryIfNotExists(page: Page, name: string) { - const exists = await page - .getByTestId('filesTable') - .getByTestId(name) - .isVisible() - if (!exists) { - await createDirectory(page, name) +export const createDirectoryIfNotExists = step( + 'create directory if not exists', + async (page: Page, name: string) => { + const exists = await page + .getByTestId('filesTable') + .getByTestId(name) + .isVisible() + if (!exists) { + await createDirectory(page, name) + } } -} +) -export async function fileInList(page: Page, path: string, timeout?: number) { - await expect(page.getByTestId('filesTable').getByTestId(path)).toBeVisible({ - timeout, - }) -} +export const fileInList = step( + 'expect file in list', + async (page: Page, path: string, timeout?: number) => { + await expect(page.getByTestId('filesTable').getByTestId(path)).toBeVisible({ + timeout, + }) + } +) -export async function fileNotInList(page: Page, path: string) { - await expect(page.getByTestId('filesTable').getByTestId(path)).toBeHidden() -} +export const fileNotInList = step( + 'expect file not in list', + async (page: Page, path: string) => { + await expect(page.getByTestId('filesTable').getByTestId(path)).toBeHidden() + } +) -export function getFileRowById(page: Page, id: string) { - return page.getByTestId('filesTable').getByTestId(id) -} +export const getFileRowById = step( + 'get file row by ID', + async (page: Page, id: string, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page.getByTestId('filesTable').getByTestId(id), + shouldExpect + ) + } +) async function simulateDragAndDropFile( page: Page, @@ -143,7 +198,9 @@ async function simulateDragAndDropFile( const blobData = await fetch(bufferData).then((res) => res.blob()) - const file = new File([blobData], localFileName, { type: localFileType }) + const file = new File([blobData], localFileName, { + type: localFileType, + }) dt.items.add(file) return dt }, @@ -157,76 +214,73 @@ async function simulateDragAndDropFile( await page.dispatchEvent(selector, 'drop', { dataTransfer }) } -export async function dragAndDropFileFromSystem( - page: Page, - systemFilePath: string, - localFileName?: string -) { - await simulateDragAndDropFile( - page, - `[data-testid=filesDropzone]`, - join(__dirname, 'sample-files', systemFilePath), - '/' + (localFileName || systemFilePath) - ) -} +export const dragAndDropFileFromSystem = step( + 'drag and drop file from system', + async (page: Page, systemFilePath: string, localFileName?: string) => { + await simulateDragAndDropFile( + page, + `[data-testid=filesDropzone]`, + join(__dirname, 'sample-files', systemFilePath), + '/' + (localFileName || systemFilePath) + ) + } +) export interface FileMap { [key: string]: string | FileMap } // Iterate through the file map and create files/directories. -export async function createFilesMap( - page: Page, - bucketName: string, - map: FileMap -) { - const create = async (map: FileMap, stack: string[]) => { - for (const name in map) { - await openDirectoryFromAnywhere(page, stack.join('/')) - const currentDirPath = stack.join('/') - const path = `${currentDirPath}/${name}` - if (!!map[name] && typeof map[name] === 'object') { - await createDirectory(page, name) - await fileInList(page, path + '/') - await create(map[name] as FileMap, stack.concat(name)) - } else { - await dragAndDropFileFromSystem(page, 'sample.txt', name) - await fileInList(page, path) +export const createFilesMap = step( + 'create files map', + async (page: Page, bucketName: string, map: FileMap) => { + const create = async (map: FileMap, stack: string[]) => { + for (const name in map) { + await openDirectoryFromAnywhere(page, stack.join('/')) + const currentDirPath = stack.join('/') + const path = `${currentDirPath}/${name}` + if (!!map[name] && typeof map[name] === 'object') { + await createDirectory(page, name) + await fileInList(page, path + '/') + await create(map[name] as FileMap, stack.concat(name)) + } else { + await dragAndDropFileFromSystem(page, 'sample.txt', name) + await fileInList(page, path) + } } } + await create(map, [bucketName]) + await navigateToBuckets({ page }) + await openBucket(page, bucketName) } - await create(map, [bucketName]) - await navigateToBuckets({ page }) - await openBucket(page, bucketName) -} +) interface FileExpectMap { [key: string]: 'visible' | 'hidden' | FileExpectMap } // Check each file and directory in the map exists. -export async function expectFilesMap( - page: Page, - bucketName: string, - map: FileExpectMap -) { - const check = async (map: FileMap, stack: string[]) => { - for (const name in map) { - await openDirectoryFromAnywhere(page, stack.join('/')) - const currentDirPath = stack.join('/') - const path = `${currentDirPath}/${name}` - if (typeof map[name] === 'string') { - const state = map[name] as 'visible' | 'hidden' - if (state === 'visible') { - await fileInList(page, path) +export const expectFilesMap = step( + 'expect files map', + async (page: Page, bucketName: string, map: FileExpectMap) => { + const check = async (map: FileMap, stack: string[]) => { + for (const name in map) { + await openDirectoryFromAnywhere(page, stack.join('/')) + const currentDirPath = stack.join('/') + const path = `${currentDirPath}/${name}` + if (typeof map[name] === 'string') { + const state = map[name] as 'visible' | 'hidden' + if (state === 'visible') { + await fileInList(page, path) + } else { + await fileNotInList(page, path) + } } else { - await fileNotInList(page, path) + await fileInList(page, path + '/') + await check(map[name] as FileMap, stack.concat(name)) } - } else { - await fileInList(page, path + '/') - await check(map[name] as FileMap, stack.concat(name)) } } + await check(map, [bucketName]) } - await check(map, [bucketName]) -} +) diff --git a/apps/renterd-e2e/src/fixtures/hosts.ts b/apps/renterd-e2e/src/fixtures/hosts.ts index 286de1454..9dc8edad4 100644 --- a/apps/renterd-e2e/src/fixtures/hosts.ts +++ b/apps/renterd-e2e/src/fixtures/hosts.ts @@ -1,35 +1,59 @@ -import { Locator, Page } from '@playwright/test' +import { Locator, Page, expect } from '@playwright/test' +import { maybeExpectAndReturn, step } from '@siafoundation/e2e' -export function getHostRowById(page: Page, id: string) { - return page.getByTestId('hostsTable').getByTestId(id) -} +export const getHostRowById = step( + 'get host row by ID', + async (page: Page, id: string, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page.getByTestId('hostsTable').getByTestId(id), + shouldExpect + ) + } +) -export function getHostsSummaryRow(page: Page) { - return page.getByTestId('hostsTable').locator('thead').getByRole('row').nth(1) -} +export const getHostsSummaryRow = step( + 'get hosts summary row', + async (page: Page, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page.getByTestId('hostsTable').locator('thead').getByRole('row').nth(1), + shouldExpect + ) + } +) -export async function getHostRowByIndex(page: Page, index: number) { - return page - .getByTestId('hostsTable') - .locator('tbody') - .getByRole('row') - .nth(index) -} +export const getHostRowByIndex = step( + 'get host row by index', + async (page: Page, index: number, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page + .getByTestId('hostsTable') + .locator('tbody') + .getByRole('row') + .nth(index), + shouldExpect + ) + } +) -export async function getHostRows(page: Page) { - return page.getByTestId('hostsTable').locator('tbody').getByRole('row').all() -} +export const openHostContextMenu = step( + 'open host context menu', + async (page: Page, name: string) => { + const menu = page + .getByTestId('hostsTable') + .locator('tbody') + .getByRole('row', { name }) + .getByRole('button') + .first() + await expect(menu).toBeVisible() + return menu.click() + } +) -export async function openHostContextMenu(page: Page, name: string) { - await page - .getByTestId('hostsTable') - .locator('tbody') - .getByRole('row', { name }) - .getByRole('button') - .first() - .click() -} - -export async function openRowHostContextMenu(row: Locator) { - await row.getByTestId('actions').getByRole('button').first().click() -} +export const openRowHostContextMenu = step( + 'open row host context menu', + async (row: Locator) => { + const menu = row.getByTestId('actions').getByRole('button').first() + await expect(menu).toBeVisible() + return menu.click() + } +) diff --git a/apps/renterd-e2e/src/fixtures/keys.ts b/apps/renterd-e2e/src/fixtures/keys.ts index 0269bb020..338942880 100644 --- a/apps/renterd-e2e/src/fixtures/keys.ts +++ b/apps/renterd-e2e/src/fixtures/keys.ts @@ -1,41 +1,64 @@ import { Page, expect } from '@playwright/test' import { navigateToKeys } from './navigate' -import { getTextInputValueByName } from '@siafoundation/e2e' +import { + getTextInputValueByName, + maybeExpectAndReturn, + step, +} from '@siafoundation/e2e' -export function getKeyRowById(page: Page, id: string) { - return page.getByTestId('keysTable').getByTestId(id) -} +export const getKeyRowById = step( + 'get key row by ID', + async (page: Page, id: string, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page.getByTestId('keysTable').getByTestId(id), + shouldExpect + ) + } +) -export function getKeysSummaryRow(page: Page) { - return page.getByTestId('keysTable').locator('thead').getByRole('row').nth(1) -} +export const getKeysSummaryRow = step( + 'get keys summary row', + async (page: Page, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page.getByTestId('keysTable').locator('thead').getByRole('row').nth(1), + shouldExpect + ) + } +) -export function getKeyRowByIndex(page: Page, index: number) { - return page - .getByTestId('keysTable') - .locator('tbody') - .getByRole('row') - .nth(index) -} +export const getKeyRowByIndex = step( + 'get key row by index', + async (page: Page, index: number, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page + .getByTestId('keysTable') + .locator('tbody') + .getByRole('row') + .nth(index), + shouldExpect + ) + } +) -export async function getKeyRows(page: Page) { - return page.getByTestId('keysTable').locator('tbody').getByRole('row').all() -} +export const createKey = step( + 'create key', + async (page: Page): Promise => { + await navigateToKeys({ page }) + await page.getByTestId('navbar').getByText('Create keypair').click() + const dialog = page.getByRole('dialog') + const accessKeyId = await getTextInputValueByName(page, 'name') + await dialog.getByRole('button', { name: 'Create' }).click() + await getKeyRowById(page, accessKeyId, true) + await expect(dialog).toBeHidden() + return accessKeyId + } +) -export async function createKey(page: Page): Promise { - await navigateToKeys({ page }) - await page.getByTestId('navbar').getByText('Create keypair').click() - const dialog = page.getByRole('dialog') - const accessKeyId = await getTextInputValueByName(page, 'name') - await dialog.getByRole('button', { name: 'Create' }).click() - const row = getKeyRowById(page, accessKeyId) - await expect(dialog).toBeHidden() - await expect(row).toBeVisible() - return accessKeyId -} - -export async function openKeyContextMenu(page: Page, key: string) { - const selector = page.getByTestId(key).getByLabel('key context menu') - await expect(selector).toBeVisible() - await selector.click() -} +export const openKeyContextMenu = step( + 'open key context menu', + async (page: Page, key: string) => { + const selector = page.getByTestId(key).getByLabel('key context menu') + await expect(selector).toBeVisible() + await selector.click() + } +) diff --git a/apps/renterd-e2e/src/fixtures/login.ts b/apps/renterd-e2e/src/fixtures/login.ts index 549c7a3e3..5f7547b0e 100644 --- a/apps/renterd-e2e/src/fixtures/login.ts +++ b/apps/renterd-e2e/src/fixtures/login.ts @@ -1,21 +1,24 @@ import { Page, expect } from '@playwright/test' -import { fillTextInputByName } from '@siafoundation/e2e' +import { fillTextInputByName, step } from '@siafoundation/e2e' -export async function login({ - page, - address, - password, -}: { - page: Page - address: string - password: string -}) { - await page.goto('/login') - await expect(page).toHaveTitle('renterd') - await page.getByLabel('login settings').click() - await page.getByRole('menuitem', { name: 'Show custom API' }).click() - await fillTextInputByName(page, 'api', address) - await fillTextInputByName(page, 'password', password) - await page.locator('input[name=password]').press('Enter') - await expect(page.getByTestId('navbar').getByText('Buckets')).toBeVisible() -} +export const login = step( + 'login', + async ({ + page, + address, + password, + }: { + page: Page + address: string + password: string + }) => { + await page.goto('/login') + await expect(page).toHaveTitle('renterd') + await page.getByLabel('login settings').click() + await page.getByRole('menuitem', { name: 'Show custom API' }).click() + await fillTextInputByName(page, 'api', address) + await fillTextInputByName(page, 'password', password) + await page.locator('input[name=password]').press('Enter') + await expect(page.getByTestId('navbar').getByText('Buckets')).toBeVisible() + } +) diff --git a/apps/renterd-e2e/src/fixtures/navigate.ts b/apps/renterd-e2e/src/fixtures/navigate.ts index 695c951b5..8d810bdae 100644 --- a/apps/renterd-e2e/src/fixtures/navigate.ts +++ b/apps/renterd-e2e/src/fixtures/navigate.ts @@ -1,38 +1,57 @@ import { Page, expect } from '@playwright/test' +import { step } from '@siafoundation/e2e' -export async function navigateToBuckets({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Files').click() - await expect(page.getByTestId('navbar').getByText('Buckets')).toBeVisible() -} +export const navigateToBuckets = step( + 'navigate to buckets', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Files').click() + await expect(page.getByTestId('navbar').getByText('Buckets')).toBeVisible() + } +) -export async function navigateToContracts({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Contracts').click() - await expect( - page.getByTestId('navbar').getByText('Active contracts') - ).toBeVisible() -} +export const navigateToContracts = step( + 'navigate to contracts', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Contracts').click() + await expect( + page.getByTestId('navbar').getByText('Active contracts') + ).toBeVisible() + } +) -export async function navigateToConfig({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Configuration').click() - await expect( - page.getByTestId('navbar').getByText('Configuration') - ).toBeVisible() -} +export const navigateToConfig = step( + 'navigate to config', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Configuration').click() + await expect( + page.getByTestId('navbar').getByText('Configuration') + ).toBeVisible() + } +) -export async function navigateToWallet(page: Page) { - await page.getByTestId('sidenav').getByLabel('Wallet').click() - await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible() -} +export const navigateToWallet = step( + 'navigate to wallet', + async (page: Page) => { + await page.getByTestId('sidenav').getByLabel('Wallet').click() + await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible() + } +) -export async function navigateToHosts({ page }: { page: Page }) { - await page.getByTestId('sidenav').getByLabel('Hosts').click() - await expect(page.getByTestId('navbar').getByText('Hosts')).toBeVisible() -} +export const navigateToHosts = step( + 'navigate to hosts', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Hosts').click() + await expect(page.getByTestId('navbar').getByText('Hosts')).toBeVisible() + } +) -export async function navigateToKeys({ page }: { page: Page }) { - await page - .getByTestId('sidenav') - .getByLabel('S3 authentication keypairs') - .click() - await expect(page.getByTestId('navbar').getByText('Keys')).toBeVisible() -} +export const navigateToKeys = step( + 'navigate to keys', + async ({ page }: { page: Page }) => { + await page + .getByTestId('sidenav') + .getByLabel('S3 authentication keypairs') + .click() + await expect(page.getByTestId('navbar').getByText('Keys')).toBeVisible() + } +) diff --git a/apps/renterd-e2e/src/specs/files.spec.ts b/apps/renterd-e2e/src/specs/files.spec.ts index 6c46dc796..bd878523d 100644 --- a/apps/renterd-e2e/src/specs/files.spec.ts +++ b/apps/renterd-e2e/src/specs/files.spec.ts @@ -195,12 +195,15 @@ test('batch delete across nested directories', async ({ page }) => { await openBucket(page, bucketName) // Select entire dir1. - await getFileRowById(page, 'bucket1/dir1/').click() + const dir1 = await getFileRowById(page, 'bucket1/dir1/') + await dir1.click() await openDirectory(page, 'bucket1/dir2/') // Select file3 and file4. - await getFileRowById(page, 'bucket1/dir2/file3.txt').click() - await getFileRowById(page, 'bucket1/dir2/file4.txt').click() + const file3 = await getFileRowById(page, 'bucket1/dir2/file3.txt') + await file3.click() + const file4 = await getFileRowById(page, 'bucket1/dir2/file4.txt') + await file4.click() const menu = page.getByLabel('file multiselect menu') // Delete selected files. @@ -240,10 +243,13 @@ test('batch delete using the all files explorer mode', async ({ page }) => { await page.getByRole('menuitem', { name: 'All files' }).click() // Select entire dir1. - await getFileRowById(page, 'bucket1/dir1/').click() + const dir1 = await getFileRowById(page, 'bucket1/dir1/') + await dir1.click() // Select file3 and file4. - await getFileRowById(page, 'bucket1/dir2/file3.txt').click() - await getFileRowById(page, 'bucket1/dir2/file4.txt').click() + const file3 = await getFileRowById(page, 'bucket1/dir2/file3.txt') + await file3.click() + const file4 = await getFileRowById(page, 'bucket1/dir2/file4.txt') + await file4.click() const menu = page.getByLabel('file multiselect menu') // Delete selected files. diff --git a/apps/renterd-e2e/src/specs/keys.spec.ts b/apps/renterd-e2e/src/specs/keys.spec.ts index d937d13a8..67419540c 100644 --- a/apps/renterd-e2e/src/specs/keys.spec.ts +++ b/apps/renterd-e2e/src/specs/keys.spec.ts @@ -17,7 +17,7 @@ test.afterEach(async () => { test('create and delete a key', async ({ page }) => { const key = await createKey(page) - const row = getKeyRowById(page, key) + const row = await getKeyRowById(page, key, true) await openKeyContextMenu(page, key) await page.getByRole('menu').getByText('Delete').click() const dialog = page.getByRole('dialog') @@ -30,13 +30,13 @@ test('batch delete multiple keys', async ({ page }) => { const key1 = await createKey(page) const key2 = await createKey(page) const key3 = await createKey(page) - const row1 = getKeyRowById(page, key1) - const row2 = getKeyRowById(page, key2) - const row3 = getKeyRowById(page, key3) + const row1 = await getKeyRowById(page, key1, true) + const row2 = await getKeyRowById(page, key2, true) + const row3 = await getKeyRowById(page, key3, true) // There are 4 keys total. Get the first and last row. - const rowIdx0 = getKeyRowByIndex(page, 0) - const rowIdx3 = getKeyRowByIndex(page, 3) + const rowIdx0 = await getKeyRowByIndex(page, 0, true) + const rowIdx3 = await getKeyRowByIndex(page, 3, true) // Select all 4 keys. await rowIdx0.click() diff --git a/apps/walletd-e2e/src/fixtures/events.ts b/apps/walletd-e2e/src/fixtures/events.ts index d349f97fa..c29dd5651 100644 --- a/apps/walletd-e2e/src/fixtures/events.ts +++ b/apps/walletd-e2e/src/fixtures/events.ts @@ -1,9 +1,16 @@ +import { maybeExpectAndReturn, step } from '@siafoundation/e2e' import { Page } from 'playwright' -export async function getEventRowByIndex(page: Page, index: number) { - return page - .getByTestId('eventsTable') - .locator('tbody') - .getByRole('row') - .nth(index) -} +export const getEventRowByIndex = step( + 'get event row by index', + async (page: Page, index: number, shouldExpect?: boolean) => { + return maybeExpectAndReturn( + page + .getByTestId('eventsTable') + .locator('tbody') + .getByRole('row') + .nth(index), + shouldExpect + ) + } +) diff --git a/apps/walletd-e2e/src/fixtures/login.ts b/apps/walletd-e2e/src/fixtures/login.ts index 766f96b16..bb8984495 100644 --- a/apps/walletd-e2e/src/fixtures/login.ts +++ b/apps/walletd-e2e/src/fixtures/login.ts @@ -1,21 +1,24 @@ import { Page, expect } from '@playwright/test' -import { fillTextInputByName } from '@siafoundation/e2e' +import { fillTextInputByName, step } from '@siafoundation/e2e' -export async function login({ - page, - address, - password, -}: { - page: Page - address: string - password: string -}) { - await page.goto('/login') - await expect(page).toHaveTitle('walletd') - await page.getByLabel('login settings').click() - await page.getByRole('menuitem', { name: 'Show custom API' }).click() - await fillTextInputByName(page, 'api', address) - await fillTextInputByName(page, 'password', password) - await page.locator('input[name=password]').press('Enter') - await expect(page.getByTestId('navbar').getByText('Wallets')).toBeVisible() -} +export const login = step( + 'login', + async ({ + page, + address, + password, + }: { + page: Page + address: string + password: string + }) => { + await page.goto('/login') + await expect(page).toHaveTitle('walletd') + await page.getByLabel('login settings').click() + await page.getByRole('menuitem', { name: 'Show custom API' }).click() + await fillTextInputByName(page, 'api', address) + await fillTextInputByName(page, 'password', password) + await page.locator('input[name=password]').press('Enter') + await expect(page.getByTestId('navbar').getByText('Wallets')).toBeVisible() + } +) diff --git a/apps/walletd-e2e/src/fixtures/navigate.ts b/apps/walletd-e2e/src/fixtures/navigate.ts new file mode 100644 index 000000000..4998f3efe --- /dev/null +++ b/apps/walletd-e2e/src/fixtures/navigate.ts @@ -0,0 +1,12 @@ +import { Page, expect } from '@playwright/test' +import { openWallet } from './wallet' +import { step } from '@siafoundation/e2e' + +export const navigateToWallet = step( + 'navigate to wallet', + async (page: Page, name: string) => { + await page.getByLabel('Dashboard').click() + await openWallet(page, name) + await expect(page.getByTestId('navbar').getByText(name)).toBeVisible() + } +) diff --git a/apps/walletd-e2e/src/fixtures/navigateToWallet.ts b/apps/walletd-e2e/src/fixtures/navigateToWallet.ts deleted file mode 100644 index cd790c7e6..000000000 --- a/apps/walletd-e2e/src/fixtures/navigateToWallet.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Page, expect } from '@playwright/test' -import { openWallet } from './wallet' - -export async function navigateToWallet(page: Page, name: string) { - await page.getByLabel('Dashboard').click() - await openWallet(page, name) - await expect(page.getByTestId('navbar').getByText(name)).toBeVisible() -} diff --git a/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts index 938951529..1ffa542c1 100644 --- a/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts +++ b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts @@ -1,20 +1,23 @@ import { Page } from 'playwright' -import { fillTextInputByName } from '@siafoundation/e2e' +import { fillTextInputByName, step } from '@siafoundation/e2e' -export async function fillComposeTransactionSiacoin({ - page, - receiveAddress, - changeAddress, - amount, -}: { - page: Page - receiveAddress: string - changeAddress: string - amount: string -}) { - await fillTextInputByName(page, 'receiveAddress', receiveAddress) - await page.getByLabel('customChangeAddress').click() - await fillTextInputByName(page, 'changeAddress', changeAddress) - await fillTextInputByName(page, 'siacoin', amount) - await page.getByRole('button', { name: 'Generate transaction' }).click() -} +export const fillComposeTransactionSiacoin = step( + 'fill compose transaction siacoin', + async ({ + page, + receiveAddress, + changeAddress, + amount, + }: { + page: Page + receiveAddress: string + changeAddress: string + amount: string + }) => { + await fillTextInputByName(page, 'receiveAddress', receiveAddress) + await page.getByLabel('customChangeAddress').click() + await fillTextInputByName(page, 'changeAddress', changeAddress) + await fillTextInputByName(page, 'siacoin', amount) + await page.getByRole('button', { name: 'Generate transaction' }).click() + } +) diff --git a/apps/walletd-e2e/src/fixtures/wallet.ts b/apps/walletd-e2e/src/fixtures/wallet.ts index a214ed850..3cb1f1a0d 100644 --- a/apps/walletd-e2e/src/fixtures/wallet.ts +++ b/apps/walletd-e2e/src/fixtures/wallet.ts @@ -4,144 +4,183 @@ import { clickTextareaByName, fillTextareaByName, waitForTableToReload, + step, } from '@siafoundation/e2e' -import { navigateToWallet } from './navigateToWallet' +import { navigateToWallet } from './navigate' -export async function createNewWallet(page: Page, name: string) { - await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() - await page.getByRole('button', { name: 'Add wallet' }).click() - await page.getByRole('button', { name: 'Create a wallet' }).click() - await fillTextInputByName(page, 'name', name, true) - await fillTextareaByName(page, 'description', name, true) - await clickTextareaByName(page, 'mnemonic') - const cb = await page.evaluateHandle(() => navigator.clipboard.readText()) - const mnemonic = await cb.jsonValue() - await page.getByRole('button', { name: 'Add wallet' }).click() - await expect(page.getByText(`Wallet ${name.slice(0, 5)}`)).toBeVisible() - await fillTextInputByName(page, 'mnemonic', mnemonic) - await page.getByRole('button', { name: 'Generate addresses' }).click() -} +export const createNewWallet = step( + 'create new wallet', + async (page: Page, name: string) => { + await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() + await page.getByRole('button', { name: 'Add wallet' }).click() + await page.getByRole('button', { name: 'Create a wallet' }).click() + await fillTextInputByName(page, 'name', name, true) + await fillTextareaByName(page, 'description', name, true) + await clickTextareaByName(page, 'mnemonic') + const cb = await page.evaluateHandle(() => navigator.clipboard.readText()) + const mnemonic = await cb.jsonValue() + await page.getByRole('button', { name: 'Add wallet' }).click() + await expect(page.getByText(`Wallet ${name.slice(0, 5)}`)).toBeVisible() + await fillTextInputByName(page, 'mnemonic', mnemonic) + await page.getByRole('button', { name: 'Generate addresses' }).click() + } +) -export async function recoverWallet( - page: Page, - name: string, - mnemonic: string -) { - await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() - await page.getByRole('button', { name: 'Add wallet' }).click() - await page.getByRole('button', { name: 'Recover a wallet' }).click() - await fillTextInputByName(page, 'name', name, true) - await fillTextareaByName(page, 'description', name, true) - await fillTextareaByName(page, 'mnemonic', mnemonic, true) - await page.getByRole('button', { name: 'Add wallet' }).click() - await expect(page.getByText(`Wallet ${name.slice(0, 5)}`)).toBeVisible() - await fillTextInputByName(page, 'mnemonic', mnemonic) - await page.getByRole('button', { name: 'Generate addresses' }).click() -} +export const recoverWallet = step( + 'recover wallet', + async (page: Page, name: string, mnemonic: string) => { + await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() + await page.getByRole('button', { name: 'Add wallet' }).click() + await page.getByRole('button', { name: 'Recover a wallet' }).click() + await fillTextInputByName(page, 'name', name, true) + await fillTextareaByName(page, 'description', name, true) + await fillTextareaByName(page, 'mnemonic', mnemonic, true) + await page.getByRole('button', { name: 'Add wallet' }).click() + await expect(page.getByText(`Wallet ${name.slice(0, 5)}`)).toBeVisible() + await fillTextInputByName(page, 'mnemonic', mnemonic) + await page.getByRole('button', { name: 'Generate addresses' }).click() + } +) -export async function deleteWallet(page: Page, name: string) { - await openWalletContextMenu(page, name) - await page.getByRole('menuitem', { name: 'Delete wallet' }).click() - await fillTextInputByName(page, 'name', name) - await page.locator('input[name=name]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() - await walletNotInList(page, name) -} +export const deleteWallet = step( + 'delete wallet', + async (page: Page, name: string) => { + await openWalletContextMenu(page, name) + await page.getByRole('menuitem', { name: 'Delete wallet' }).click() + await fillTextInputByName(page, 'name', name) + await page.locator('input[name=name]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + await walletNotInList(page, name) + } +) -export async function unlockWallet(page: Page, name: string, mnemonic: string) { - await openWalletContextMenu(page, name) - await page.getByRole('menuitem', { name: 'Unlock wallet' }).click() - await fillTextInputByName(page, 'mnemonic', mnemonic) - await page.locator('input[name=mnemonic]').press('Enter') - await expect(page.getByRole('dialog')).toBeHidden() -} +export const unlockWallet = step( + 'unlock wallet', + async (page: Page, name: string, mnemonic: string) => { + await openWalletContextMenu(page, name) + await page.getByRole('menuitem', { name: 'Unlock wallet' }).click() + await fillTextInputByName(page, 'mnemonic', mnemonic) + await page.locator('input[name=mnemonic]').press('Enter') + await expect(page.getByRole('dialog')).toBeHidden() + } +) -export async function rescanWallets(page: Page) { +export const rescanWallets = step('rescan wallets', async (page: Page) => { await openWalletsContextMenu(page) await page.getByRole('menuitem', { name: 'Rescan blockchain' }).click() await fillTextInputByName(page, 'rescanStartHeight', '0') await page.locator('input[name=rescanStartHeight]').press('Enter') await waitForRescanToFinish(page) -} +}) -export async function waitForRescanToFinish(page: Page) { - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1000) - const isScanning = page - .getByTestId('rescanStatusPanel') - .getByText('Rescanning the blockchain') - .isVisible() - if (isScanning) { - await expect( - page - .getByTestId('rescanStatusPanel') - .getByText('Rescanning the blockchain') - ).toBeHidden() +export const waitForRescanToFinish = step( + 'wait for rescan to finish', + async (page: Page) => { + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(1000) + const isScanning = page + .getByTestId('rescanStatusPanel') + .getByText('Rescanning the blockchain') + .isVisible() + if (isScanning) { + await expect( + page + .getByTestId('rescanStatusPanel') + .getByText('Rescanning the blockchain') + ).toBeHidden() + } } -} +) -export async function generateAddresses({ - walletName, - page, - startIndex, - count, - rescan = false, - rescanStartHeight = 0, -}: { - walletName: string - page: Page - startIndex: number - count: number - rescan?: boolean - rescanStartHeight: number -}) { - await navigateToWallet(page, walletName) - await page.getByLabel('view addresses').click() - await page.getByRole('button', { name: 'Add addresses' }).click() - await fillTextInputByName(page, 'index', startIndex.toString()) - await fillTextInputByName(page, 'count', count.toString()) - if (rescan) { - await page.getByLabel('shouldRescan').click() - await fillTextInputByName( - page, - 'rescanStartHeight', - rescanStartHeight.toString() - ) - } - await page.getByRole('button', { name: 'Generate addresses' }).click() - if (rescan) { - await waitForRescanToFinish(page) +export const generateAddresses = step( + 'generate addresses', + async ({ + walletName, + page, + startIndex, + count, + rescan = false, + rescanStartHeight = 0, + }: { + walletName: string + page: Page + startIndex: number + count: number + rescan?: boolean + rescanStartHeight: number + }) => { + await navigateToWallet(page, walletName) + await page.getByLabel('view addresses').click() + await page.getByRole('button', { name: 'Add addresses' }).click() + await fillTextInputByName(page, 'index', startIndex.toString()) + await fillTextInputByName(page, 'count', count.toString()) + if (rescan) { + await page.getByLabel('shouldRescan').click() + await fillTextInputByName( + page, + 'rescanStartHeight', + rescanStartHeight.toString() + ) + } + await page.getByRole('button', { name: 'Generate addresses' }).click() + if (rescan) { + await waitForRescanToFinish(page) + } } -} +) -export async function deleteWalletIfExists(page: Page, name: string) { - await waitForTableToReload(page, 'walletsTable') - const doesWalletExist = await page - .getByTestId('walletsTable') - .getByText(name) - .first() - .isVisible() - if (doesWalletExist) { - await deleteWallet(page, name) +export const deleteWalletIfExists = step( + 'delete wallet if exists', + async (page: Page, name: string) => { + await waitForTableToReload(page, 'walletsTable') + const doesWalletExist = await page + .getByTestId('walletsTable') + .getByText(name) + .first() + .isVisible() + if (doesWalletExist) { + await deleteWallet(page, name) + } } -} +) -export async function openWallet(page: Page, name: string) { - await page.getByRole('table').getByText(name).first().click() -} +export const openWallet = step( + 'open wallet', + async (page: Page, name: string) => { + const row = page.getByRole('table').getByText(name).first() + await expect(row).toBeVisible() + await row.click() + } +) -export async function openWalletContextMenu(page: Page, name: string) { - await page.getByRole('row', { name }).getByRole('button').first().click() -} +export const openWalletContextMenu = step( + 'open wallet context menu', + async (page: Page, name: string) => { + const menu = page.getByRole('row', { name }).getByRole('button').first() + await expect(menu).toBeVisible() + await menu.click() + } +) -export async function openWalletsContextMenu(page: Page) { - await page.getByLabel('wallet settings').click() -} +export const openWalletsContextMenu = step( + 'open wallets context menu', + async (page: Page) => { + const menu = page.getByLabel('wallet settings') + await expect(menu).toBeVisible() + await menu.click() + } +) -export async function walletInList(page: Page, name: string) { - await expect(page.getByRole('table').getByText(name).first()).toBeVisible() -} +export const walletInList = step( + 'expect wallet in list', + async (page: Page, name: string) => { + await expect(page.getByRole('table').getByText(name).first()).toBeVisible() + } +) -export async function walletNotInList(page: Page, name: string) { - await expect(page.getByRole('table').getByText(name).first()).toBeHidden() -} +export const walletNotInList = step( + 'expect wallet not in list', + async (page: Page, name: string) => { + await expect(page.getByRole('table').getByText(name).first()).toBeHidden() + } +) diff --git a/apps/walletd-e2e/src/specs/seedGenerateAddresses.spec.ts b/apps/walletd-e2e/src/specs/seedGenerateAddresses.spec.ts index 60dbf9166..478f4adc5 100644 --- a/apps/walletd-e2e/src/specs/seedGenerateAddresses.spec.ts +++ b/apps/walletd-e2e/src/specs/seedGenerateAddresses.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test' import { recoverWallet } from '../fixtures/wallet' -import { navigateToWallet } from '../fixtures/navigateToWallet' +import { navigateToWallet } from '../fixtures/navigate' import { afterTest, beforeTest } from '../fixtures/beforeTest' import { mine } from '@siafoundation/clusterd' import { diff --git a/apps/walletd-e2e/src/specs/seedSendSiacoin.spec.ts b/apps/walletd-e2e/src/specs/seedSendSiacoin.spec.ts index 4064db132..a7b076b89 100644 --- a/apps/walletd-e2e/src/specs/seedSendSiacoin.spec.ts +++ b/apps/walletd-e2e/src/specs/seedSendSiacoin.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test' import { recoverWallet, rescanWallets, walletInList } from '../fixtures/wallet' -import { navigateToWallet } from '../fixtures/navigateToWallet' +import { navigateToWallet } from '../fixtures/navigate' import { fillComposeTransactionSiacoin } from '../fixtures/sendSiacoinDialog' import { random } from '@technically/lodash' import { diff --git a/apps/walletd-e2e/src/specs/wallet.spec.ts b/apps/walletd-e2e/src/specs/wallet.spec.ts index e78d6e739..4dc4f7f0f 100644 --- a/apps/walletd-e2e/src/specs/wallet.spec.ts +++ b/apps/walletd-e2e/src/specs/wallet.spec.ts @@ -5,7 +5,7 @@ import { recoverWallet, walletInList, } from '../fixtures/wallet' -import { navigateToWallet } from '../fixtures/navigateToWallet' +import { navigateToWallet } from '../fixtures/navigate' import { afterTest, beforeTest, @@ -61,9 +61,9 @@ test('recover wallet and see existing transactions', async ({ rescanStartHeight: 0, }) await navigateToWallet(page, name) - const row1 = await getEventRowByIndex(page, 0) - const row2 = await getEventRowByIndex(page, 1) - const row3 = await getEventRowByIndex(page, 2) + const row1 = await getEventRowByIndex(page, 0, true) + const row2 = await getEventRowByIndex(page, 1, true) + const row3 = await getEventRowByIndex(page, 2, true) const today = humanDate(new Date(), { timeStyle: 'short' }) await expect(row1.getByText(today)).toBeVisible() await expect(row1.getByText('siacoin transfer')).toBeVisible() diff --git a/libs/e2e/src/fixtures/clearToasts.ts b/libs/e2e/src/fixtures/clearToasts.ts index 5827415c2..8cb742a98 100644 --- a/libs/e2e/src/fixtures/clearToasts.ts +++ b/libs/e2e/src/fixtures/clearToasts.ts @@ -1,14 +1,18 @@ import { Page } from '@playwright/test' +import { step } from './step' -export async function clearToasts({ page }: { page: Page }) { - const clearButtons = page.getByTestId('toasts').locator('button') - while ((await clearButtons.count()) > 0) { - try { - await clearButtons.first().click({ - timeout: 1000, - }) - } catch (e) { - console.log('Attempted to clear toast, but it is already detached.') +export const clearToasts = step( + 'clear toasts', + async ({ page }: { page: Page }) => { + const clearButtons = page.getByTestId('toasts').locator('button') + while ((await clearButtons.count()) > 0) { + try { + await clearButtons.first().click({ + timeout: 1000, + }) + } catch (e) { + console.log('Attempted to clear toast, but it is already detached.') + } } } -} +) diff --git a/libs/e2e/src/fixtures/click.ts b/libs/e2e/src/fixtures/click.ts index b6eac81d8..6cb26c553 100644 --- a/libs/e2e/src/fixtures/click.ts +++ b/libs/e2e/src/fixtures/click.ts @@ -1,40 +1,44 @@ import { Locator, expect } from '@playwright/test' +import { step } from './step' -export async function clickIfEnabledAndWait( - locator: Locator, - waitForLocator?: Locator -) { - const isDisabled = await locator.isDisabled() - if (!isDisabled) { +export const clickIfEnabledAndWait = step( + 'click if enabled and wait', + async (locator: Locator, waitForLocator?: Locator) => { + const isDisabled = await locator.isDisabled() + if (!isDisabled) { + await locator.click() + if (waitForLocator) { + await expect(waitForLocator).toBeVisible() + } + return true + } + return false + } +) + +export const clickAndWait = step( + 'click and wait', + async (locator: Locator, waitForLocator?: Locator) => { await locator.click() if (waitForLocator) { await expect(waitForLocator).toBeVisible() } - return true - } - return false -} - -export async function clickAndWait(locator: Locator, waitForLocator?: Locator) { - await locator.click() - if (waitForLocator) { - await expect(waitForLocator).toBeVisible() } -} +) -export async function clickIf( - locator: Locator, - clickIf: 'isVisible' | 'isDisabled' -) { - const click = await locator[clickIf]() - if (click) { - await locator.click() - return true +export const clickIf = step( + 'click if', + async (locator: Locator, clickIf: 'isVisible' | 'isDisabled') => { + const click = await locator[clickIf]() + if (click) { + await locator.click() + return true + } + return false } - return false -} +) -export async function clickTwice(locator: Locator) { +export const clickTwice = step('click twice', async (locator: Locator) => { await locator.click() await locator.click() -} +}) diff --git a/libs/e2e/src/fixtures/cmdk.ts b/libs/e2e/src/fixtures/cmdk.ts index bdf9f6b2c..adf1832cf 100644 --- a/libs/e2e/src/fixtures/cmdk.ts +++ b/libs/e2e/src/fixtures/cmdk.ts @@ -1,10 +1,11 @@ import { Page, expect } from '@playwright/test' +import { step } from './step' -export async function openCmdkMenu(page: Page) { +export const openCmdkMenu = step('open cmdk menu', async (page: Page) => { const isMac = process.platform === 'darwin' const modifier = isMac ? 'Meta' : 'Control' await page.keyboard.press(`${modifier}+k`) const dialog = page.getByRole('dialog') await expect(dialog.locator('div[cmdk-root]')).toBeVisible() return dialog -} +}) diff --git a/libs/e2e/src/fixtures/configViewMode.ts b/libs/e2e/src/fixtures/configViewMode.ts index 48bdcc112..e085fd8c5 100644 --- a/libs/e2e/src/fixtures/configViewMode.ts +++ b/libs/e2e/src/fixtures/configViewMode.ts @@ -1,24 +1,25 @@ import { Page } from '@playwright/test' import { getSwitchByLabel } from './switchValue' +import { step } from './step' -export async function setViewMode({ - page, - state, -}: { - page: Page - state: 'advanced' | 'basic' -}) { - const { el, value } = await getViewMode({ page }) - if (state !== value) { - await el.click() +export const setViewMode = step( + 'set view mode', + async ({ page, state }: { page: Page; state: 'advanced' | 'basic' }) => { + const { el, value } = await getViewMode({ page }) + if (state !== value) { + await el.click() + } } -} +) -export async function getViewMode({ page }: { page: Page }) { - await page.getByRole('button', { name: 'View' }).click() - const { el, value } = await getSwitchByLabel(page, 'configViewMode') - return { - el, - value: value ? 'advanced' : 'basic', +export const getViewMode = step( + 'get view mode', + async ({ page }: { page: Page }) => { + await page.getByRole('button', { name: 'View' }).click() + const { el, value } = await getSwitchByLabel(page, 'configViewMode') + return { + el, + value: value ? 'advanced' : 'basic', + } } -} +) diff --git a/libs/e2e/src/fixtures/expect.ts b/libs/e2e/src/fixtures/expect.ts new file mode 100644 index 000000000..277a96591 --- /dev/null +++ b/libs/e2e/src/fixtures/expect.ts @@ -0,0 +1,11 @@ +import { expect, Locator } from '@playwright/test' + +export async function maybeExpectAndReturn( + locator: Locator, + shouldExpect?: boolean +) { + if (shouldExpect) { + await expect(locator).toBeVisible() + } + return locator +} diff --git a/libs/e2e/src/fixtures/preferences.ts b/libs/e2e/src/fixtures/preferences.ts index 1f0da0239..cbef8b4dc 100644 --- a/libs/e2e/src/fixtures/preferences.ts +++ b/libs/e2e/src/fixtures/preferences.ts @@ -1,41 +1,44 @@ import { Page, expect } from '@playwright/test' import { fillSelectInputByName } from './selectInput' import { CurrencyId } from '@siafoundation/react-core' +import { step } from './step' -export async function setCurrencyDisplay( - page: Page, - display: 'sc' | 'fiat' | 'bothPreferSc' | 'bothPreferFiat', - currency?: CurrencyId -) { - await page.getByTestId('sidenav').getByLabel('App preferences').click() - await fillSelectInputByName(page, 'currencyDisplay', display) - if (currency) { - await fillSelectInputByName(page, 'currencyFiat', currency) +export const setCurrencyDisplay = step( + 'set currency display', + async ( + page: Page, + display: 'sc' | 'fiat' | 'bothPreferSc' | 'bothPreferFiat', + currency?: CurrencyId + ) => { + await page.getByTestId('sidenav').getByLabel('App preferences').click() + await fillSelectInputByName(page, 'currencyDisplay', display) + if (currency) { + await fillSelectInputByName(page, 'currencyFiat', currency) + } + await page.getByRole('dialog').getByLabel('close').click() } - await page.getByRole('dialog').getByLabel('close').click() -} +) -export async function toggleColumnVisibility( - page: Page, - name: string, - visible: boolean -) { - await page.getByLabel('configure view').click() - const configureView = page.getByRole('dialog') - const columnToggle = configureView.getByRole('checkbox', { - name, - }) +export const toggleColumnVisibility = step( + 'toggle column visibility', + async (page: Page, name: string, visible: boolean) => { + await page.getByLabel('configure view').click() + const configureView = page.getByRole('dialog') + const columnToggle = configureView.getByRole('checkbox', { + name, + }) - if (visible) { - await expect(columnToggle).toBeVisible() - if (!(await columnToggle.isChecked())) { - await columnToggle.click() - } - } else { - await expect(columnToggle).toBeHidden() - if (await columnToggle.isChecked()) { - await columnToggle.click() + if (visible) { + await expect(columnToggle).toBeVisible() + if (!(await columnToggle.isChecked())) { + await columnToggle.click() + } + } else { + await expect(columnToggle).toBeHidden() + if (await columnToggle.isChecked()) { + await columnToggle.click() + } } + await page.getByLabel('configure view').click() } - await page.getByLabel('configure view').click() -} +) diff --git a/libs/e2e/src/fixtures/selectInput.ts b/libs/e2e/src/fixtures/selectInput.ts index 2b7a628f0..38c753610 100644 --- a/libs/e2e/src/fixtures/selectInput.ts +++ b/libs/e2e/src/fixtures/selectInput.ts @@ -1,26 +1,24 @@ import { Page, expect } from '@playwright/test' +import { step } from './step' -export async function fillSelectInputByName( - page: Page, - name: string, - value: string -) { - await page.locator(`select[name="${name}"]`).click() - await page.locator(`select[name="${name}"]`).selectOption(value) -} +export const fillSelectInputByName = step( + 'fill select input by name', + async (page: Page, name: string, value: string) => { + await page.locator(`select[name="${name}"]`).click() + await page.locator(`select[name="${name}"]`).selectOption(value) + } +) -export async function expectSelectInputByName( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`select[name="${name}"]`)).toHaveValue(value) -} +export const expectSelectInputByName = step( + 'expect select input by name', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`select[name="${name}"]`)).toHaveValue(value) + } +) -export async function expectSelectInputByNameAttribute( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`select[name="${name}"]`)).toHaveAttribute(value) -} +export const expectSelectInputByNameAttribute = step( + 'expect select input by name attribute', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`select[name="${name}"]`)).toHaveAttribute(value) + } +) diff --git a/libs/e2e/src/fixtures/step.ts b/libs/e2e/src/fixtures/step.ts new file mode 100644 index 000000000..1a8b9b4f1 --- /dev/null +++ b/libs/e2e/src/fixtures/step.ts @@ -0,0 +1,10 @@ +import test from 'playwright/test' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function step( + name: string, + fn: (...args: Args) => Promise, + { box = true } = {} +): (...args: Args) => Promise { + return (...args: Args) => test.step(name, () => fn(...args), { box }) +} diff --git a/libs/e2e/src/fixtures/switchValue.ts b/libs/e2e/src/fixtures/switchValue.ts index 08b4a9440..ac30fa7af 100644 --- a/libs/e2e/src/fixtures/switchValue.ts +++ b/libs/e2e/src/fixtures/switchValue.ts @@ -1,44 +1,54 @@ import { Page, expect } from '@playwright/test' +import { step } from './step' -export async function setSwitchByLabel( - page: Page, - label: string, - state: boolean -) { - const { el, value } = await getSwitchByLabel(page, label) - if (state !== value) { - await el.click() +export const setSwitchByLabel = step( + 'set switch by label', + async (page: Page, label: string, state: boolean) => { + const { el, value } = await getSwitchByLabel(page, label) + if (state !== value) { + await el.click() + } + await expect(el).toHaveAttribute( + 'data-state', + state ? 'checked' : 'unchecked' + ) } - await expect(el).toHaveAttribute( - 'data-state', - state ? 'checked' : 'unchecked' - ) -} +) -export async function expectSwitchVisible(page: Page, label: string) { - const el = page.getByLabel(label) - await expect(el).toBeVisible() -} +export const expectSwitchVisible = step( + 'expect switch visible', + async (page: Page, label: string) => { + const el = page.getByLabel(label) + await expect(el).toBeVisible() + } +) -export async function expectSwitchNotVisible(page: Page, label: string) { - const el = page.getByLabel(label) - await expect(el).toBeHidden() -} +export const expectSwitchNotVisible = step( + 'expect switch not visible', + async (page: Page, label: string) => { + const el = page.getByLabel(label) + await expect(el).toBeHidden() + } +) -export async function getSwitchByLabel(page: Page, label: string) { - const el = page.getByLabel(label) - const value = (await el.getAttribute('data-state')) as 'checked' | 'unchecked' - return { - el, - value: value === 'checked', +export const getSwitchByLabel = step( + 'get switch by label', + async (page: Page, label: string) => { + const el = page.getByLabel(label) + const value = (await el.getAttribute('data-state')) as + | 'checked' + | 'unchecked' + return { + el, + value: value === 'checked', + } } -} +) -export async function expectSwitchByLabel( - page: Page, - label: string, - value: boolean -) { - const { value: actualValue } = await getSwitchByLabel(page, label) - expect(actualValue).toBe(value) -} +export const expectSwitchByLabel = step( + 'expect switch by label', + async (page: Page, label: string, value: boolean) => { + const { value: actualValue } = await getSwitchByLabel(page, label) + expect(actualValue).toBe(value) + } +) diff --git a/libs/e2e/src/fixtures/table.ts b/libs/e2e/src/fixtures/table.ts index a75ebba67..a1a32aded 100644 --- a/libs/e2e/src/fixtures/table.ts +++ b/libs/e2e/src/fixtures/table.ts @@ -1,10 +1,14 @@ import { Page } from '@playwright/test' +import { step } from './step' -export async function waitForTableToReload(page: Page, tableTestId: string) { - await page - .locator(`[data-testid=${tableTestId}][data-loading=true]`) - .isVisible() - await page - .locator(`[data-testid=${tableTestId}][data-loading=false]`) - .isVisible() -} +export const waitForTableToReload = step( + 'wait for table to reload', + async (page: Page, tableTestId: string) => { + await page + .locator(`[data-testid=${tableTestId}][data-loading=true]`) + .isVisible() + await page + .locator(`[data-testid=${tableTestId}][data-loading=false]`) + .isVisible() + } +) diff --git a/libs/e2e/src/fixtures/textInput.ts b/libs/e2e/src/fixtures/textInput.ts index 0261feb7b..95e376c67 100644 --- a/libs/e2e/src/fixtures/textInput.ts +++ b/libs/e2e/src/fixtures/textInput.ts @@ -1,45 +1,44 @@ import { Page, expect } from '@playwright/test' +import { step } from './step' -export async function fillTextInputByName( - page: Page, - name: string, - value: string, - tabAfterFill = false -) { - await page.locator(`input[name="${name}"]`).click() - await page.locator(`input[name="${name}"]`).fill(value) - if (tabAfterFill) { - await page.locator(`input[name="${name}"]`).press('Tab') +export const fillTextInputByName = step( + 'fill text input by name', + async (page: Page, name: string, value: string, tabAfterFill = false) => { + await page.locator(`input[name="${name}"]`).click() + await page.locator(`input[name="${name}"]`).fill(value) + if (tabAfterFill) { + await page.locator(`input[name="${name}"]`).press('Tab') + } } -} +) -export async function expectTextInputByName( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`input[name="${name}"]`)).toHaveValue(value) -} +export const expectTextInputByName = step( + 'expect text input by name', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`input[name="${name}"]`)).toHaveValue(value) + } +) -export async function expectTextInputNotVisible(page: Page, name: string) { - await expect(page.locator(`input[name="${name}"]`)).toBeHidden() -} +export const expectTextInputNotVisible = step( + 'expect text input not visible', + async (page: Page, name: string) => { + await expect(page.locator(`input[name="${name}"]`)).toBeHidden() + } +) -export async function expectTextInputByNameAttribute( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`input[name="${name}"]`)).toHaveAttribute(value) -} +export const expectTextInputByNameAttribute = step( + 'expect text input by name attribute', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`input[name="${name}"]`)).toHaveAttribute(value) + } +) -export async function getTextInputValueByName( - page: Page, - name: string, - waitForValue = true -) { - if (waitForValue) { - await expect(page.locator(`input[name="${name}"]`)).toHaveValue(/.*/) +export const getTextInputValueByName = step( + 'get text input value by name', + async (page: Page, name: string, waitForValue = true) => { + if (waitForValue) { + await expect(page.locator(`input[name="${name}"]`)).toHaveValue(/.*/) + } + return await page.locator(`input[name="${name}"]`).inputValue() } - return await page.locator(`input[name="${name}"]`).inputValue() -} +) diff --git a/libs/e2e/src/fixtures/textarea.ts b/libs/e2e/src/fixtures/textarea.ts index 24be686c8..dc86c4a84 100644 --- a/libs/e2e/src/fixtures/textarea.ts +++ b/libs/e2e/src/fixtures/textarea.ts @@ -1,38 +1,43 @@ import { Page, expect } from '@playwright/test' +import { step } from './step' -export async function clickTextareaByName(page: Page, name: string) { - await page.locator(`textarea[name="${name}"]`).click() -} +export const clickTextareaByName = step( + 'click textarea by name', + async (page: Page, name: string) => { + await page.locator(`textarea[name="${name}"]`).click() + } +) -export async function fillTextareaByName( - page: Page, - name: string, - value: string, - tabAfterFill = false -) { - await page.locator(`textarea[name="${name}"]`).click() - await page.locator(`textarea[name="${name}"]`).fill(value) - if (tabAfterFill) { - await page.locator(`textarea[name="${name}"]`).press('Tab') +export const fillTextareaByName = step( + 'fill textarea by name', + async (page: Page, name: string, value: string, tabAfterFill = false) => { + await page.locator(`textarea[name="${name}"]`).click() + await page.locator(`textarea[name="${name}"]`).fill(value) + if (tabAfterFill) { + await page.locator(`textarea[name="${name}"]`).press('Tab') + } } -} +) -export async function expectTextareaByName( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`textarea[name="${name}"]`)).toHaveValue(value) -} +export const expectTextareaByName = step( + 'expect textarea by name', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`textarea[name="${name}"]`)).toHaveValue(value) + } +) -export async function expectTextareaNotVisible(page: Page, name: string) { - await expect(page.locator(`textarea[name="${name}"]`)).toBeHidden() -} +export const expectTextareaNotVisible = step( + 'expect textarea not visible', + async (page: Page, name: string) => { + await expect(page.locator(`textarea[name="${name}"]`)).toBeHidden() + } +) -export async function expectTextareaByNameAttribute( - page: Page, - name: string, - value: string -) { - await expect(page.locator(`textarea[name="${name}"]`)).toHaveAttribute(value) -} +export const expectTextareaByNameAttribute = step( + 'expect textarea by name attribute', + async (page: Page, name: string, value: string) => { + await expect(page.locator(`textarea[name="${name}"]`)).toHaveAttribute( + value + ) + } +) diff --git a/libs/e2e/src/index.ts b/libs/e2e/src/index.ts index e3078dca3..b6614d859 100644 --- a/libs/e2e/src/index.ts +++ b/libs/e2e/src/index.ts @@ -10,3 +10,5 @@ export * from './fixtures/textInput' export * from './fixtures/textarea' export * from './fixtures/table' export * from './fixtures/skip' +export * from './fixtures/step' +export * from './fixtures/expect'