diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index cf38d5497a..0a72971f47 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -34,6 +34,8 @@ import { changeProxy, changeServerUrl, changeTimezone, + clearLocale, + clearTimezone, initializeWizard, loadWizardState, selectDistribution, @@ -121,6 +123,7 @@ const CreateImageWizard = () => { const { userData } = useGetUser(auth); const hasTrackedInitialStepRef = useRef(false); const hasTrackedWizardOpenedRef = useRef(false); + const isHostDistroDetected = useRef(!isOnPremise); const { data: blueprintDetails, @@ -247,6 +250,7 @@ const CreateImageWizard = () => { const initializeHostDistro = async () => { const distro = await getHostDistro(); dispatch(changeDistribution(distro)); + isHostDistroDetected.current = true; }; const initializeHostArch = async () => { @@ -297,15 +301,33 @@ const CreateImageWizard = () => { return; } + if (restrictions.locale.shouldHide) { + dispatch(clearLocale()); + } + + if (restrictions.timezone.shouldHide) { + if (timezone) { + dispatch(clearTimezone()); + } + return; + } + const defaultTimezone = distribution === RHEL_10 || targetEnvironments.includes('azure') ? DEFAULT_TIMEZONE : 'America/New_York'; - if (!timezone) { + if (!timezone && isHostDistroDetected.current) { dispatch(changeTimezone(defaultTimezone)); } - }, [distribution, targetEnvironments, mode, dispatch]); + }, [ + distribution, + targetEnvironments, + mode, + dispatch, + restrictions, + timezone, + ]); useEffect(() => { if (!isOnPremise && showWizardModal && !hasTrackedWizardOpenedRef.current) { diff --git a/src/store/api/distributions/tests/distributionDetailsApi.test.ts b/src/store/api/distributions/tests/distributionDetailsApi.test.ts index 3e980772f9..10769885cb 100644 --- a/src/store/api/distributions/tests/distributionDetailsApi.test.ts +++ b/src/store/api/distributions/tests/distributionDetailsApi.test.ts @@ -50,7 +50,7 @@ describe('DISTRO_DETAILS configuration', () => { }); describe('network-installer restrictions', () => { - it('should only allow locale and fips for network-installer', () => { + it('should only allow fips and locale for network-installer', () => { const networkInstallerSupported = DISTRO_DETAILS['network-installer'].supported_blueprint_options; diff --git a/src/store/api/distributions/tests/hooks.test.tsx b/src/store/api/distributions/tests/hooks.test.tsx index 7d38f3f35c..52e76b51d5 100644 --- a/src/store/api/distributions/tests/hooks.test.tsx +++ b/src/store/api/distributions/tests/hooks.test.tsx @@ -247,6 +247,7 @@ describe('useCustomizationRestrictions hook logic', () => { expect(result.packages.shouldHide).toBe(true); expect(result.filesystem.shouldHide).toBe(true); expect(result.kernel.shouldHide).toBe(true); + expect(result.timezone.shouldHide).toBe(true); expect(result.users.shouldHide).toBe(true); }); diff --git a/src/store/slices/wizard/index.ts b/src/store/slices/wizard/index.ts index 7b593533c6..60e902dff6 100644 --- a/src/store/slices/wizard/index.ts +++ b/src/store/slices/wizard/index.ts @@ -1463,6 +1463,10 @@ export const wizardSlice = createSlice({ clearLanguages: (state) => { state.locale.languages = []; }, + clearLocale: (state) => { + state.locale.languages = []; + state.locale.keyboard = ''; + }, changeKeyboard: (state, action: PayloadAction) => { state.locale.keyboard = action.payload; }, @@ -1613,6 +1617,10 @@ export const wizardSlice = createSlice({ } } }, + clearTimezone: (state) => { + state.timezone.timezone = ''; + state.timezone.ntpservers = []; + }, changeHostname: (state, action: PayloadAction) => { state.hostname = action.payload; }, @@ -1841,6 +1849,7 @@ export const { removeLanguage, replaceLanguage, clearLanguages, + clearLocale, changeKeyboard, changeBlueprintName, setIsCustomName, @@ -1874,6 +1883,7 @@ export const { changeAapTlsConfirmation, addNtpServer, removeNtpServer, + clearTimezone, changeHostname, addPort, removePort, diff --git a/src/store/slices/wizard/tests/wizardSlice.test.ts b/src/store/slices/wizard/tests/wizardSlice.test.ts index 774b8f86e3..5edae41c28 100644 --- a/src/store/slices/wizard/tests/wizardSlice.test.ts +++ b/src/store/slices/wizard/tests/wizardSlice.test.ts @@ -5,6 +5,8 @@ import wizardReducer, { changeArchitecture, changeDistribution, changeImageTypes, + clearLocale, + clearTimezone, initializeWizard, initialState, loadWizardState, @@ -232,4 +234,52 @@ describe('wizardSlice core reducers', () => { }); }); }); + + describe('clearLocale', () => { + it('should reset languages and keyboard', () => { + const stateWithLocale: wizardState = { + ...initialState, + locale: { + languages: ['en_US.UTF-8', 'fr_FR.UTF-8'], + keyboard: 'us', + }, + }; + + const result = wizardReducer(stateWithLocale, clearLocale()); + + expect(result.locale.languages).toEqual([]); + expect(result.locale.keyboard).toBe(''); + }); + + it('should be a no-op on already empty locale state', () => { + const result = wizardReducer(initialState, clearLocale()); + + expect(result.locale.languages).toEqual([]); + expect(result.locale.keyboard).toBe(''); + }); + }); + + describe('clearTimezone', () => { + it('should reset timezone and ntpservers', () => { + const stateWithTimezone: wizardState = { + ...initialState, + timezone: { + timezone: 'America/New_York', + ntpservers: ['0.pool.ntp.org', '1.pool.ntp.org'], + }, + }; + + const result = wizardReducer(stateWithTimezone, clearTimezone()); + + expect(result.timezone.timezone).toBe(''); + expect(result.timezone.ntpservers).toEqual([]); + }); + + it('should be a no-op on already empty timezone state', () => { + const result = wizardReducer(initialState, clearTimezone()); + + expect(result.timezone.timezone).toBe(''); + expect(result.timezone.ntpservers).toEqual([]); + }); + }); }); diff --git a/src/test/Components/CreateImageWizard/steps/ImageOutput/ImageOutput.test.tsx b/src/test/Components/CreateImageWizard/steps/ImageOutput/ImageOutput.test.tsx index ea45153272..c8fd7ba07b 100644 --- a/src/test/Components/CreateImageWizard/steps/ImageOutput/ImageOutput.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/ImageOutput/ImageOutput.test.tsx @@ -3,7 +3,16 @@ // Redux State → mapRequestFromState() → API Request. They could be replaced by unit // tests for the request mapper functions (mapRequestToState/mapRequestFromState) which // would be faster and more focused than full integration tests. -import { EDIT_BLUEPRINT } from '../../../../../constants'; +import { screen, within } from '@testing-library/react'; + +import { CreateBlueprintRequest } from '@/store/api/backend'; +import { clickWithWait, createUser } from '@/test/testUtils'; + +import { + CREATE_BLUEPRINT, + EDIT_BLUEPRINT, + RHEL_10, +} from '../../../../../constants'; import { mockBlueprintIds } from '../../../../fixtures/blueprints'; import { aarch64CreateBlueprintRequest, @@ -13,8 +22,14 @@ import { x86_64CreateBlueprintRequest, } from '../../../../fixtures/editMode'; import { + blueprintRequest, + enterBlueprintName, + goToReview, + interceptBlueprintRequest, interceptEditBlueprintRequest, + renderCreateMode, renderEditMode, + selectGuestImageTarget, } from '../../wizardTestUtils'; describe('Image output edit mode', () => { @@ -82,3 +97,103 @@ describe('Image output edit mode', () => { expect(receivedRequest).toEqual(expectedRequest); }); }); + +const selectNetworkInstaller = async () => { + const user = createUser(); + const checkbox = await screen.findByRole('checkbox', { + name: /Network installer checkbox/i, + }); + await clickWithWait(user, checkbox); + return checkbox; +}; + +describe('Network installer target', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('selecting network-installer shows alert and disables other checkboxes', async () => { + await renderCreateMode(); + const networkInstallerCheckbox = await selectNetworkInstaller(); + + await screen.findByText( + /This image type requires specific, minimal configuration for remote installation/i, + ); + const guestImageCheckbox = await screen.findByRole('checkbox', { + name: /Virtualization guest image/i, + }); + expect(guestImageCheckbox).toBeDisabled(); + + const bareMetalCheckbox = await screen.findByRole('checkbox', { + name: /Bare metal installer/i, + }); + expect(bareMetalCheckbox).toBeDisabled(); + + expect(networkInstallerCheckbox).toBeChecked(); + expect(networkInstallerCheckbox).toBeEnabled(); + }); + + test('selecting another target first disables network-installer', async () => { + await renderCreateMode(); + await selectGuestImageTarget(); + + const networkInstallerCheckbox = await screen.findByRole('checkbox', { + name: /Network installer checkbox/i, + }); + expect(networkInstallerCheckbox).toBeDisabled(); + }); + + test('selecting network-installer only shows base settings, advanced settings, and review steps', async () => { + await renderCreateMode(); + await selectNetworkInstaller(); + + const navigation = await screen.findByRole('navigation', { + name: /wizard steps/i, + }); + + const stepButtons = within(navigation).getAllByRole('button'); + expect(stepButtons).toHaveLength(3); + + expect( + within(navigation).getByRole('button', { name: /base settings/i }), + ).toBeInTheDocument(); + expect( + within(navigation).getByRole('button', { name: /advanced settings/i }), + ).toBeInTheDocument(); + expect( + within(navigation).getByRole('button', { name: /review/i }), + ).toBeInTheDocument(); + }); + + test('can create a blueprint with network-installer', async () => { + await renderCreateMode(); + await selectNetworkInstaller(); + await enterBlueprintName('Red Velvet'); + + await goToReview(); + + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + + const expectedRequest: CreateBlueprintRequest = { + ...blueprintRequest, + distribution: RHEL_10, + customizations: { + locale: { + languages: ['C.UTF-8'], + }, + }, + image_requests: [ + { + architecture: 'x86_64', + image_type: 'network-installer', + upload_request: { + options: {}, + type: 'aws.s3', + }, + }, + ], + }; + + expect(receivedRequest).toEqual(expectedRequest); + }); +});