From d5612187b7d1502ae13231a88e05bb76213e6511 Mon Sep 17 00:00:00 2001 From: Katarina Sieklova Date: Tue, 11 Aug 2026 10:27:24 +0200 Subject: [PATCH 1/2] Wizard: restrict multiple targets when creating a bp (HMS-11101) When a user creates a new blueprint, we want to have the options for target environments restricted to just one selection, so there will be radio buttons. However, if the user wants to edit a bp that already consisted of multiple target envs, we keep the checkboxes there (for now, until the migration is done.) --- .../components/TargetEnvironment.tsx | 4 ++- .../components/TargetEnvironmentOption.tsx | 34 +++++++++++++------ src/store/slices/wizard/output/parsers.ts | 22 +++++++----- src/store/slices/wizard/output/selectors.ts | 9 +++++ src/store/slices/wizard/output/state.ts | 1 + src/store/slices/wizard/output/types.ts | 1 + 6 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx index cef5535840..cac32074c6 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx @@ -33,6 +33,7 @@ import { selectIsOnlyNetworkInstallerSelected, selectIsoPayloadReference, selectIsOtherEnvironmentSelected, + selectUseSingleTarget, } from '@/store/slices/wizard'; import Aws from './Aws'; @@ -75,6 +76,7 @@ const TargetEnvironment = () => { const isOtherEnvironmentSelected = useAppSelector( selectIsOtherEnvironmentSelected, ); + const useSingleTarget = useAppSelector(selectUseSingleTarget); const forceShowErrors = useAppSelector(selectForceShowErrors); const { restrictions } = useCustomizationRestrictions({ @@ -196,7 +198,7 @@ const TargetEnvironment = () => { fieldId='target-environments' > - {isImageMode + {isImageMode || useSingleTarget ? 'Select a target environment.' : 'Select one or more target environments.'} diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironmentOption.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironmentOption.tsx index 2c6b6277a7..3e3659a879 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironmentOption.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironmentOption.tsx @@ -12,6 +12,7 @@ import { removeImageType, selectImageTypes, selectIsImageMode, + selectUseSingleTarget, type SupportedImageTypes, } from '@/store/slices/wizard'; @@ -33,21 +34,27 @@ const TargetEnvironmentOption = ({ const dispatch = useAppDispatch(); const environments = useAppSelector(selectImageTypes); const isImageMode = useAppSelector(selectIsImageMode); + const useSingleTarget = useAppSelector(selectUseSingleTarget); const isChecked = environments.includes(environment); + const reinitializeCloudProvider = (env: SupportedImageTypes) => { + switch (env) { + case 'aws': + dispatch(reinitializeAws()); + break; + case 'azure': + dispatch(reinitializeAzure()); + break; + case 'gcp': + dispatch(reinitializeGcp()); + break; + } + }; + const handleToggle = () => { if (isChecked) { - switch (environment) { - case 'aws': - dispatch(reinitializeAws()); - break; - case 'azure': - dispatch(reinitializeAzure()); - break; - case 'gcp': - dispatch(reinitializeGcp()); - } + reinitializeCloudProvider(environment); dispatch(removeImageType(environment)); } else { dispatch(addImageType(environment)); @@ -55,10 +62,15 @@ const TargetEnvironmentOption = ({ }; const handleSelect = () => { + for (const prev of environments) { + if (prev !== environment) { + reinitializeCloudProvider(prev); + } + } dispatch(changeImageTypes([environment])); }; - if (isImageMode) { + if (isImageMode || useSingleTarget) { return ( { return 'official'; }; -export const parseOutputFromRequest = (request: RequestLike): OutputSlice => ({ - architecture: parseArchitecture(request), - distribution: parseDistribution(request), - imageTypes: parseImageTypes(request), - imageSource: parseImageSource(request), - imageSourceType: parseImageSourceType(request), - isoPayloadReference: parseIsoPayloadRef(request), - bootcDistributions: [], -}); +export const parseOutputFromRequest = (request: RequestLike): OutputSlice => { + const imageTypes = parseImageTypes(request); + return { + architecture: parseArchitecture(request), + distribution: parseDistribution(request), + imageTypes, + imageSource: parseImageSource(request), + imageSourceType: parseImageSourceType(request), + isoPayloadReference: parseIsoPayloadRef(request), + bootcDistributions: [], + initialImageTypeCount: imageTypes.length, + }; +}; diff --git a/src/store/slices/wizard/output/selectors.ts b/src/store/slices/wizard/output/selectors.ts index 8b4650a5d5..76b0fecb8e 100644 --- a/src/store/slices/wizard/output/selectors.ts +++ b/src/store/slices/wizard/output/selectors.ts @@ -49,6 +49,15 @@ export const selectIsOfficialImage = createSelector( (imageSource) => !!imageSource && isKnownImageRef(imageSource), ); +export const selectInitialImageTypeCount = (state: RootState) => { + return state.wizard.output.initialImageTypeCount; +}; + +export const selectUseSingleTarget = createSelector( + selectInitialImageTypeCount, + (initialCount) => initialCount <= 1, +); + export const selectImageSourceFilter = createSelector( selectIsOnPremise, selectImageSource, diff --git a/src/store/slices/wizard/output/state.ts b/src/store/slices/wizard/output/state.ts index 724da67d6b..d7bf66bcc8 100644 --- a/src/store/slices/wizard/output/state.ts +++ b/src/store/slices/wizard/output/state.ts @@ -8,4 +8,5 @@ export const initialState: OutputSlice = { distribution: RHEL_10, imageSourceType: 'official', imageTypes: [], + initialImageTypeCount: 0, }; diff --git a/src/store/slices/wizard/output/types.ts b/src/store/slices/wizard/output/types.ts index 7a457b7b92..5f39d590a1 100644 --- a/src/store/slices/wizard/output/types.ts +++ b/src/store/slices/wizard/output/types.ts @@ -36,4 +36,5 @@ export type OutputSlice = { architecture: ImageRequest['architecture']; distribution: Distributions; imageTypes: SupportedImageTypes[]; + initialImageTypeCount: number; }; From ecb69fd3085cf778fc4548bbe640d93680a8eb49 Mon Sep 17 00:00:00 2001 From: Katarina Sieklova Date: Tue, 11 Aug 2026 12:51:11 +0200 Subject: [PATCH 2/2] Wizard: edit tests to work with radio buttons --- playwright/Basic/imageMode.spec.ts | 10 +- .../BootTests/ImageMode/ImageMode.boot.ts | 2 +- playwright/Cockpit/cockpit.spec.ts | 2 +- playwright/Customizations/Filesystem.spec.ts | 58 +----- playwright/Customizations/OpenSCAP.spec.ts | 9 +- .../TargetEnvironments/Azure.spec.ts | 10 +- playwright/Import/Import.spec.ts | 2 +- playwright/helpers/navHelpers.ts | 2 +- playwright/helpers/targetChooser.ts | 12 +- .../tests/TargetEnvironment.test.tsx | 179 ++++++++++++------ .../steps/ImageOutput/tests/helpers.tsx | 8 + .../tests/CreateMode.test.tsx | 14 +- .../CreateImageWizard/tests/helpers.tsx | 10 +- .../wizard/output/tests/parsers.test.ts | 1 + 14 files changed, 163 insertions(+), 156 deletions(-) diff --git a/playwright/Basic/imageMode.spec.ts b/playwright/Basic/imageMode.spec.ts index 3199d2f9c3..6e5c6c4674 100644 --- a/playwright/Basic/imageMode.spec.ts +++ b/playwright/Basic/imageMode.spec.ts @@ -81,7 +81,7 @@ test('Image mode blueprint create, edit, export, import', async ({ ).toBeHidden(); // but the guest image is await expect( - frame.getByRole('radio', { name: 'Virtualization' }), + frame.getByRole('radio', { name: /^Virtualization/ }), ).toBeVisible(); imageSourceDropdown = frame.getByRole('button', { @@ -119,7 +119,7 @@ test('Image mode blueprint create, edit, export, import', async ({ frame.getByRole('radio', { name: 'Microsoft Azure' }), ).toBeVisible(); - await frame.getByRole('radio', { name: 'Virtualization' }).click(); + await frame.getByRole('radio', { name: /^Virtualization/ }).click(); }); await test.step('Create blueprint', async () => { @@ -146,10 +146,10 @@ test('Image mode blueprint create, edit, export, import', async ({ ).toBeVisible(); await expect( - frame.getByRole('radio', { name: 'Virtualization' }), + frame.getByRole('radio', { name: /^Virtualization/ }), ).toBeVisible(); await expect( - frame.getByRole('radio', { name: 'Virtualization' }), + frame.getByRole('radio', { name: /^Virtualization/ }), ).toBeChecked(); }); @@ -187,7 +187,7 @@ test('Image mode blueprint create, edit, export, import', async ({ await expect(importedImageMode).toHaveAttribute('aria-pressed', 'true'); // Export doesn't include image_requests, so image types must be re-selected - await frame.getByRole('radio', { name: 'Virtualization' }).click(); + await frame.getByRole('radio', { name: /^Virtualization/ }).click(); // Change the name to avoid "name already exists" conflict await frame diff --git a/playwright/BootTests/ImageMode/ImageMode.boot.ts b/playwright/BootTests/ImageMode/ImageMode.boot.ts index 497c82a8a9..0d38bef8db 100644 --- a/playwright/BootTests/ImageMode/ImageMode.boot.ts +++ b/playwright/BootTests/ImageMode/ImageMode.boot.ts @@ -72,7 +72,7 @@ test('Image mode boot integration test', async ({ page, cleanup }) => { await rhelSourceOption.click(); // In image mode, arch is determined by the image source, so we only select the target - await frame.getByRole('radio', { name: 'Virtualization' }).click(); + await frame.getByRole('radio', { name: /^Virtualization/ }).click(); }); await test.step('Create blueprint', async () => { diff --git a/playwright/Cockpit/cockpit.spec.ts b/playwright/Cockpit/cockpit.spec.ts index 6ba31d6a9f..765cc9aa09 100644 --- a/playwright/Cockpit/cockpit.spec.ts +++ b/playwright/Cockpit/cockpit.spec.ts @@ -92,7 +92,7 @@ test('Cockpit AWS cloud upload', async ({ page, cleanup }) => { await expect( frame.getByRole('heading', { name: 'Base settings' }), ).toBeVisible(); - await frame.getByRole('checkbox', { name: /amazon web services/i }).click(); + await frame.getByRole('radio', { name: /amazon web services/i }).click(); await registerLater(frame); await frame.getByRole('button', { name: 'Review image' }).click(); await frame.getByRole('button', { name: 'Back', exact: true }).click(); diff --git a/playwright/Customizations/Filesystem.spec.ts b/playwright/Customizations/Filesystem.spec.ts index 3744806470..203fed7dc7 100644 --- a/playwright/Customizations/Filesystem.spec.ts +++ b/playwright/Customizations/Filesystem.spec.ts @@ -261,18 +261,7 @@ test('Filesystem configuration is hidden for ISO target only', async ({ await fillInDetails(frame, blueprintName); await test.step('Select only ISO target', async () => { - await fillInImageOutput(frame); - const imageInstallerCheckbox = frame.getByRole('checkbox', { - name: /bare metal installer/i, - }); - await imageInstallerCheckbox.click(); - - const guestImageCheckbox = frame.getByRole('checkbox', { - name: /virtualization guest image/i, - }); - if (await guestImageCheckbox.isChecked()) { - await guestImageCheckbox.click(); - } + await fillInImageOutput(frame, 'iso'); }); await test.step('Verify filesystem configuration is not available', async () => { @@ -284,48 +273,3 @@ test('Filesystem configuration is hidden for ISO target only', async ({ ).toBeHidden(); }); }); - -test('Filesystem configuration is available for ISO and other target', async ({ - page, - cleanup, -}) => { - const blueprintName = 'test-' + crypto.randomUUID(); - cleanup.add(() => deleteBlueprint(page, blueprintName)); - - await ensureAuthenticated(page); - await navigateToLandingPage(page); - const frame = ibFrame(page); - await openWizard(frame); - await fillInDetails(frame, blueprintName); - - await test.step('Select ISO and guest image targets', async () => { - await fillInImageOutput(frame); - const imageInstallerCheckbox = frame.getByRole('checkbox', { - name: /bare metal installer/i, - }); - await imageInstallerCheckbox.click(); - - const guestImageCheckbox = frame.getByRole('checkbox', { - name: /virtualization guest image/i, - }); - if (!(await guestImageCheckbox.isChecked())) { - await guestImageCheckbox.click(); - } - }); - - await test.step('Verify manual partitioning is available', async () => { - await registerLater(frame); - await frame.getByRole('button', { name: /Advanced settings/ }).click(); - - await frame - .getByRole('button', { name: /Automatic partitioning/i }) - .click(); - await frame - .getByRole('option', { name: /Basic filesystem partitioning/i }) - .click(); - - await expect( - frame.getByRole('button', { name: /Basic filesystem partitioning/i }), - ).toBeVisible(); - }); -}); diff --git a/playwright/Customizations/OpenSCAP.spec.ts b/playwright/Customizations/OpenSCAP.spec.ts index 4fddf4ecbb..9c81c9ceb8 100644 --- a/playwright/Customizations/OpenSCAP.spec.ts +++ b/playwright/Customizations/OpenSCAP.spec.ts @@ -47,14 +47,9 @@ test('Create a blueprint with OpenSCAP customization', async ({ await fillInDetails(frame, blueprintName); }); - await test.step('WSL + Installer shows WSL is not supported', async () => { - await fillInImageOutput(frame, 'wsl', 'rhel9'); - await frame.getByRole('checkbox', { name: 'Bare metal installer' }).click(); + await test.step('Select target and register later', async () => { + await fillInImageOutput(frame, 'qcow2', 'rhel9'); await registerLater(frame); - - await expect( - frame.getByText('WSL: customization is not supported'), - ).toBeVisible(); }); await test.step('Select a CIS profile then switch to None', async () => { diff --git a/playwright/Customizations/TargetEnvironments/Azure.spec.ts b/playwright/Customizations/TargetEnvironments/Azure.spec.ts index 0c5bcd77c3..ba6955170e 100644 --- a/playwright/Customizations/TargetEnvironments/Azure.spec.ts +++ b/playwright/Customizations/TargetEnvironments/Azure.spec.ts @@ -164,20 +164,16 @@ test('Deselecting Azure removes its config from the blueprint', async ({ .fill(RESOURCE_GROUP); }); - await test.step('Go back and deselect Azure', async () => { + await test.step('Go back and select a different target', async () => { await frame.getByRole('button', { name: 'Base settings' }).click(); - await selectTarget(frame, 'azure'); + await frame.getByRole('radio', { name: /^Virtualization/i }).click(); await expect( - frame.getByRole('checkbox', { name: 'Microsoft Azure' }), + frame.getByRole('radio', { name: 'Microsoft Azure' }), ).not.toBeChecked(); }); - await test.step('Select Guest Image and continue', async () => { - await frame.getByRole('checkbox', { name: /Virtualization/i }).click(); - }); - await test.step('Navigate to review and verify no Azure details', async () => { await registerLater(frame); await frame.getByRole('button', { name: 'Review image' }).click(); diff --git a/playwright/Import/Import.spec.ts b/playwright/Import/Import.spec.ts index f56cbd2423..e5aceaf5e3 100644 --- a/playwright/Import/Import.spec.ts +++ b/playwright/Import/Import.spec.ts @@ -46,7 +46,7 @@ test('Import a blueprint with invalid customization', async ({ }); await test.step('Select Virtualization and register later', async () => { - await frame.getByRole('checkbox', { name: 'Virtualization' }).click(); + await frame.getByRole('radio', { name: /^Virtualization/ }).click(); await registerLater(frame); }); diff --git a/playwright/helpers/navHelpers.ts b/playwright/helpers/navHelpers.ts index 67a5ff89af..3023f3dd67 100644 --- a/playwright/helpers/navHelpers.ts +++ b/playwright/helpers/navHelpers.ts @@ -43,7 +43,7 @@ export const fillInImageOutput = async ( if (target) { await selectTarget(page, target); } else { - await page.getByRole('checkbox', { name: 'Virtualization' }).click(); + await page.getByRole('radio', { name: /^Virtualization/ }).click(); } }; diff --git a/playwright/helpers/targetChooser.ts b/playwright/helpers/targetChooser.ts index 138c62cc12..7c03457364 100644 --- a/playwright/helpers/targetChooser.ts +++ b/playwright/helpers/targetChooser.ts @@ -13,30 +13,30 @@ export const selectTarget = async ( */ switch (target) { case 'qcow2': - await page.getByRole('checkbox', { name: 'Virtualization' }).click(); + await page.getByRole('radio', { name: /^Virtualization/ }).click(); break; case 'iso': - await page.getByRole('checkbox', { name: 'Bare metal' }).click(); + await page.getByRole('radio', { name: 'Bare metal' }).click(); break; case 'wsl': await page - .getByRole('checkbox', { name: 'Windows Subsystem for Linux' }) + .getByRole('radio', { name: 'Windows Subsystem for Linux' }) .click(); break; case 'ova': await page - .getByRole('checkbox', { + .getByRole('radio', { name: 'VMware vSphere - Open virtualization format', }) .click(); break; case 'vmdk': await page - .getByRole('checkbox', { name: 'VMware vSphere - Virtual disk' }) + .getByRole('radio', { name: 'VMware vSphere - Virtual disk' }) .click(); break; case 'azure': - await page.getByRole('checkbox', { name: /Microsoft Azure/ }).click(); + await page.getByRole('radio', { name: /Microsoft Azure/ }).click(); break; } }; diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/tests/TargetEnvironment.test.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/tests/TargetEnvironment.test.tsx index d1818a8462..6f72ee05b1 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/tests/TargetEnvironment.test.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/tests/TargetEnvironment.test.tsx @@ -12,7 +12,11 @@ import { type WizardStateOverrides, } from '@/test/testUtils'; -import { clickTargetCheckbox, renderTargetEnvironment } from './helpers'; +import { + clickTargetCheckbox, + clickTargetRadio, + renderTargetEnvironment, +} from './helpers'; import { createCustomArchitecturesHandler, createDefaultFetchHandler, @@ -34,6 +38,13 @@ afterEach(() => { fetchMock.resetMocks(); }); +const multiTargetOverrides: WizardStateOverrides = { + output: { + ...initialState.output, + initialImageTypeCount: 2, + }, +}; + describe('TargetEnvironment', () => { describe('Rendering', () => { test('renders target environment form group', async () => { @@ -44,30 +55,30 @@ describe('TargetEnvironment', () => { ).toBeInTheDocument(); }); - test('shows public cloud targets for x86_64', async () => { + test('shows public cloud targets as radios in create mode', async () => { renderTargetEnvironment(); expect( - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }), + await screen.findByRole('radio', { name: /Amazon Web Services/i }), ).toBeInTheDocument(); expect( - screen.getByRole('checkbox', { name: /Google Cloud/i }), + screen.getByRole('radio', { name: /Google Cloud/i }), ).toBeInTheDocument(); expect( - screen.getByRole('checkbox', { name: /Microsoft Azure/i }), + screen.getByRole('radio', { name: /Microsoft Azure/i }), ).toBeInTheDocument(); }); - test('shows other target options', async () => { + test('shows other target options as radios in create mode', async () => { renderTargetEnvironment(); - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }); + await screen.findByRole('radio', { name: /Amazon Web Services/i }); expect( - screen.getByRole('checkbox', { name: /Virtualization guest image/i }), + screen.getByRole('radio', { name: /Virtualization.*Guest image/i }), ).toBeInTheDocument(); expect( - screen.getByRole('checkbox', { name: /Bare metal installer/i }), + screen.getByRole('radio', { name: /Bare metal.*Installer/i }), ).toBeInTheDocument(); }); @@ -81,61 +92,63 @@ describe('TargetEnvironment', () => { }); }); - describe('Target selection', () => { - test('clicking AWS checkbox properly adds and removes aws from image types', async () => { + describe('Single-target selection (create mode)', () => { + test('selecting a radio adds the target to image types', async () => { const user = createUser(); const { store } = renderTargetEnvironment(); - await clickTargetCheckbox(user, /Amazon Web Services/i); - expect(selectImageTypes(store.getState())).toContain('aws'); - - await clickTargetCheckbox(user, /Amazon Web Services/i); - expect(selectImageTypes(store.getState())).not.toContain('aws'); + await clickTargetRadio(user, /Amazon Web Services/i); + expect(selectImageTypes(store.getState())).toEqual(['aws']); }); - test('clicking Google Cloud checkbox adds gcp to image types', async () => { + test('selecting a different radio replaces the previous selection', async () => { const user = createUser(); const { store } = renderTargetEnvironment(); - await clickTargetCheckbox(user, /Google Cloud/i); + await clickTargetRadio(user, /Amazon Web Services/i); + expect(selectImageTypes(store.getState())).toEqual(['aws']); - expect(selectImageTypes(store.getState())).toContain('gcp'); + await clickTargetRadio(user, /Google Cloud/i); + expect(selectImageTypes(store.getState())).toEqual(['gcp']); }); - test('clicking Azure checkbox adds azure to image types', async () => { + test('radio shows checked state when selected', async () => { const user = createUser(); - const { store } = renderTargetEnvironment(); + renderTargetEnvironment(); - await clickTargetCheckbox(user, /Microsoft Azure/i); + const radio = await screen.findByRole('radio', { + name: /Amazon Web Services/i, + }); - expect(selectImageTypes(store.getState())).toContain('azure'); - }); + await clickTargetRadio(user, /Amazon Web Services/i); - test('clicking guest image checkbox properly addd and removes guest-image from image types', async () => { - const user = createUser(); - const { store } = renderTargetEnvironment(); + expect(radio).toBeChecked(); + }); - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }); - await clickTargetCheckbox(user, /Virtualization guest image/i); - expect(selectImageTypes(store.getState())).toContain('guest-image'); + test('shows singular helper text', async () => { + renderTargetEnvironment(); - await clickTargetCheckbox(user, /Virtualization guest image/i); - expect(selectImageTypes(store.getState())).not.toContain('guest-image'); + expect( + await screen.findByText('Select a target environment.'), + ).toBeInTheDocument(); }); + }); - test('clicking bare metal checkbox adds image-installer to image types', async () => { - const user = createUser(); - const { store } = renderTargetEnvironment(); - - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }); - await clickTargetCheckbox(user, /Bare metal installer/i); + describe('Multi-target selection (edit with multiple targets)', () => { + test('renders checkboxes when initialImageTypeCount > 1', async () => { + renderTargetEnvironment(multiTargetOverrides); - expect(selectImageTypes(store.getState())).toContain('image-installer'); + expect( + await screen.findByRole('checkbox', { + name: /Amazon Web Services checkbox/i, + }), + ).toBeInTheDocument(); + expect(screen.queryAllByRole('radio')).toHaveLength(0); }); test('can select multiple targets', async () => { const user = createUser(); - const { store } = renderTargetEnvironment(); + const { store } = renderTargetEnvironment(multiTargetOverrides); await clickTargetCheckbox(user, /Amazon Web Services/i); await clickTargetCheckbox(user, /Google Cloud/i); @@ -146,21 +159,63 @@ describe('TargetEnvironment', () => { expect(imageTypes).toContain('gcp'); expect(imageTypes).toContain('guest-image'); }); - }); - describe('Visual state', () => { - test('checkbox shows checked state when selected', async () => { + test('clicking a checkbox toggles the target', async () => { const user = createUser(); - renderTargetEnvironment(); + const { store } = renderTargetEnvironment(multiTargetOverrides); + + await clickTargetCheckbox(user, /Amazon Web Services/i); + expect(selectImageTypes(store.getState())).toContain('aws'); + + await clickTargetCheckbox(user, /Amazon Web Services/i); + expect(selectImageTypes(store.getState())).not.toContain('aws'); + }); + + test('shows plural helper text', async () => { + renderTargetEnvironment(multiTargetOverrides); + + expect( + await screen.findByText('Select one or more target environments.'), + ).toBeInTheDocument(); + }); + + test('still shows checkboxes after unchecking down to one target', async () => { + const user = createUser(); + renderTargetEnvironment({ + output: { + ...initialState.output, + imageTypes: ['aws', 'gcp', 'guest-image'], + initialImageTypeCount: 3, + }, + }); - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }); - const checkbox = screen.getByRole('checkbox', { - name: /Virtualization guest image/i, + await screen.findByRole('checkbox', { + name: /Amazon Web Services checkbox/i, }); - await clickTargetCheckbox(user, /Virtualization guest image/i); + await clickTargetCheckbox(user, /Amazon Web Services/i); + await clickTargetCheckbox(user, /Google Cloud/i); - expect(checkbox).toBeChecked(); + expect( + screen.getByRole('checkbox', { + name: /Virtualization guest image checkbox/i, + }), + ).toBeInTheDocument(); + }); + + test('renders radios when editing a single-target blueprint', async () => { + renderTargetEnvironment({ + output: { + ...initialState.output, + imageTypes: ['aws'], + initialImageTypeCount: 1, + }, + }); + + expect( + await screen.findByRole('radio', { name: /Amazon Web Services/i }), + ).toBeInTheDocument(); + expect(screen.queryAllByRole('checkbox')).toHaveLength(0); }); }); @@ -178,16 +233,23 @@ describe('TargetEnvironment', () => { output: { ...initialState.output, imageTypes: ['network-installer'], + initialImageTypeCount: 2, }, }); - await screen.findByRole('checkbox', { name: /Network installer/i }); + await screen.findByRole('checkbox', { + name: /Network installer checkbox/i, + }); expect( - screen.getByRole('checkbox', { name: /Virtualization guest image/i }), + screen.getByRole('checkbox', { + name: /Virtualization guest image checkbox/i, + }), ).toBeDisabled(); expect( - screen.getByRole('checkbox', { name: /Bare metal installer/i }), + screen.getByRole('checkbox', { + name: /Bare metal installer checkbox/i, + }), ).toBeDisabled(); }); @@ -196,6 +258,7 @@ describe('TargetEnvironment', () => { output: { ...initialState.output, imageTypes: ['network-installer'], + initialImageTypeCount: 2, }, }); @@ -211,21 +274,27 @@ describe('TargetEnvironment', () => { output: { ...initialState.output, imageTypes: ['guest-image'], + initialImageTypeCount: 2, }, }); const networkInstallerCheckbox = await screen.findByRole('checkbox', { - name: /Network installer/i, + name: /Network installer checkbox/i, }); expect(networkInstallerCheckbox).toBeDisabled(); }); test('network installer checkbox is enabled when no other targets selected', async () => { - renderTargetEnvironment(); + renderTargetEnvironment({ + output: { + ...initialState.output, + initialImageTypeCount: 2, + }, + }); const networkInstallerCheckbox = await screen.findByRole('checkbox', { - name: /Network installer/i, + name: /Network installer checkbox/i, }); expect(networkInstallerCheckbox).toBeEnabled(); diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/tests/helpers.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/tests/helpers.tsx index f93401a2ea..9157026d0e 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/tests/helpers.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/tests/helpers.tsx @@ -106,6 +106,14 @@ export const clickTargetCheckbox = async ( const checkbox = await screen.findByRole('checkbox', { name: checkboxLabel }); await clickWithWait(user, checkbox); }; + +export const clickTargetRadio = async ( + user: UserEventInstance, + radioLabel: RegExp | string, +) => { + const radio = await screen.findByRole('radio', { name: radioLabel }); + await clickWithWait(user, radio); +}; // BlueprintMode render function (uses on-premise store) export const renderBlueprintMode = ( wizardStateOverrides: WizardStateOverrides = {}, diff --git a/src/Components/CreateImageWizard/tests/CreateMode.test.tsx b/src/Components/CreateImageWizard/tests/CreateMode.test.tsx index 0bdbf3c693..565b59ff65 100644 --- a/src/Components/CreateImageWizard/tests/CreateMode.test.tsx +++ b/src/Components/CreateImageWizard/tests/CreateMode.test.tsx @@ -3,7 +3,7 @@ import { vi } from 'vitest'; import { composeHandlers, createArchitecturesHandler } from '@/test/testUtils'; -import { renderCreateMode, testCheckbox } from './helpers'; +import { renderCreateMode, testRadio } from './helpers'; import { createDefaultFetchHandler, fetchMock, @@ -76,7 +76,7 @@ describe('Keyboard accessibility', () => { fetchMock.disableMocks(); }); - test('target environment checkboxes are keyboard selectable', async () => { + test('target environment radios are keyboard selectable', async () => { await renderCreateMode(); await waitFor(() => @@ -87,14 +87,8 @@ describe('Keyboard accessibility', () => { ).not.toBeInTheDocument(), ); - await testCheckbox( - await screen.findByRole('checkbox', { name: /Amazon Web Services/i }), - ); - await testCheckbox( - await screen.findByRole('checkbox', { name: /Google Cloud/i }), - ); - await testCheckbox( - await screen.findByRole('checkbox', { name: /Microsoft Azure/i }), + await testRadio( + await screen.findByRole('radio', { name: /Amazon Web Services/i }), ); }); }); diff --git a/src/Components/CreateImageWizard/tests/helpers.tsx b/src/Components/CreateImageWizard/tests/helpers.tsx index d559dee536..4641ecb0e9 100644 --- a/src/Components/CreateImageWizard/tests/helpers.tsx +++ b/src/Components/CreateImageWizard/tests/helpers.tsx @@ -147,19 +147,19 @@ export const renderImportMode = async ( }); }; -export const testCheckbox = async (checkbox: HTMLElement) => { +export const testRadio = async (radio: HTMLElement) => { const user = createUser(); - checkbox.focus(); + radio.focus(); await keyboardWithWait(user, ' '); - expect(checkbox).toBeChecked(); + expect(radio).toBeChecked(); }; export const selectGuestImage = async (user: UserEventInstance) => { await clickWithWait( user, - await screen.findByRole('checkbox', { - name: /virtualization guest image/i, + await screen.findByRole('radio', { + name: /virtualization.*guest image/i, }), ); }; diff --git a/src/store/slices/wizard/output/tests/parsers.test.ts b/src/store/slices/wizard/output/tests/parsers.test.ts index 46307e8f82..8f1d360446 100644 --- a/src/store/slices/wizard/output/tests/parsers.test.ts +++ b/src/store/slices/wizard/output/tests/parsers.test.ts @@ -64,6 +64,7 @@ describe('parseOutputFromRequest', () => { isoPayloadReference: undefined, imageTypes: ['guest-image'], bootcDistributions: [], + initialImageTypeCount: 1, }); });