feat(HMS-3582): add first boot step to wizard

This commit is contained in:
Amir 2024-04-18 18:29:46 +03:00 committed by Lucas Garfield
parent c88a0323f2
commit 6f9c4f3864
23 changed files with 401 additions and 12 deletions

62
package-lock.json generated
View file

@ -11,6 +11,7 @@
"@data-driven-forms/pf4-component-mapper": "3.22.4",
"@data-driven-forms/react-form-renderer": "3.22.4",
"@patternfly/patternfly": "5.3.0",
"@patternfly/react-code-editor": "5.3.0",
"@patternfly/react-core": "5.3.0",
"@patternfly/react-table": "5.2.4",
"@redhat-cloud-services/frontend-components": "4.2.7",
@ -3208,6 +3209,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/@monaco-editor/loader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
"dependencies": {
"state-local": "^1.0.6"
},
"peerDependencies": {
"monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
"dependencies": {
"@monaco-editor/loader": "^1.4.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@mswjs/cookies": {
"version": "0.2.2",
"dev": true,
@ -3363,6 +3388,32 @@
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-5.3.0.tgz",
"integrity": "sha512-93uWA15bOJDgu8NF2iReWbbNtWdtM+v7iaDpK33mJChgej+whiFpGLtQPI2jFk1aVW3rDpbt4qm4OaNinpzSsg=="
},
"node_modules/@patternfly/react-code-editor": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.3.0.tgz",
"integrity": "sha512-myPZh+POJpnXOrDtAYq83zMa26YCuM6KsAgLeaW6riajLyZ1VE3lHztX300xaJR1sDbIvxrdZCNDqmyoJI2z1g==",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@patternfly/react-core": "^5.3.0",
"@patternfly/react-icons": "^5.3.0",
"@patternfly/react-styles": "^5.3.0",
"react-dropzone": "14.2.3",
"tslib": "^2.5.0"
},
"peerDependencies": {
"react": "^17 || ^18",
"react-dom": "^17 || ^18"
}
},
"node_modules/@patternfly/react-code-editor/node_modules/@patternfly/react-icons": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.1.tgz",
"integrity": "sha512-puiMzX39asr+j5adA3J1xuK5NjwKH4UAp57GoLTga9DcsPu0g8u0H3WHtunYCJzUQ8n7FvaMYFH1H0WcWlDIQQ==",
"peerDependencies": {
"react": "^17 || ^18",
"react-dom": "^17 || ^18"
}
},
"node_modules/@patternfly/react-component-groups": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.0.0.tgz",
@ -14320,6 +14371,12 @@
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.47.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz",
"integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw==",
"peer": true
},
"node_modules/moo-color": {
"version": "1.0.3",
"dev": true,
@ -17395,6 +17452,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
},
"node_modules/statuses": {
"version": "2.0.1",
"dev": true,

View file

@ -11,6 +11,7 @@
"@data-driven-forms/react-form-renderer": "3.22.4",
"@patternfly/patternfly": "5.3.0",
"@patternfly/react-core": "5.3.0",
"@patternfly/react-code-editor": "5.3.0",
"@patternfly/react-table": "5.2.4",
"@redhat-cloud-services/frontend-components": "4.2.7",
"@redhat-cloud-services/frontend-components-notifications": "4.1.0",

View file

@ -8,11 +8,13 @@ import {
WizardStepType,
useWizardContext,
} from '@patternfly/react-core';
import { useFlag } from '@unleash/proxy-client-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DetailsStep from './steps/Details';
import FileSystemStep from './steps/FileSystem';
import { FileSystemStepFooter } from './steps/FileSystem/FileSystemConfiguration';
import FirstBootStep from './steps/FirstBoot';
import ImageOutputStep from './steps/ImageOutput';
import OscapStep from './steps/Oscap';
import PackagesStep from './steps/Packages';
@ -126,6 +128,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
// =========================TO REMOVE=======================
const firstbootFlag = useFlag('image-builder.firstboot.enabled');
const isFirstBootEnabled = isBeta() && firstbootFlag;
// IMPORTANT: Ensure the wizard starts with a fresh initial state
useEffect(() => {
dispatch(initializeWizard());
@ -176,13 +180,22 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
) => setCurrentStep(currentStep);
const detailsValidation = useAppSelector(selectStepValidation('details'));
let startIndex = 1; // default index
if (isEdit) {
if (snapshottingEnabled) {
startIndex = isFirstBootEnabled ? 15 : 14;
} else {
startIndex = isFirstBootEnabled ? 14 : 13;
}
}
return (
<>
<ImageBuilderHeader />
<section className="pf-l-page__main-section pf-c-page__main-section">
<Wizard
startIndex={isEdit ? (snapshottingEnabled ? 14 : 13) : 1}
startIndex={startIndex}
onClose={() => navigate(resolveRelPath(''))}
onStepChange={onStepChange}
isVisitRequired
@ -334,6 +347,16 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
</WizardStep>,
]}
/>
{isFirstBootEnabled && (
<WizardStep
name="First boot script configuration"
id="wizard-first-boot"
key="wizard-first-boot"
footer={<CustomWizardFooter disableNext={false} />}
>
<FirstBootStep />
</WizardStep>
)}
<WizardStep
name="Details"
id="step-details"

View file

@ -0,0 +1,52 @@
import React from 'react';
import { CodeEditor, Language } from '@patternfly/react-code-editor';
import { Text, Form, Title, Alert } from '@patternfly/react-core';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
selectFirstBootScript,
setFirstBootScript,
} from '../../../../store/wizardSlice';
const FirstBootStep = () => {
const dispatch = useAppDispatch();
const selectedScript = useAppSelector(selectFirstBootScript);
return (
<Form>
<Title headingLevel="h1" size="xl">
First boot configuration
</Title>
<Text>
Configure the image with a custom script that will execute on its first
boot.
</Text>
<Alert
variant="warning"
isExpandable
isInline
title="Important: please do not include sensitive information"
>
<Text>
Please ensure that your script does not contain any secrets,
passwords, or other sensitive data. All scripts should be crafted
without including confidential information to maintain security and
privacy.
</Text>
</Alert>
<CodeEditor
isUploadEnabled
isDownloadEnabled
isCopyEnabled
isLanguageLabelVisible
language={Language.shell}
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
code={selectedScript}
height="35vh"
/>
</Form>
);
};
export default FirstBootStep;

