diff --git a/frontends/main/src/app-pages/ContractLearnersPage/ContractLearnersPage.test.tsx b/frontends/main/src/app-pages/ContractLearnersPage/ContractLearnersPage.test.tsx index 8e8e561c96..c56740a3e9 100644 --- a/frontends/main/src/app-pages/ContractLearnersPage/ContractLearnersPage.test.tsx +++ b/frontends/main/src/app-pages/ContractLearnersPage/ContractLearnersPage.test.tsx @@ -212,6 +212,67 @@ describe("ContractLearnersPage", () => { expect(within(row).getByText("Certificate")).toBeInTheDocument() }) + test("a learner row shows the email alongside the name", async () => { + const { org, contract, orgSlug } = setup() + const contractId = String(contract.id) + setMockResponse.get( + mitxUrls.organization.managerOrganizationsList(), + paginate([org]), + ) + mockTotal(contractId, 1) + mockList(contractId, [ + analyticsFactories.learnerProgress({ + full_name: "Anton Petrov", + email: "anton@example.com", + courserun_title: "Module 5", + }), + ]) + + renderWithProviders( + , + ) + + const name = await screen.findByText("Anton Petrov") + const row = rowOf(name) + expect(within(row).getByText("anton@example.com")).toBeInTheDocument() + expect(within(row).getByText("AP")).toBeInTheDocument() + }) + + test.each([ + { fullName: null, label: "null" }, + { fullName: "", label: "empty" }, + { fullName: " ", label: "whitespace-only" }, + ])( + "a learner with a $label name shows only the email and an icon avatar", + async ({ fullName }) => { + const { org, contract, orgSlug } = setup() + const contractId = String(contract.id) + setMockResponse.get( + mitxUrls.organization.managerOrganizationsList(), + paginate([org]), + ) + mockTotal(contractId, 1) + mockList(contractId, [ + analyticsFactories.learnerProgress({ + full_name: fullName, + email: "x7k2m@example.com", + courserun_title: "Module 5", + }), + ]) + + renderWithProviders( + , + ) + + const email = await screen.findByText("x7k2m@example.com") + const row = rowOf(email) + expect(within(row).getByText("Module 5")).toBeInTheDocument() + expect(within(row).queryByText("?")).not.toBeInTheDocument() + expect(within(row).queryByText("Unknown learner")).not.toBeInTheDocument() + expect(row.querySelector("[aria-hidden='true'] svg")).not.toBeNull() + }, + ) + test("a learner who withheld consent shows No consent given", async () => { const { org, contract, orgSlug } = setup() const contractId = String(contract.id) diff --git a/frontends/main/src/app-pages/ContractLearnersPage/LearnerRow.tsx b/frontends/main/src/app-pages/ContractLearnersPage/LearnerRow.tsx index 8d9faaa0a1..729cb1bc0e 100644 --- a/frontends/main/src/app-pages/ContractLearnersPage/LearnerRow.tsx +++ b/frontends/main/src/app-pages/ContractLearnersPage/LearnerRow.tsx @@ -1,6 +1,7 @@ "use client" import React from "react" +import { RiUserLine } from "@remixicon/react" import { styled, Typography } from "ol-components" import { initials } from "ol-utilities" import type { LearnerProgress } from "api/analytics-hooks/organizations" @@ -68,8 +69,9 @@ import { COLUMN_FLEX } from "./columns" // -------------------------------------------------------------------------- /** - * Initials, not a photo: the analytics API returns no avatar image, and a name - * is the only identity it carries. + * Initials, not a photo: the analytics API returns no avatar image. Without a + * name, a generic person icon — email-derived initials are wrong for addresses + * like `jdoe@` or `x7k2m@`. */ const Avatar = styled.div(({ theme }) => ({ display: "flex", @@ -97,6 +99,13 @@ const LearnerName = styled(Typography)(({ theme }) => ({ textOverflow: "ellipsis", })) as typeof Typography +const LearnerEmail = styled(Typography)(({ theme }) => ({ + ...theme.typography.body3, + color: theme.custom.colors.darkGray2, + overflow: "hidden", + textOverflow: "ellipsis", +})) as typeof Typography + const CourseTitle = styled(Typography)(({ theme }) => ({ ...theme.typography.body3, color: theme.custom.colors.silverGrayDark, @@ -184,7 +193,7 @@ const LearnerRow: React.FC = ({ row }) => { const status = getDisplayStatus(row) const statusLabel = DISPLAY_STATUS_LABEL[status] const isWithheld = status === "not-shared" - const name = row.full_name ?? row.email ?? "Unknown learner" + const name = row.full_name?.trim() || null return ( @@ -195,7 +204,7 @@ const LearnerRow: React.FC = ({ row }) => { checked={selected} onChange={() => onToggleSelect(rowId)} inputProps={{ - "aria-label": `Select ${name}, ${row.courserun_title}`, + "aria-label": `Select ${name ?? row.email ?? "learner"}, ${row.courserun_title}`, }} /> @@ -203,10 +212,13 @@ const LearnerRow: React.FC = ({ row }) => { - {name} + {name && {name}} + {row.email && ( + {row.email} + )} {row.courserun_title}