From 697a0bdd0a336553949c52ddfc981bc6bc05e7d9 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Wed, 17 Sep 2025 18:00:59 +0100 Subject: [PATCH] update host details and my device page to show users card for android devices (#32975) resolves #32356 This updates the host details and my device pages to show the users card that will show the idp info on android devices this also adds some tests for the various rendering states of the users card component - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually --- .../issue-32356-update-ui-android-user-card | 1 + frontend/__mocks__/hostMock.ts | 17 +- .../DeviceUserPage/DeviceUserPage.tests.tsx | 295 ++---------------- .../details/DeviceUserPage/DeviceUserPage.tsx | 1 + .../HostDetailsPage/HostDetailsPage.tsx | 1 + .../hosts/details/cards/User/User.tests.tsx | 177 +++++++++++ .../pages/hosts/details/cards/User/User.tsx | 4 +- 7 files changed, 227 insertions(+), 269 deletions(-) create mode 100644 changes/issue-32356-update-ui-android-user-card create mode 100644 frontend/pages/hosts/details/cards/User/User.tests.tsx diff --git a/changes/issue-32356-update-ui-android-user-card b/changes/issue-32356-update-ui-android-user-card new file mode 100644 index 0000000000..f768d2170b --- /dev/null +++ b/changes/issue-32356-update-ui-android-user-card @@ -0,0 +1 @@ +- update host details and my device UI to show the users card for android hosts diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index c4d512857f..97cc5d0956 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -1,4 +1,4 @@ -import { IHost } from "interfaces/host"; +import { IHost, IHostEndUser } from "interfaces/host"; import { IHostMdmProfile } from "interfaces/mdm"; import { pick } from "lodash"; @@ -240,4 +240,19 @@ export const createMockGetHostSoftwareResponse = ( }; }; +export const DEFAULT_HOST_END_USER_MOCK: IHostEndUser = { + idp_department: "Engineering", + idp_info_updated_at: "2025-09-15T12:00:00Z", + idp_username: "jdoe", + idp_full_name: "John Doe", + idp_groups: ["GroupA", "GroupB"], + other_emails: [{ email: "other@example.com", source: "chrome" }], +}; + +export const createMockHostEndUser = ( + overrides?: Partial +): IHostEndUser => { + return { ...DEFAULT_HOST_END_USER_MOCK, ...overrides }; +}; + export default createMockHost; diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx index ce10f85107..227498307d 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx @@ -2,7 +2,7 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; import { IDeviceUserResponse, IHostDevice } from "interfaces/host"; -import createMockHost from "__mocks__/hostMock"; +import createMockHost, { createMockHostEndUser } from "__mocks__/hostMock"; import mockServer from "test/mock-server"; import { createCustomRenderer, createMockRouter } from "test/test-utils"; import createMockLicense from "__mocks__/licenseMock"; @@ -106,6 +106,34 @@ describe("Device User Page", () => { expect(screen.queryByText(/Certificates/)).not.toBeInTheDocument(); }); + + it("hides the user card if the device is not apple or android device", async () => { + const host = createMockHost() as IHostDevice; + host.platform = "windows"; + host.end_users = []; + + mockServer.use(customDeviceHandler({ host })); + mockServer.use(defaultDeviceCertificatesHandler); + mockServer.use(emptySetupExperienceHandler); + + const render = createCustomRenderer({ + withBackendMock: true, + }); + + render( + + ); + + // waiting for the device data to render + await screen.findByText(/Details/); + + expect(screen.queryByText(/User/)).not.toBeInTheDocument(); + }); + describe("Setup experience software installation", () => { const REGULAR_DUP_MATCHER = /Last fetched/; const SETTING_UP_YOUR_DEVICE_MATCHER = /Setting up your device/; @@ -268,269 +296,4 @@ describe("Device User Page", () => { expect(btn).toBeNull(); }); }); - // // FIXME: revisit these tests when we have a better way to test modals - // describe("AutoEnrollMDMModal", () => { - // it("shows the pre-Sonoma body when the host is pre-Sonoma", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 13.1.1"; - // host.dep_assigned_to_fleet = true; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: true }, - // features: { enable_software_inventory: false }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // autoenroll-specific copy - // expect( - // screen.getByText("sudo profiles renew -type enrollment") - // ).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("notification center")).toBeInTheDocument(); - // }); - - // it("shows the Sonoma-and-above body when the host is Sonoma", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 14.7"; - // host.dep_assigned_to_fleet = true; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: true }, - // features: { enable_software_inventory: false }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // autoenroll-specific copy - // expect( - // screen.getByText("sudo profiles renew -type enrollment") - // ).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("System Settings")).toBeInTheDocument(); - // }); - - // it("shows the Sonoma-and-above body when the host is post-Sonoma", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 15.3"; - // host.dep_assigned_to_fleet = true; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: true }, - // features: { enable_software_inventory: false }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // autoenroll-specific copy - // expect( - // screen.getByText("sudo profiles renew -type enrollment") - // ).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("System Settings")).toBeInTheDocument(); - // }); - // }); - // // FIXME: revisit these tests when we have a better way to test modals - // describe("ManualEnrollMDMModal", () => { - // it("shows the pre-Seqouia body when the host is pre-Seqouia", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 14.1.1"; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: false }, - // features: { enable_software_inventory: true }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // manualenroll-specific copy - // expect(screen.getByText("Download your profile.")).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("In the search bar")).toBeInTheDocument(); - // }); - - // it("shows the Sequoia-and-above body when the host is Sequoia", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 15.3"; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: false }, - // features: { enable_software_inventory: true }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // manualenroll-specific copy - // expect(screen.getByText("Download your profile.")).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("In the sidebar menu")).toBeInTheDocument(); - // }); - - // it("shows the Sequoia-and-above body when the host is post-Sequoia", async () => { - // const host = createMockHost() as IHostDevice; - // host.platform = "darwin"; - // host.os_version = "macOS 16.0"; - - // mockServer.use( - // customDeviceHandler({ - // host, - // global_config: { - // mdm: { enabled_and_configured: false }, - // features: { enable_software_inventory: true }, - // }, - // }) - // ); - - // const render = createCustomRenderer({ - // withBackendMock: true, - // }); - - // const { user } = render( - // - // ); - - // // waiting for the device data to render - // await screen.findByText("About"); - - // // open the modal - // await user.click(screen.getByRole("button", { name: "Turn on MDM" })); - - // // waiting for the modal to render - // await screen.findByText("To turn on MDM,"); - - // // manual-specific copy - // expect(screen.getByText("Download your profile.")).toBeInTheDocument(); - // // version-specific copy - // expect(screen.getByText("In the sidebar menu")).toBeInTheDocument(); - // }); - // }); }); diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index c9decf9f65..c6bd13ba79 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -469,6 +469,7 @@ const DeviceUserPage = ({ const showUsersCard = host?.platform === "darwin" || + host?.platform === "android" || generateChromeProfilesValues(host?.end_users ?? []).length > 0 || generateOtherEmailsValues(host?.end_users ?? []).length > 0; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 298c2915b3..629f52ad64 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -964,6 +964,7 @@ const HostDetailsPage = ({ const showUsersCard = isAppleDevice(host.platform) || + isAndroidHost || generateChromeProfilesValues(host.end_users ?? []).length > 0 || generateOtherEmailsValues(host.end_users ?? []).length > 0; const showActivityCard = !isAndroidHost; diff --git a/frontend/pages/hosts/details/cards/User/User.tests.tsx b/frontend/pages/hosts/details/cards/User/User.tests.tsx new file mode 100644 index 0000000000..b82e986544 --- /dev/null +++ b/frontend/pages/hosts/details/cards/User/User.tests.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import { screen, render } from "@testing-library/react"; +import { noop } from "lodash"; + +import { createMockHostEndUser } from "__mocks__/hostMock"; + +import User from "."; + +describe("User card", () => { + it("renders the username field when the platform is Apple", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Username (IdP)")).toBeInTheDocument(); + expect(screen.getByText("jdoe")).toBeInTheDocument(); + }); + + it("renders the username field when the platform is android", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Username (IdP)")).toBeInTheDocument(); + expect(screen.getByText("jdoe")).toBeInTheDocument(); + }); + + it("renders the full name field when the platform is Apple and has full name values", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Full name (IdP)")).toBeInTheDocument(); + expect(screen.getByText("John Doe")).toBeInTheDocument(); + }); + + it("renders the full name field when the platform is Android and has full name values", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Full name (IdP)")).toBeInTheDocument(); + expect(screen.getByText("John Doe")).toBeInTheDocument(); + }); + + it("renders the groups field when the platform is Apple and has groups values", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Groups (IdP)")).toBeInTheDocument(); + expect(screen.getByText("GroupA")).toBeInTheDocument(); + expect(screen.getByText("+ 1 more")).toBeInTheDocument(); + }); + + it("renders the groups field when the platform is Android and has groups values", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Groups (IdP)")).toBeInTheDocument(); + expect(screen.getByText("GroupA")).toBeInTheDocument(); + expect(screen.getByText("+ 1 more")).toBeInTheDocument(); + }); + + it("renders the chrome profiles field when has chrome profile values", () => { + const endUsers = [ + createMockHostEndUser({ + other_emails: [ + { email: "Profile1", source: "google_chrome_profiles" }, + { email: "Profile2", source: "google_chrome_profiles" }, + ], + }), + ]; + render( + + ); + + expect(screen.getByText("Google Chrome profiles")).toBeInTheDocument(); + expect(screen.getByText("Profile1")).toBeInTheDocument(); + expect(screen.getByText("+ 1 more")).toBeInTheDocument(); + }); + + it("renders other emails field when has other email values", () => { + const endUsers = [ + createMockHostEndUser({ + other_emails: [ + { email: "other1@example.com", source: "custom" }, + { email: "other2@example.com", source: "custom" }, + ], + }), + ]; + render( + + ); + + expect(screen.getByText("Other emails")).toBeInTheDocument(); + expect(screen.getByText("other1@example.com")).toBeInTheDocument(); + expect(screen.getByText("+ 1 more")).toBeInTheDocument(); + }); + + it("renders the department field when the platform is Apple and it has department value", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Department (IdP)")).toBeInTheDocument(); + expect(screen.getByText("Engineering")).toBeInTheDocument(); + }); + + it("renders the department field when the platform is Android and it has department value", () => { + const endUsers = [createMockHostEndUser()]; + render( + + ); + + expect(screen.getByText("Department (IdP)")).toBeInTheDocument(); + expect(screen.getByText("Engineering")).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/User/User.tsx b/frontend/pages/hosts/details/cards/User/User.tsx index bbd4478a60..0c40f0935d 100644 --- a/frontend/pages/hosts/details/cards/User/User.tsx +++ b/frontend/pages/hosts/details/cards/User/User.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { noop } from "lodash"; import { IHostEndUser } from "interfaces/host"; -import { HostPlatform, isAppleDevice } from "interfaces/platform"; +import { HostPlatform, isAndroid, isAppleDevice } from "interfaces/platform"; import Card from "components/Card"; import CardHeader from "components/CardHeader"; @@ -50,7 +50,7 @@ const User = ({ const otherEmailsDisplayValues = generateOtherEmailsValues(endUsers); const endUser = endUsers[0]; - const showUsername = isAppleDevice(platform); + const showUsername = isAppleDevice(platform) || isAndroid(platform); const showFullName = showUsername && userNameDisplayValues.length > 0; const showGroups = showUsername && userNameDisplayValues.length > 0; const showChromeProfiles = chromeProfilesDisplayValues.length > 0;