From 1356f1de906ceb4e963b3a0a85c5486ab785007b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Feb 2026 10:12:49 -0800 Subject: [PATCH 1/5] Add CPU platform to instance create form and settings tab --- app/forms/instance-create.tsx | 16 +++ .../project/instances/CpuPlatformCard.tsx | 98 +++++++++++++++++++ app/pages/project/instances/SettingsTab.tsx | 2 + app/util/cpu-platform.ts | 17 ++++ app/util/links.ts | 5 + mock-api/instance.ts | 1 + mock-api/msw/handlers.ts | 9 +- test/e2e/instance-cpu-platform.e2e.ts | 54 ++++++++++ test/e2e/instance-create.e2e.ts | 25 +++++ 9 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 app/pages/project/instances/CpuPlatformCard.tsx create mode 100644 app/util/cpu-platform.ts create mode 100644 test/e2e/instance-cpu-platform.e2e.ts diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index e772930020..4637b6d34c 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -55,6 +55,7 @@ import { import { FileField } from '~/components/form/fields/FileField' import { BootDiskImageSelectField as ImageSelectField } from '~/components/form/fields/ImageSelectField' import { IpPoolSelector } from '~/components/form/fields/IpPoolSelector' +import { ListboxField } from '~/components/form/fields/ListboxField' import { NameField } from '~/components/form/fields/NameField' import { NetworkInterfaceField } from '~/components/form/fields/NetworkInterfaceField' import { NumberField } from '~/components/form/fields/NumberField' @@ -83,6 +84,7 @@ import { TipIcon } from '~/ui/lib/TipIcon' import { Tooltip } from '~/ui/lib/Tooltip' import { Wrap } from '~/ui/util/wrap' import { ALL_ISH } from '~/util/consts' +import { cpuPlatformItems, type FormCpuPlatform } from '~/util/cpu-platform' import { readBlobAsBase64 } from '~/util/file' import { ipHasVersion } from '~/util/ip' import { docLinks, links } from '~/util/links' @@ -148,6 +150,9 @@ export type InstanceCreateInput = Assign< // Selected floating IPs to attach on create. floatingIps: NameOrId[] + + // CPU platform preference + cpuPlatform: FormCpuPlatform } > @@ -218,6 +223,7 @@ const baseDefaultValues: InstanceCreateInput = { ephemeralIpPool: '', assignEphemeralIp: false, floatingIps: [], + cpuPlatform: 'none', } export async function clientLoader({ params }: LoaderFunctionArgs) { @@ -470,6 +476,7 @@ export default function CreateInstanceForm() { description: values.description, memory: instance.memory * GiB, ncpus: instance.ncpus, + cpuPlatform: values.cpuPlatform === 'none' ? null : values.cpuPlatform, disks: values.otherDisks.map( (d): InstanceDiskAttachment => d.action === 'attach' @@ -730,6 +737,15 @@ export default function CreateInstanceForm() { /> Advanced + } diff --git a/app/pages/project/instances/CpuPlatformCard.tsx b/app/pages/project/instances/CpuPlatformCard.tsx new file mode 100644 index 0000000000..772e3bdae2 --- /dev/null +++ b/app/pages/project/instances/CpuPlatformCard.tsx @@ -0,0 +1,98 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { useForm } from 'react-hook-form' +import { match } from 'ts-pattern' + +import { api, q, queryClient, useApiMutation, usePrefetchedQuery } from '~/api' +import { ListboxField } from '~/components/form/fields/ListboxField' +import { useInstanceSelector } from '~/hooks/use-params' +import { addToast } from '~/stores/toast' +import { Button } from '~/ui/lib/Button' +import { CardBlock, LearnMore } from '~/ui/lib/CardBlock' +import { cpuPlatformItems, type FormCpuPlatform } from '~/util/cpu-platform' +import { docLinks } from '~/util/links' + +type FormValues = { + cpuPlatform: FormCpuPlatform +} + +export function CpuPlatformCard() { + const instanceSelector = useInstanceSelector() + + const { data: instance } = usePrefetchedQuery( + q(api.instanceView, { + path: { instance: instanceSelector.instance }, + query: { project: instanceSelector.project }, + }) + ) + + const instanceUpdate = useApiMutation(api.instanceUpdate, { + onSuccess() { + queryClient.invalidateEndpoint('instanceView') + addToast({ content: 'CPU platform preference updated' }) + }, + onError(err) { + addToast({ + title: 'Could not update CPU platform preference', + content: err.message, + variant: 'error', + }) + }, + }) + + const cpuPlatform: FormCpuPlatform = instance.cpuPlatform || 'none' + const defaultValues: FormValues = { cpuPlatform } + + const form = useForm({ defaultValues }) + + const disableSubmit = form.watch('cpuPlatform') === cpuPlatform + + const onSubmit = form.handleSubmit((values) => { + instanceUpdate.mutate({ + path: { instance: instanceSelector.instance }, + query: { project: instanceSelector.project }, + body: { + cpuPlatform: match(values.cpuPlatform) + .with('none', () => null) + .with('amd_milan', () => 'amd_milan' as const) + .with('amd_turin', () => 'amd_turin' as const) + .exhaustive(), + ncpus: instance.ncpus, + memory: instance.memory, + autoRestartPolicy: instance.autoRestartPolicy || null, + bootDisk: instance.bootDiskId || null, + }, + }) + }) + + return ( +
+ + + + + + + + + + +
+ ) +} diff --git a/app/pages/project/instances/SettingsTab.tsx b/app/pages/project/instances/SettingsTab.tsx index 4f80a321ae..1cb2c3fcd1 100644 --- a/app/pages/project/instances/SettingsTab.tsx +++ b/app/pages/project/instances/SettingsTab.tsx @@ -15,6 +15,7 @@ import { getInstanceSelector } from '~/hooks/use-params' import { AntiAffinityCard, instanceAntiAffinityGroups } from './AntiAffinityCard' import { AutoRestartCard } from './AutoRestartCard' +import { CpuPlatformCard } from './CpuPlatformCard' export const handle = { crumb: 'Settings' } @@ -30,6 +31,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { export default function SettingsTab() { return (
+
diff --git a/app/util/cpu-platform.ts b/app/util/cpu-platform.ts new file mode 100644 index 0000000000..34de5b3366 --- /dev/null +++ b/app/util/cpu-platform.ts @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import type { InstanceCpuPlatform } from '~/api' +import type { ListboxItem } from '~/ui/lib/Listbox' + +export type FormCpuPlatform = InstanceCpuPlatform | 'none' + +export const cpuPlatformItems: ListboxItem[] = [ + { value: 'none', label: 'No preference' }, + { value: 'amd_milan', label: 'AMD Milan' }, + { value: 'amd_turin', label: 'AMD Turin' }, +] diff --git a/app/util/links.ts b/app/util/links.ts index 41fc043c86..08365a54d7 100644 --- a/app/util/links.ts +++ b/app/util/links.ts @@ -76,6 +76,11 @@ export const docLinks = { href: 'https://docs.oxide.computer/guides/managing-instances#_update_instances', linkText: 'Instance Auto-Restart', }, + cpuPlatform: { + // TODO: Add a proper CPU Platform section to https://docs.oxide.computer/guides/deploying-workloads + href: 'https://docs.oxide.computer/api/instance_create', + linkText: 'CPU Platforms', + }, instanceActions: { href: 'https://docs.oxide.computer/guides/managing-instances', linkText: 'Instance Actions', diff --git a/mock-api/instance.ts b/mock-api/instance.ts index 8ddfbcaa7b..c4d58abf9b 100644 --- a/mock-api/instance.ts +++ b/mock-api/instance.ts @@ -21,6 +21,7 @@ const base = { auto_restart_enabled: true, ncpus: 2, memory: 4 * GiB, + cpu_platform: null, } export const instance: Json = { diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 042e85d9d0..1be23ad192 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -711,7 +711,14 @@ export const handlers = makeHandlers({ const newInstance: Json = { id: instanceId, project_id: project.id, - ...R.pick(body, ['name', 'description', 'hostname', 'memory', 'ncpus']), + ...R.pick(body, [ + 'name', + 'description', + 'hostname', + 'memory', + 'ncpus', + 'cpu_platform', + ]), ...getTimestamps(), run_state: 'creating', time_run_state_updated: new Date().toISOString(), diff --git a/test/e2e/instance-cpu-platform.e2e.ts b/test/e2e/instance-cpu-platform.e2e.ts new file mode 100644 index 0000000000..cc97f5cb72 --- /dev/null +++ b/test/e2e/instance-cpu-platform.e2e.ts @@ -0,0 +1,54 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { expect, test } from '@playwright/test' + +import { expectToast } from './utils' + +test('can update CPU platform preference', async ({ page }) => { + await page.goto('/projects/mock-project/instances/db1') + + // go to settings tab + await page.getByRole('tab', { name: 'settings' }).click() + + const save = page.getByRole('button', { name: 'Save' }) + await expect(save).toBeDisabled() + + // verify initial state is "No preference" + const platformListbox = page.getByRole('button', { name: 'Preference' }) + await expect(platformListbox).toContainText('No preference') + + // change to AMD Milan + await platformListbox.click() + await page.getByRole('option', { name: 'AMD Milan' }).click() + await expect(save).toBeEnabled() + await save.click() + + await expectToast(page, 'CPU platform preference updated') + await expect(platformListbox).toContainText('AMD Milan') + await expect(save).toBeDisabled() + + // change to AMD Turin + await platformListbox.click() + await page.getByRole('option', { name: 'AMD Turin' }).click() + await expect(save).toBeEnabled() + await save.click() + + await expectToast(page, 'CPU platform preference updated') + await expect(platformListbox).toContainText('AMD Turin') + await expect(save).toBeDisabled() + + // change back to No preference + await platformListbox.click() + await page.getByRole('option', { name: 'No preference' }).click() + await expect(save).toBeEnabled() + await save.click() + + await expectToast(page, 'CPU platform preference updated') + await expect(platformListbox).toContainText('No preference') + await expect(save).toBeDisabled() +}) diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index ebdbcb632b..d1937688dd 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -686,6 +686,31 @@ test('Validate CPU and RAM', async ({ page }) => { await expect(memMsg).toBeVisible() }) +test('can create instance with CPU platform preference', async ({ page }) => { + await page.goto('/projects/mock-project/instances-new') + + const instanceName = 'cpu-platform-instance' + await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName) + + // Select CPU platform + const cpuPlatformDropdown = page.getByRole('button', { name: 'CPU platform' }) + await expect(cpuPlatformDropdown).toContainText('No preference') + await cpuPlatformDropdown.click() + await page.getByRole('option', { name: 'AMD Milan' }).click() + await expect(cpuPlatformDropdown).toContainText('AMD Milan') + + await page.getByRole('button', { name: 'Create instance' }).click() + await closeToast(page) + + // Wait for navigation to storage tab + await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`) + + // Navigate to settings tab to verify CPU platform was set + await page.getByRole('tab', { name: 'settings' }).click() + const settingsCpuPlatformDropdown = page.getByRole('button', { name: 'Preference' }) + await expect(settingsCpuPlatformDropdown).toContainText('AMD Milan') +}) + test('create instance with IPv6-only networking', async ({ page }) => { await page.goto('/projects/mock-project/instances-new') From 7994573e18e28db479caf5111560ea7080e8db19 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Feb 2026 10:40:39 -0800 Subject: [PATCH 2/5] copy --- app/pages/project/instances/CpuPlatformCard.tsx | 2 +- app/util/cpu-platform.ts | 2 +- test/e2e/instance-cpu-platform.e2e.ts | 12 ++++++------ test/e2e/instance-create.e2e.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/pages/project/instances/CpuPlatformCard.tsx b/app/pages/project/instances/CpuPlatformCard.tsx index 772e3bdae2..ed19ac1128 100644 --- a/app/pages/project/instances/CpuPlatformCard.tsx +++ b/app/pages/project/instances/CpuPlatformCard.tsx @@ -79,7 +79,7 @@ export function CpuPlatformCard() { [] = [ - { value: 'none', label: 'No preference' }, + { value: 'none', label: 'No requirement' }, { value: 'amd_milan', label: 'AMD Milan' }, { value: 'amd_turin', label: 'AMD Turin' }, ] diff --git a/test/e2e/instance-cpu-platform.e2e.ts b/test/e2e/instance-cpu-platform.e2e.ts index cc97f5cb72..d9e5f37f06 100644 --- a/test/e2e/instance-cpu-platform.e2e.ts +++ b/test/e2e/instance-cpu-platform.e2e.ts @@ -18,9 +18,9 @@ test('can update CPU platform preference', async ({ page }) => { const save = page.getByRole('button', { name: 'Save' }) await expect(save).toBeDisabled() - // verify initial state is "No preference" - const platformListbox = page.getByRole('button', { name: 'Preference' }) - await expect(platformListbox).toContainText('No preference') + // verify initial state is "No requirement" + const platformListbox = page.getByRole('button', { name: 'Required CPU' }) + await expect(platformListbox).toContainText('No requirement') // change to AMD Milan await platformListbox.click() @@ -42,13 +42,13 @@ test('can update CPU platform preference', async ({ page }) => { await expect(platformListbox).toContainText('AMD Turin') await expect(save).toBeDisabled() - // change back to No preference + // change back to No requirement await platformListbox.click() - await page.getByRole('option', { name: 'No preference' }).click() + await page.getByRole('option', { name: 'No requirement' }).click() await expect(save).toBeEnabled() await save.click() await expectToast(page, 'CPU platform preference updated') - await expect(platformListbox).toContainText('No preference') + await expect(platformListbox).toContainText('No requirement') await expect(save).toBeDisabled() }) diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index d1937688dd..b6256c58f4 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -694,7 +694,7 @@ test('can create instance with CPU platform preference', async ({ page }) => { // Select CPU platform const cpuPlatformDropdown = page.getByRole('button', { name: 'CPU platform' }) - await expect(cpuPlatformDropdown).toContainText('No preference') + await expect(cpuPlatformDropdown).toContainText('No requirement') await cpuPlatformDropdown.click() await page.getByRole('option', { name: 'AMD Milan' }).click() await expect(cpuPlatformDropdown).toContainText('AMD Milan') @@ -707,7 +707,7 @@ test('can create instance with CPU platform preference', async ({ page }) => { // Navigate to settings tab to verify CPU platform was set await page.getByRole('tab', { name: 'settings' }).click() - const settingsCpuPlatformDropdown = page.getByRole('button', { name: 'Preference' }) + const settingsCpuPlatformDropdown = page.getByRole('button', { name: 'Required CPU' }) await expect(settingsCpuPlatformDropdown).toContainText('AMD Milan') }) From 5cfd8ab2019a05a0787c1f88b0a4f510672fb96d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Feb 2026 11:15:45 -0800 Subject: [PATCH 3/5] Update tests --- test/e2e/instance-auto-restart.e2e.ts | 55 +++++++++++++++++++-------- test/e2e/instance-cpu-platform.e2e.ts | 9 ++++- test/e2e/instance-create.e2e.ts | 9 ++++- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/test/e2e/instance-auto-restart.e2e.ts b/test/e2e/instance-auto-restart.e2e.ts index 37a171154c..60a619dc4e 100644 --- a/test/e2e/instance-auto-restart.e2e.ts +++ b/test/e2e/instance-auto-restart.e2e.ts @@ -27,13 +27,17 @@ test('Auto restart policy on failed instance', async ({ page }) => { await expect(page.getByText(/Cooldown expiration.+, 202\d.+\(5 minutes\)/)).toBeVisible() await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() - const save = page.getByRole('button', { name: 'Save' }) - await expect(save).toBeDisabled() + // Scope to the Auto-restart card to avoid ambiguity with other Save buttons + const autoRestartSection = page + .getByRole('heading', { name: 'Auto-restart' }) + .locator('..') + const save = autoRestartSection.getByRole('button', { name: 'Save' }) + const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) - const policyListbox = page.getByRole('button', { name: 'Policy' }) + await expect(save).toBeDisabled() await expect(policyListbox).toContainText('Default') - await page.getByRole('button', { name: 'Policy' }).click() + await policyListbox.click() await page.getByRole('option', { name: 'Never' }).click() await save.click() @@ -56,14 +60,17 @@ test('Auto restart policy on running instance', async ({ page }) => { await expect(page.getByText('Cooldown expirationN/A')).toBeVisible() await expect(page.getByText('Last auto-restartedN/A')).toBeVisible() - // await expect(page.getByRole('button', { name: 'Policy' })) - const save = page.getByRole('button', { name: 'Save' }) - await expect(save).toBeDisabled() + // Scope to the Auto-restart card to avoid ambiguity with other Save buttons + const autoRestartSection = page + .getByRole('heading', { name: 'Auto-restart' }) + .locator('..') + const save = autoRestartSection.getByRole('button', { name: 'Save' }) + const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) - const policyListbox = page.getByRole('button', { name: 'Policy' }) + await expect(save).toBeDisabled() await expect(policyListbox).toContainText('Default') - await page.getByRole('button', { name: 'Policy' }).click() + await policyListbox.click() await page.getByRole('option', { name: 'Never' }).click() await save.click() @@ -92,9 +99,15 @@ test('Auto restart popover, restarting soon', async ({ page }) => { ).toBeVisible() await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() - const policyListbox = page.getByRole('button', { name: 'Policy' }) + // Scope to the Auto-restart card to avoid ambiguity with other Save buttons + const autoRestartSection = page + .getByRole('heading', { name: 'Auto-restart' }) + .locator('..') + const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) + const save = autoRestartSection.getByRole('button', { name: 'Save' }) + await expect(policyListbox).toContainText('Default') - await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() + await expect(save).toBeDisabled() }) test('Auto restart popover, policy never', async ({ page }) => { @@ -114,9 +127,15 @@ test('Auto restart popover, policy never', async ({ page }) => { await expect(page.getByText(/Cooldown expiration.+, 202\d.+/)).toBeVisible() await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() - const policyListbox = page.getByRole('button', { name: 'Policy' }) + // Scope to the Auto-restart card to avoid ambiguity with other Save buttons + const autoRestartSection = page + .getByRole('heading', { name: 'Auto-restart' }) + .locator('..') + const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) + const save = autoRestartSection.getByRole('button', { name: 'Save' }) + await expect(policyListbox).toContainText('Never') - await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() + await expect(save).toBeDisabled() }) test('Auto restart popover, cooled, policy never, cooled', async ({ page }) => { @@ -137,7 +156,13 @@ test('Auto restart popover, cooled, policy never, cooled', async ({ page }) => { await expect(page.getByText('restarting soon')).toBeHidden() await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() - const policyListbox = page.getByRole('button', { name: 'Policy' }) + // Scope to the Auto-restart card to avoid ambiguity with other Save buttons + const autoRestartSection = page + .getByRole('heading', { name: 'Auto-restart' }) + .locator('..') + const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) + const save = autoRestartSection.getByRole('button', { name: 'Save' }) + await expect(policyListbox).toContainText('Never') - await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() + await expect(save).toBeDisabled() }) diff --git a/test/e2e/instance-cpu-platform.e2e.ts b/test/e2e/instance-cpu-platform.e2e.ts index d9e5f37f06..35ada1ec87 100644 --- a/test/e2e/instance-cpu-platform.e2e.ts +++ b/test/e2e/instance-cpu-platform.e2e.ts @@ -15,11 +15,16 @@ test('can update CPU platform preference', async ({ page }) => { // go to settings tab await page.getByRole('tab', { name: 'settings' }).click() - const save = page.getByRole('button', { name: 'Save' }) + // Scope to the CPU platform card to avoid ambiguity with other Save buttons + const cpuPlatformSection = page + .getByRole('heading', { name: 'CPU platform' }) + .locator('..') + const save = cpuPlatformSection.getByRole('button', { name: 'Save' }) + const platformListbox = cpuPlatformSection.getByRole('button', { name: 'Required CPU' }) + await expect(save).toBeDisabled() // verify initial state is "No requirement" - const platformListbox = page.getByRole('button', { name: 'Required CPU' }) await expect(platformListbox).toContainText('No requirement') // change to AMD Milan diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index b6256c58f4..01517cd786 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -707,7 +707,14 @@ test('can create instance with CPU platform preference', async ({ page }) => { // Navigate to settings tab to verify CPU platform was set await page.getByRole('tab', { name: 'settings' }).click() - const settingsCpuPlatformDropdown = page.getByRole('button', { name: 'Required CPU' }) + + // Scope to the CPU platform card to verify the platform was set + const cpuPlatformSection = page + .getByRole('heading', { name: 'CPU platform' }) + .locator('..') + const settingsCpuPlatformDropdown = cpuPlatformSection.getByRole('button', { + name: 'Required CPU', + }) await expect(settingsCpuPlatformDropdown).toContainText('AMD Milan') }) From cd8943a4bf5ed0a27fff4ed5d401aa02affa49bb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Feb 2026 13:51:47 -0800 Subject: [PATCH 4/5] locator adjustments --- test/e2e/instance-auto-restart.e2e.ts | 30 +++++++++++++-------------- test/e2e/instance-cpu-platform.e2e.ts | 6 +++--- test/e2e/instance-create.e2e.ts | 6 +++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/test/e2e/instance-auto-restart.e2e.ts b/test/e2e/instance-auto-restart.e2e.ts index 60a619dc4e..6a631b088e 100644 --- a/test/e2e/instance-auto-restart.e2e.ts +++ b/test/e2e/instance-auto-restart.e2e.ts @@ -28,9 +28,9 @@ test('Auto restart policy on failed instance', async ({ page }) => { await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() // Scope to the Auto-restart card to avoid ambiguity with other Save buttons - const autoRestartSection = page - .getByRole('heading', { name: 'Auto-restart' }) - .locator('..') + const autoRestartSection = page.locator('form', { + has: page.getByRole('heading', { name: 'Auto-restart' }), + }) const save = autoRestartSection.getByRole('button', { name: 'Save' }) const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) @@ -61,9 +61,9 @@ test('Auto restart policy on running instance', async ({ page }) => { await expect(page.getByText('Last auto-restartedN/A')).toBeVisible() // Scope to the Auto-restart card to avoid ambiguity with other Save buttons - const autoRestartSection = page - .getByRole('heading', { name: 'Auto-restart' }) - .locator('..') + const autoRestartSection = page.locator('form', { + has: page.getByRole('heading', { name: 'Auto-restart' }), + }) const save = autoRestartSection.getByRole('button', { name: 'Save' }) const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) @@ -100,9 +100,9 @@ test('Auto restart popover, restarting soon', async ({ page }) => { await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() // Scope to the Auto-restart card to avoid ambiguity with other Save buttons - const autoRestartSection = page - .getByRole('heading', { name: 'Auto-restart' }) - .locator('..') + const autoRestartSection = page.locator('form', { + has: page.getByRole('heading', { name: 'Auto-restart' }), + }) const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) const save = autoRestartSection.getByRole('button', { name: 'Save' }) @@ -128,9 +128,9 @@ test('Auto restart popover, policy never', async ({ page }) => { await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() // Scope to the Auto-restart card to avoid ambiguity with other Save buttons - const autoRestartSection = page - .getByRole('heading', { name: 'Auto-restart' }) - .locator('..') + const autoRestartSection = page.locator('form', { + has: page.getByRole('heading', { name: 'Auto-restart' }), + }) const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) const save = autoRestartSection.getByRole('button', { name: 'Save' }) @@ -157,9 +157,9 @@ test('Auto restart popover, cooled, policy never, cooled', async ({ page }) => { await expect(page.getByText(/Last auto-restarted.+, 202\d/)).toBeVisible() // Scope to the Auto-restart card to avoid ambiguity with other Save buttons - const autoRestartSection = page - .getByRole('heading', { name: 'Auto-restart' }) - .locator('..') + const autoRestartSection = page.locator('form', { + has: page.getByRole('heading', { name: 'Auto-restart' }), + }) const policyListbox = autoRestartSection.getByRole('button', { name: 'Policy' }) const save = autoRestartSection.getByRole('button', { name: 'Save' }) diff --git a/test/e2e/instance-cpu-platform.e2e.ts b/test/e2e/instance-cpu-platform.e2e.ts index 35ada1ec87..204ed655a9 100644 --- a/test/e2e/instance-cpu-platform.e2e.ts +++ b/test/e2e/instance-cpu-platform.e2e.ts @@ -16,9 +16,9 @@ test('can update CPU platform preference', async ({ page }) => { await page.getByRole('tab', { name: 'settings' }).click() // Scope to the CPU platform card to avoid ambiguity with other Save buttons - const cpuPlatformSection = page - .getByRole('heading', { name: 'CPU platform' }) - .locator('..') + const cpuPlatformSection = page.locator('form', { + has: page.getByRole('heading', { name: 'CPU platform' }), + }) const save = cpuPlatformSection.getByRole('button', { name: 'Save' }) const platformListbox = cpuPlatformSection.getByRole('button', { name: 'Required CPU' }) diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index 01517cd786..282dca06b3 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -709,9 +709,9 @@ test('can create instance with CPU platform preference', async ({ page }) => { await page.getByRole('tab', { name: 'settings' }).click() // Scope to the CPU platform card to verify the platform was set - const cpuPlatformSection = page - .getByRole('heading', { name: 'CPU platform' }) - .locator('..') + const cpuPlatformSection = page.locator('form', { + has: page.getByRole('heading', { name: 'CPU platform' }), + }) const settingsCpuPlatformDropdown = cpuPlatformSection.getByRole('button', { name: 'Required CPU', }) From ac8b2773822258a0684f315fcacfcf795ea0bcd6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Feb 2026 14:57:32 -0800 Subject: [PATCH 5/5] Attempt to use the reduced motion helper --- app/components/ToastStack.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/ToastStack.tsx b/app/components/ToastStack.tsx index d2f94ebc30..f503b85d06 100644 --- a/app/components/ToastStack.tsx +++ b/app/components/ToastStack.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { AnimatePresence } from 'motion/react' +import { AnimatePresence, useReducedMotion } from 'motion/react' import * as m from 'motion/react-m' import { removeToast, useToastStore } from '~/stores/toast' @@ -13,6 +13,7 @@ import { Toast } from '~/ui/lib/Toast' export function ToastStack() { const toasts = useToastStore((state) => state.toasts) + const prefersReducedMotion = useReducedMotion() return (
(