Fix bug for not being to reenable end user migration in UI (#30106)

Fixes #30063

This fixes an issue added in the
[PR](https://github.com/fleetdm/fleet/pull/29968) where the user was not
able to reenable the end user migration form.

I've also added improved a11y attributes to the slider component,
ensured we are functionally disabling the form controls during gitops
mode and not just visually, and updated/added tests for the
EndUserMigrationSection component.


- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Gabriel Hernandez 2025-06-20 17:04:30 +01:00 committed by GitHub
parent 092b068657
commit a5c69a60a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 162 additions and 16 deletions

1
.gitignore vendored
View file

@ -85,6 +85,7 @@ macoffice_rel_notes/
# IDE
.vscode
.cursor
# residual files when running the build-windows tool
orbit/cmd/desktop/manifest.xml

View file

@ -0,0 +1 @@
- fixes an issue where users were not able to reenable end user migration in the UI.

View file

@ -209,7 +209,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
},
};
const createMockConfig = (overrides?: Partial<IConfig>): IConfig => {
export const createMockConfig = (overrides?: Partial<IConfig>): IConfig => {
return { ...DEFAULT_CONFIG_MOCK, ...overrides };
};

View file

@ -13,18 +13,18 @@ describe("Slider Component", () => {
it("renders correctly with default props", () => {
render(<Slider {...defaultProps} />);
expect(screen.getByText("Off")).toBeInTheDocument();
expect(screen.getByRole("button")).toHaveClass("fleet-slider");
expect(screen.getByRole("switch")).toHaveClass("fleet-slider");
});
it("renders active state correctly", () => {
render(<Slider {...defaultProps} value />);
expect(screen.getByText("On")).toBeInTheDocument();
expect(screen.getByRole("button")).toHaveClass("fleet-slider--active");
expect(screen.getByRole("switch")).toHaveClass("fleet-slider--active");
});
it("calls onChange when clicked", () => {
render(<Slider {...defaultProps} />);
fireEvent.click(screen.getByRole("button"));
fireEvent.click(screen.getByRole("switch"));
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
});

View file

@ -67,8 +67,11 @@ const Slider = (props: ISliderProps): JSX.Element => {
<FormField {...formFieldProps} type="slider">
<div className={wrapperClassNames}>
<button
role="switch"
aria-checked={value}
className={`button button--unstyled ${sliderBtnClass}`}
onClick={handleClick}
disabled={disabled}
ref={sliderRef}
>
<div className={sliderDotClass} />

View file

@ -0,0 +1,138 @@
import React from "react";
import { screen } from "@testing-library/react";
import { createMockConfig, createMockMdmConfig } from "__mocks__/configMock";
import { IConfig } from "interfaces/config";
import { createCustomRenderer, createMockRouter } from "test/test-utils";
import EndUserMigrationSection from "./EndUserMigrationSection";
const createTestMockData = (
configOverrides: Partial<IConfig>,
isPremiumTier = true
) => {
return {
context: {
app: {
isPremiumTier,
config: createMockConfig({
...configOverrides,
}),
setConfig: jest.fn(),
},
notification: {
renderFlash: jest.fn(),
},
},
};
};
describe("EndUserMigrationSection", () => {
const mockRouter = createMockRouter();
it("toggles form elements disabled state when slider is clicked", async () => {
const render = createCustomRenderer(
createTestMockData({
mdm: createMockMdmConfig({
macos_migration: {
enable: false,
mode: "voluntary",
webhook_url: "",
},
}),
})
);
const { user } = render(<EndUserMigrationSection router={mockRouter} />);
// Verify slider is initially disabled (off)
const slider = screen.getByRole("switch");
expect(slider).not.toBeChecked();
// Verify form elements are disabled
const voluntaryRadio = screen.getByRole("radio", { name: "Voluntary" });
const forcedRadio = screen.getByRole("radio", { name: "Forced" });
const webhookInput = screen.getByRole("textbox", { name: "Webhook URL" });
expect(voluntaryRadio).toBeDisabled();
expect(forcedRadio).toBeDisabled();
expect(webhookInput).toBeDisabled();
// Click the slider to enable it form elements.
// have to wait for the async state update
user.click(slider);
await screen.findByRole("switch", { checked: true });
expect(slider).toBeChecked();
expect(voluntaryRadio).not.toBeDisabled();
expect(forcedRadio).not.toBeDisabled();
expect(webhookInput).not.toBeDisabled();
});
it("disables form elements when gitops mode is enabled", async () => {
const render = createCustomRenderer(
createTestMockData({
mdm: createMockMdmConfig({
macos_migration: {
enable: true,
mode: "voluntary",
webhook_url: "",
},
}),
gitops: {
gitops_mode_enabled: true,
repository_url: "https://example.com/repo.git",
},
})
);
const { user } = render(<EndUserMigrationSection router={mockRouter} />);
// Verify slider is enabled but disabled due to gitops mode
const slider = screen.getByRole("switch");
expect(slider).toBeChecked();
expect(slider).toBeDisabled();
// Verify form elements are disabled
const voluntaryRadio = screen.getByRole("radio", { name: "Voluntary" });
const forcedRadio = screen.getByRole("radio", { name: "Forced" });
const webhookInput = screen.getByRole("textbox", { name: "Webhook URL" });
expect(voluntaryRadio).toBeDisabled();
expect(forcedRadio).toBeDisabled();
expect(webhookInput).toBeDisabled();
// clicking the slider should have no effect
user.click(slider);
expect(slider).toBeDisabled();
expect(voluntaryRadio).toBeDisabled();
expect(forcedRadio).toBeDisabled();
expect(webhookInput).toBeDisabled();
});
it("renders the connect button when MDM is not connected", () => {
const render = createCustomRenderer(
createTestMockData({
mdm: createMockMdmConfig({
apple_bm_enabled_and_configured: false,
}),
})
);
render(<EndUserMigrationSection router={mockRouter} />);
expect(
screen.getByText("Connect to Apple Business Manager to get started.")
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect" })).toBeInTheDocument();
});
it("renders the premium feature message when not on premium tier", () => {
const render = createCustomRenderer(createTestMockData({}, false));
render(<EndUserMigrationSection router={mockRouter} />);
expect(
screen.getByText("This feature is included in Fleet Premium.")
).toBeInTheDocument();
});
});

View file

@ -57,6 +57,7 @@ const validateWebhookUrl = (val: string) => {
const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
const { config, isPremiumTier, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const [formData, setFormData] = useState<IEndUserMigrationFormData>({
isEnabled: config?.mdm.macos_migration.enable || false,
mode: config?.mdm.macos_migration.mode || "voluntary",
@ -130,8 +131,10 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
}
};
const isGitOpsModeEnabled = config?.gitops.gitops_mode_enabled;
const formClasses = classnames(`${baseClass}__end-user-migration-form`, {
disabled: !formData.isEnabled || config?.gitops.gitops_mode_enabled,
disabled: !formData.isEnabled || isGitOpsModeEnabled,
});
if (!isPremiumTier) {
@ -166,18 +169,18 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
alt="end user migration preview"
className={`${baseClass}__migration-preview`}
/>
<Slider
value={formData.isEnabled}
onChange={toggleMigrationEnabled}
activeText="Enabled"
inactiveText="Disabled"
disabled={isGitOpsModeEnabled}
/>
<div className={`form ${formClasses}`}>
<Slider
value={formData.isEnabled}
onChange={toggleMigrationEnabled}
activeText="Enabled"
inactiveText="Disabled"
className={`${baseClass}__enabled-slider`}
/>
<div className={`form-field ${baseClass}__mode-field`}>
<div className="form-field__label">Mode</div>
<Radio
disabled={!formData.isEnabled}
disabled={!formData.isEnabled || isGitOpsModeEnabled}
checked={formData.mode === "voluntary"}
value="voluntary"
id="voluntary"
@ -187,7 +190,7 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
name="mode-type"
/>
<Radio
disabled={!formData.isEnabled}
disabled={!formData.isEnabled || isGitOpsModeEnabled}
checked={formData.mode === "forced"}
value="forced"
id="forced"
@ -208,7 +211,7 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
page.
</p>
<InputField
readOnly={!formData.isEnabled}
readOnly={!formData.isEnabled || isGitOpsModeEnabled}
name="webhook_url"
label="Webhook URL"
value={formData.webhookUrl}

View file

@ -267,7 +267,7 @@ describe("EditQueryForm - component", () => {
expect(automationsSlider).toBeInTheDocument();
// Check if the automations are enabled
const automationsButton = within(automationsSlider).getByRole("button");
const automationsButton = within(automationsSlider).getByRole("switch");
expect(automationsButton).toHaveClass("fleet-slider--active");
// Check if the warning icon is present