-
{title || defaultTitle[variant]}
-
{content}
+ {(title || variant !== 'success') && (
+
{title || defaultTitle[variant]}
+ )}
+ {/* 'group' is necessary for HL color trick to work. see HL.tsx */}
+
+ {content}
+
{cta && (
{
it('capitalizes the first letter', () => {
@@ -76,3 +83,30 @@ describe('titleCase', () => {
expect(titleCase('123 abc')).toBe('123 Abc')
})
})
+
+describe('extractText', () => {
+ it('extracts strings from React components', () => {
+ expect(
+ extractText(
+ <>
+ This is my
text
+ >
+ )
+ ).toBe('This is my text')
+ })
+ it('extracts strings from nested elements', () => {
+ expect(
+ extractText(
+
+ This is my{' '}
+
+ nested text
+
+
+ )
+ ).toBe('This is my nested text')
+ })
+ it('can handle regular strings', () => {
+ expect(extractText('Some more text')).toBe('Some more text')
+ })
+})
diff --git a/app/util/str.ts b/app/util/str.ts
index 934530917c..a7620050c9 100644
--- a/app/util/str.ts
+++ b/app/util/str.ts
@@ -6,6 +6,8 @@
* Copyright Oxide Computer Company
*/
+import React from 'react'
+
export const capitalize = (s: string) => s && s.charAt(0).toUpperCase() + s.slice(1)
export const pluralize = (s: string, n: number) => `${n} ${s}${n === 1 ? '' : 's'}`
@@ -55,3 +57,19 @@ export const titleCase = (text: string): string => {
* it look like `AAAAAAAAAAAAAAAA==`?
*/
export const isAllZeros = (base64Data: string) => /^A*=*$/.test(base64Data)
+
+/**
+ * Extract the string contents of a ReactNode, so <>This
highlighted text> becomes "This highlighted text"
+ */
+export const extractText = (children: React.ReactNode): string =>
+ React.Children.toArray(children)
+ .map((child) =>
+ typeof child === 'string'
+ ? child
+ : React.isValidElement(child)
+ ? extractText(child.props.children)
+ : ''
+ )
+ .join(' ')
+ .trim()
+ .replace(/\s+/g, ' ')
diff --git a/test/e2e/disks.e2e.ts b/test/e2e/disks.e2e.ts
index 03398c4cd6..e735f7dad6 100644
--- a/test/e2e/disks.e2e.ts
+++ b/test/e2e/disks.e2e.ts
@@ -5,7 +5,15 @@
*
* Copyright Oxide Computer Company
*/
-import { clickRowAction, expect, expectRowVisible, expectVisible, test } from './utils'
+import {
+ clickRowAction,
+ expect,
+ expectNoToast,
+ expectRowVisible,
+ expectToast,
+ expectVisible,
+ test,
+} from './utils'
test('List disks and snapshot', async ({ page }) => {
await page.goto('/projects/mock-project/disks')
@@ -28,8 +36,11 @@ test('List disks and snapshot', async ({ page }) => {
})
await clickRowAction(page, 'disk-1 db1', 'Snapshot')
- await expect(page.getByText("Creating snapshot of disk 'disk-1'").nth(0)).toBeVisible()
- await expect(page.getByText('Snapshot successfully created').nth(0)).toBeVisible()
+ await expectToast(page, 'Creating snapshot of disk disk-1')
+ // expectToast should have closed the toast already, but verify
+ await expectNoToast(page, 'Creating snapshot of disk disk-1')
+ // Next line is a little awkward, but we don't actually know what the snapshot name will be
+ await expectToast(page, /Snapshot disk-1-[a-z0-9]{6} created/)
})
test('Disk snapshot error', async ({ page }) => {
@@ -37,11 +48,13 @@ test('Disk snapshot error', async ({ page }) => {
// special disk that triggers snapshot error
await clickRowAction(page, 'disk-snapshot-error', 'Snapshot')
- await expect(
- page.getByText("Creating snapshot of disk 'disk-snapshot-error'").nth(0)
- ).toBeVisible()
- await expect(page.getByText('Failed to create snapshot').nth(0)).toBeVisible()
- await expect(page.getByText('Cannot snapshot disk').nth(0)).toBeVisible()
+ await expectToast(page, 'Creating snapshot of disk disk-snapshot-error')
+ // just including an actual expect to satisfy the linter
+ await expect(page.getByRole('cell', { name: 'disk-snapshot-error' })).toBeVisible()
+ // expectToast should have closed the toast already, but let's just verify …
+ await expectNoToast(page, 'Creating snapshot of disk disk-snapshot-error')
+ // … before we can check for the error toast
+ await expectToast(page, 'Failed to create snapshotCannot snapshot disk')
})
test.describe('Disk create', () => {
@@ -53,7 +66,7 @@ test.describe('Disk create', () => {
test.afterEach(async ({ page }) => {
await page.getByRole('button', { name: 'Create disk' }).click()
- await expectVisible(page, ['text="Your disk has been created"'])
+ await expectToast(page, 'Disk a-new-disk created')
await expectVisible(page, ['role=cell[name="a-new-disk"]'])
})
diff --git a/test/e2e/floating-ip-update.e2e.ts b/test/e2e/floating-ip-update.e2e.ts
index 68bcf0d05d..4ce1179c90 100644
--- a/test/e2e/floating-ip-update.e2e.ts
+++ b/test/e2e/floating-ip-update.e2e.ts
@@ -6,7 +6,14 @@
* Copyright Oxide Computer Company
*/
-import { clickRowAction, expect, expectRowVisible, expectVisible, test } from './utils'
+import {
+ clickRowAction,
+ expect,
+ expectRowVisible,
+ expectToast,
+ expectVisible,
+ test,
+} from './utils'
const floatingIpsPage = '/projects/mock-project/floating-ips'
const originalName = 'cola-float'
@@ -32,6 +39,7 @@ test('can update a floating IP', async ({ page }) => {
name: updatedName,
description: updatedDescription,
})
+ await expectToast(page, `Floating IP ${updatedName} updated`)
})
// Make sure that it still works even if the name doesn't change
@@ -47,4 +55,5 @@ test('can update *just* the floating IP description', async ({ page }) => {
name: originalName,
description: updatedDescription,
})
+ await expectToast(page, `Floating IP ${originalName} updated`)
})
diff --git a/test/e2e/images.e2e.ts b/test/e2e/images.e2e.ts
index c15f91fc80..d78f83ca0a 100644
--- a/test/e2e/images.e2e.ts
+++ b/test/e2e/images.e2e.ts
@@ -12,6 +12,7 @@ import {
clipboardText,
expect,
expectNotVisible,
+ expectToast,
expectVisible,
getPageAsUser,
selectOption,
@@ -52,7 +53,7 @@ test('can promote an image from silo', async ({ page }) => {
await page.locator('role=button[name="Promote"]').click()
// Check it was promoted successfully
- await expectVisible(page, ['text="image-1 has been promoted"'])
+ await expect(page.getByText('Image image-1 promoted', { exact: true })).toBeVisible()
await expectVisible(page, ['role=cell[name="image-1"]'])
})
@@ -68,7 +69,7 @@ test('can promote an image from project', async ({ page }) => {
// Promote image and check it was successful
await page.locator('role=button[name="Promote"]').click()
- await expectVisible(page, ['text="image-2 has been promoted"'])
+ await expect(page.getByText('Image image-2 promoted', { exact: true })).toBeVisible()
await expectNotVisible(page, ['role=cell[name="image-2"]'])
await page.click('role=link[name="View silo images"]')
@@ -111,8 +112,10 @@ test('can demote an image from silo', async ({ page }) => {
await selectOption(page, 'Project', 'mock-project')
await page.getByRole('button', { name: 'Demote' }).click()
- // Promote image and check it was successful
- await expectVisible(page, ['text="arch-2022-06-01 has been demoted"'])
+ // Demote image and check it was successful
+ await expect(
+ page.getByText('Image arch-2022-06-01 demoted', { exact: true })
+ ).toBeVisible()
await expectNotVisible(page, ['role=cell[name="arch-2022-06-01"]'])
await page.click('role=link[name="View images in mock-project"]')
@@ -132,7 +135,7 @@ test('can delete an image from a project', async ({ page }) => {
await expect(spinner).toBeVisible()
// Check deletion was successful
- await expect(page.getByText('image-3 has been deleted', { exact: true })).toBeVisible()
+ await expectToast(page, 'Image image-3 deleted')
await expect(cell).toBeHidden()
await expect(spinner).toBeHidden()
})
@@ -150,9 +153,7 @@ test('can delete an image from a silo', async ({ page }) => {
await expect(spinner).toBeVisible()
// Check deletion was successful
- await expect(
- page.getByText('ubuntu-20-04 has been deleted', { exact: true })
- ).toBeVisible()
+ await expectToast(page, 'Image ubuntu-20-04 deleted')
await expect(cell).toBeHidden()
await expect(spinner).toBeHidden()
})
diff --git a/test/e2e/instance-disks.e2e.ts b/test/e2e/instance-disks.e2e.ts
index 9e54716196..b53e872e4d 100644
--- a/test/e2e/instance-disks.e2e.ts
+++ b/test/e2e/instance-disks.e2e.ts
@@ -8,8 +8,10 @@
import {
clickRowAction,
expect,
+ expectNoToast,
expectNotVisible,
expectRowVisible,
+ expectToast,
expectVisible,
stopInstance,
test,
@@ -130,7 +132,7 @@ test('Detach disk', async ({ page }) => {
// Have to stop instance to edit disks
await stopInstance(page)
- const successMsg = page.getByText('Disk detached').nth(0)
+ const successMsg = page.getByText('Disk disk-2 detached').first()
const row = page.getByRole('row', { name: 'disk-2' })
await expect(row).toBeVisible()
await expect(successMsg).toBeHidden()
@@ -143,13 +145,13 @@ test('Detach disk', async ({ page }) => {
test('Snapshot disk', async ({ page }) => {
await page.goto('/projects/mock-project/instances/db1')
- // have to use nth with toasts because the text shows up in multiple spots
- const successMsg = page.getByText('Snapshot created').nth(0)
- await expect(successMsg).toBeHidden()
+ // we don't know the full name of the disk, but this will work to find the toast
+ const toastMessage = /Snapshot disk-1-[a-z0-9]{6} created/
+ await expectNoToast(page, toastMessage)
await clickRowAction(page, 'disk-1', 'Snapshot')
- await expect(successMsg).toBeVisible() // we see the toast!
+ await expectToast(page, toastMessage) // we see the toast!
// now go see the snapshot on the snapshots page
await page.getByRole('link', { name: 'Snapshots' }).click()
diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts
index df0de16b04..ea17e815dc 100644
--- a/test/e2e/ip-pools.e2e.ts
+++ b/test/e2e/ip-pools.e2e.ts
@@ -8,7 +8,7 @@
import { expect, test } from '@playwright/test'
-import { clickRowAction, expectRowVisible } from './utils'
+import { clickRowAction, expectRowVisible, expectToast } from './utils'
test('IP pool list', async ({ page }) => {
await page.goto('/system/networking/ip-pools')
@@ -118,10 +118,10 @@ test('IP pool delete from IP Pools list page', async ({ page }) => {
await expect(page.getByRole('dialog', { name: 'Confirm delete' })).toBeVisible()
await page.getByRole('button', { name: 'Confirm' }).click()
- await expect(page.getByText('Could not delete resource').first()).toBeVisible()
- await expect(
- page.getByText('IP pool cannot be deleted while it contains IP ranges').first()
- ).toBeVisible()
+ await expectToast(
+ page,
+ 'Could not delete resourceIP pool cannot be deleted while it contains IP ranges'
+ )
await expect(page.getByRole('cell', { name: 'ip-pool-3' })).toBeVisible()
diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts
index e74c39e0ae..8545adde8c 100644
--- a/test/e2e/utils.ts
+++ b/test/e2e/utils.ts
@@ -115,6 +115,21 @@ export async function stopInstance(page: Page) {
await expect(page.getByText('statestopped')).toBeVisible()
}
+/**
+ * Assert that a toast with text matching `expectedText` is visible.
+ */
+export async function expectToast(page: Page, expectedText: string | RegExp) {
+ await expect(page.getByTestId('Toasts')).toHaveText(expectedText)
+ await closeToast(page)
+}
+
+/**
+ * Assert that a toast with text matching `expectedText` is not visible.
+ */
+export async function expectNoToast(page: Page, expectedText: string | RegExp) {
+ await expect(page.getByTestId('Toasts')).not.toHaveText(expectedText)
+}
+
/**
* Close toast and wait for it to fade out. For some reason it prevents things
* from working, but only in tests as far as we can tell.