View file

@ -10,6 +10,7 @@ import {
import {
ContentList,
FSCList,
FirstBootList,
ImageDetailsList,
ImageOutputList,
OscapList,
@ -48,6 +49,7 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
const [isExpandedRegistration, setIsExpandedRegistration] = useState(false);
const [isExpandedImageDetail, setIsExpandedImageDetail] = useState(false);
const [isExpandedOscapDetail, setIsExpandedOscapDetail] = useState(false);
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(false);
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
setIsExpandedImageOutput(isExpandedImageOutput);
@ -63,6 +65,8 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
setIsExpandedImageDetail(isExpandedImageDetail);
const onToggleOscapDetails = (isExpandedOscapDetail: boolean) =>
setIsExpandedOscapDetail(isExpandedOscapDetail);
const onToggleFirstBoot = (isExpandableFirstBoot: boolean) =>
setIsExpandedFirstBoot(isExpandableFirstBoot);
return (
<>
@ -175,6 +179,17 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
{/* Intentional prop drilling for simplicity - To be removed */}
<ContentList snapshottingEnabled={snapshottingEnabled} />
</ExpandableSection>
<ExpandableSection
toggleContent={'First boot'}
onToggle={(_event, isExpandableFirstBoot) =>
onToggleFirstBoot(isExpandableFirstBoot)
}
isExpanded={isExpandableFirstBoot}
isIndented
data-testid="firstboot-expandable"
>
<FirstBootList />
</ExpandableSection>
{(blueprintName || blueprintDescription) && (
<ExpandableSection
toggleContent={'Image details'}

View file

@ -60,6 +60,7 @@ import {
selectSnapshotDate,
selectUseLatest,
selectPartitions,
selectFirstBootScript,
} from '../../../../store/wizardSlice';
import {
convertMMDDYYYYToYYYYMMDD,
@ -754,3 +755,23 @@ export const ImageDetailsList = () => {
export const OscapList = () => {
return <OscapProfileInformation />;
};
export const FirstBootList = () => {
const isFirstbootEnabled = !!useAppSelector(selectFirstBootScript);
return (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
First boot script
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{isFirstbootEnabled ? 'Enabled' : 'Disabled'}
</TextListItem>
</TextList>
</TextContent>
);
};

View file

@ -3,6 +3,10 @@ import { v4 as uuidv4 } from 'uuid';
import { parseSizeUnit } from './parseSizeUnit';
import {
FIRST_BOOT_SERVICE,
FIRST_BOOT_SERVICE_DATA,
} from '../../../constants';
import { RootState } from '../../../store';
import {
AwsUploadRequestOptions,
@ -11,6 +15,7 @@ import {
CreateBlueprintRequest,
Customizations,
DistributionProfileItem,
File,
Filesystem,
GcpUploadRequestOptions,
ImageRequest,
@ -51,6 +56,7 @@ import {
selectPartitions,
selectSnapshotDate,
selectUseLatest,
selectFirstBootScript,
} from '../../../store/wizardSlice';
import {
convertMMDDYYYYToYYYYMMDD,
@ -170,6 +176,9 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
?.profile_id as DistributionProfileItem,
},
fileSystem: fileSystem,
firstBoot: {
script: getFirstBootScript(request.customizations.files),
},
architecture: request.image_requests[0].architecture,
distribution: request.distribution,
imageTypes: request.image_requests.map((image) => image.image_type),
@ -222,6 +231,13 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
};
};
const getFirstBootScript = (files?: File[]): string => {
const firstBootFile = files?.find(
(file) => file.path === '/usr/local/sbin/custom-first-boot'
);
return firstBootFile?.data ? atob(firstBootFile.data) : '';
};
const getImageRequests = (state: RootState): ImageRequest[] => {
const imageTypes = selectImageTypes(state);
const snapshotDate = convertMMDDYYYYToYYYYMMDD(selectSnapshotDate(state));
@ -324,7 +340,23 @@ const getCustomizations = (
return {
containers: undefined,
directories: undefined,
files: undefined,
files: selectFirstBootScript(state)
? [
{
path: '/etc/systemd/system/custom-first-boot.service',
data: FIRST_BOOT_SERVICE_DATA,
data_encoding: 'base64',
ensure_parents: true,
},
{
path: '/usr/local/sbin/custom-first-boot',
data: btoa(selectFirstBootScript(state)),
data_encoding: 'base64',
mode: '0774',
ensure_parents: true,
},
]
: undefined,
subscription: getSubscription(state, orgID),
packages: getPackages(state),
payload_repositories: getPayloadRepositories(state),
@ -332,7 +364,7 @@ const getCustomizations = (
openscap: getOpenscapProfile(state),
filesystem: getFileSystem(state),
users: undefined,
services: getServices(serverStore),
services: getServices(serverStore, state),
hostname: undefined,
kernel: serverStore.kernel?.append
? { append: serverStore.kernel?.append }
@ -349,16 +381,33 @@ const getCustomizations = (
};
};
const getServices = (serverStore: ServerStore): Services | undefined => {
const enabledServices = serverStore.services?.enabled;
const disabledServices = serverStore.services?.disabled;
const maskedServices = serverStore.services?.masked;
const getServices = (
serverStore: ServerStore,
state: RootState
): Services | undefined => {
const serverEnabledServices: string[] | undefined =
serverStore.services?.enabled;
const serverDisabledServicesFromServer: string[] | undefined =
serverStore.services?.disabled;
const serverMaskedServices = serverStore.services?.masked;
const firstbootFlag: boolean =
!!selectFirstBootScript(state) &&
!serverEnabledServices?.includes(FIRST_BOOT_SERVICE);
if (enabledServices || disabledServices || maskedServices) {
const enabledServices = [
...(serverEnabledServices ? serverEnabledServices : []),
...(firstbootFlag ? [FIRST_BOOT_SERVICE] : []),
];
if (
enabledServices.length ||
serverDisabledServicesFromServer ||
serverMaskedServices
) {
return {
enabled: enabledServices,
disabled: disabledServices,
masked: maskedServices,
disabled: serverDisabledServicesFromServer,
masked: serverMaskedServices,
};
}
return undefined;

View file

@ -195,3 +195,7 @@ export const EPEL_9_REPO_DEFINITION = {
};
export const DEBOUNCED_SEARCH_WAIT_TIME = 500;
export const FIRST_BOOT_SERVICE_DATA =
'W1VuaXRdCkRlc2NyaXB0aW9uPVJ1biBmaXJzdCBib290IHNjcmlwdApDb25kaXRpb25QYXRoRXhpc3RzPS91c3IvbG9jYWwvc2Jpbi9jdXN0b20tZmlyc3QtYm9vdApXYW50cz1uZXR3b3JrLW9ubGluZS50YXJnZXQKQWZ0ZXI9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW9zYnVpbGQtZmlyc3QtYm9vdC5zZXJ2aWNlCgpbU2VydmljZV0KVHlwZT1vbmVzaG90CkV4ZWNTdGFydD0vdXNyL2xvY2FsL3NiaW4vY3VzdG9tLWZpcnN0LWJvb3QKRXhlY1N0YXJ0UG9zdD1tdiAvdXNyL2xvY2FsL3NiaW4vY3VzdG9tLWZpcnN0LWJvb3QgL3Vzci9sb2NhbC9zYmluL2N1c3RvbS1maXJzdC1ib290LmRvbmUKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldAo=';
export const FIRST_BOOT_SERVICE = 'custom-first-boot';

View file

@ -81,6 +81,9 @@ export type wizardState = {
useLatest: boolean;
snapshotDate: string;
};
firstBoot: {
script: string;
};
repositories: {
customRepositories: CustomRepository[];
payloadRepositories: Repository[];
@ -155,6 +158,7 @@ const initialState: wizardState = {
blueprintDescription: '',
},
stepValidations: {},
firstBoot: { script: '' },
};
export const selectServerUrl = (state: RootState) => {
@ -297,6 +301,10 @@ export const selectInputValidation =
return isValid ? 'success' : 'error';
};
export const selectFirstBootScript = (state: RootState) => {
return state.wizard.firstBoot?.script;
};
export const wizardSlice = createSlice({
name: 'wizard',
initialState,
@ -599,6 +607,9 @@ export const wizardSlice = createSlice({
changeBlueprintDescription: (state, action: PayloadAction<string>) => {
state.details.blueprintDescription = action.payload;
},
setFirstBootScript: (state, action: PayloadAction<string>) => {
state.firstBoot.script = action.payload;
},
setStepInputValidation: (
state,
action: PayloadAction<{
@ -677,5 +688,6 @@ export const {
changeBlueprintDescription,
loadWizardState,
setStepInputValidation,
setFirstBootScript,
} = wizardSlice.actions;
export default wizardSlice.reducer;

View file

@ -166,6 +166,7 @@ describe('Step Packages', () => {
test('clicking Next loads Image name', async () => {
await setUp();
await clickNext();
await clickNext();
await screen.findByRole('heading', {

View file

@ -418,6 +418,7 @@ describe('Step Upload to AWS', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
@ -611,6 +612,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -658,6 +660,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -706,6 +709,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -737,6 +741,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
await screen.findByText('Register the system later');
@ -896,7 +901,9 @@ describe('Step Details', () => {
await clickNext();
// skip fsc
await clickNext();
// skip snapshots
// skip snapshot
await clickNext();
//skip firstBoot
await clickNext();
};
@ -979,6 +986,8 @@ describe('Step Review', () => {
// skip packages
await clickNext();
await clickNext();
// skip firstboot
await clickNext();
// skip Details
const blueprintName = await screen.findByRole('textbox', {
name: /blueprint name/i,
@ -1042,6 +1051,8 @@ describe('Step Review', () => {
// skip repositories
await clickNext();
await clickNext();
// skip First boot
await clickNext();
const blueprintName = await screen.findByRole('textbox', {
name: /blueprint name/i,
});
@ -1223,7 +1234,7 @@ describe('Keyboard accessibility', () => {
// TODO: Focus on textbox on Packages step
await clickNext();
await clickNext();
// TODO: Focus on textbox on Details step
await clickNext();
}, 20000);

View file

@ -39,6 +39,8 @@ const goToDetailsStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await clickNext();
};
const enterBlueprintDescription = async () => {

View file

@ -103,6 +103,7 @@ const goToReviewStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
};

View file

@ -0,0 +1,126 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import {
CREATE_BLUEPRINT,
FIRST_BOOT_SERVICE,
FIRST_BOOT_SERVICE_DATA,
} from '../../../../../constants';
import { File as ImageBuilderFile } from '../../../../../store/imageBuilderApi';
import { clickNext } from '../../../../testUtils';
import {
blueprintRequest,
clickRegisterLater,
enterBlueprintName,
interceptBlueprintRequest,
renderCreateMode,
} from '../../wizardTestUtils';
import '@testing-library/jest-dom';
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
auth: {
getUser: () => {
return {
identity: {
internal: {
org_id: 5,
},
},
};
},
},
isBeta: () => true,
isProd: () => true,
getEnvironment: () => 'prod',
}),
}));
const goToFirstBootStep = async (): Promise<void> => {
const guestImageCheckBox = await screen.findByRole('checkbox', {
name: /virtualization guest image checkbox/i,
});
await userEvent.click(guestImageCheckBox);
await clickNext();
await clickNext(); // Registration
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File System
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Snapshot
await clickNext(); // First Boot
};
const openCodeEditor = async (): Promise<void> => {
const startBtn = await screen.findByRole('button', {
name: /Start from scratch/i,
});
await userEvent.click(startBtn);
};
const uploadFile = async (): Promise<void> => {
const fileInput: HTMLElement | null =
// eslint-disable-next-line testing-library/no-node-access
document.querySelector('input[type="file"]');
if (fileInput) {
const file = new File([SCRIPT], 'script.sh', { type: 'text/x-sh' });
await userEvent.upload(fileInput, file);
}
};
const goToReviewStep = async (): Promise<void> => {
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review
};
const SCRIPT = `#!/bin/bash
systemctl enable cockpit.socket`;
const BASE64_SCRIPT = btoa(SCRIPT);
const firstBootData: ImageBuilderFile[] = [
{
path: '/etc/systemd/system/custom-first-boot.service',
data: FIRST_BOOT_SERVICE_DATA,
data_encoding: 'base64',
ensure_parents: true,
},
{
path: '/usr/local/sbin/custom-first-boot',
data: BASE64_SCRIPT,
data_encoding: 'base64',
mode: '0774',
ensure_parents: true,
},
];
describe('First Boot step', () => {
test('should render First Boot step', async () => {
await renderCreateMode();
await goToFirstBootStep();
expect(screen.getByText('First boot configuration')).toBeInTheDocument();
});
describe('validate first boot request ', () => {
test('should validate first boot request', async () => {
await renderCreateMode();
await goToFirstBootStep();
await openCodeEditor();
await uploadFile();
await goToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest = {
...blueprintRequest,
customizations: {
files: firstBootData,
services: { enabled: [FIRST_BOOT_SERVICE] },
},
};
expect(receivedRequest).toEqual(expectedRequest);
});
});
});

View file

@ -60,6 +60,7 @@ const clickToReview = async () => {
await clickNext(); // skip SnapshotRepositories
await clickNext(); // skip Repositories
await clickNext(); // skip Packages
await clickNext(); // skip First Boot
const nameInput = await screen.findByRole('textbox', {
name: /blueprint name/i,
});

View file

@ -90,6 +90,7 @@ const goToReviewStep = async () => {
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // FirstBoot
await clickNext(); // Details
await enterBlueprintName('oscap');
await clickNext(); // Review

View file

@ -53,6 +53,7 @@ const goToPackagesStep = async () => {
};
const goToReviewStep = async () => {
await clickNext(); // First Boot
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review

View file

@ -71,6 +71,7 @@ const goToReviewStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
};

View file

@ -51,6 +51,7 @@ const goToRepositoriesStep = async () => {
const goToReviewStep = async () => {
await clickNext(); // Additional packages
await clickNext();
await clickNext(); // First Boot
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review

View file

@ -47,6 +47,7 @@ const goToReview = async () => {
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details
await clickNext(); // FirstBoot
await enterBlueprintName();
await clickNext(); // Review
};

View file

@ -46,6 +46,7 @@ const goToReview = async () => {
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // FirstBoot
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review

View file

@ -48,6 +48,7 @@ const goToReview = async () => {
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details
await clickNext(); // FirstBoot
await enterBlueprintName();
await clickNext(); // Review
};

View file

@ -123,6 +123,7 @@ const goToReviewStep = async () => {
await clickNext(); // Snapshots
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // First boot
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review