diff --git a/e2e/vendors/vendors.spec.ts b/e2e/vendors/vendors.spec.ts index 8c63f029..e8f54bef 100644 --- a/e2e/vendors/vendors.spec.ts +++ b/e2e/vendors/vendors.spec.ts @@ -61,6 +61,16 @@ const inactiveVendors = [ }, ]; +const LONG_TEXT_VENDOR = { + CompanyName: "Gateway Plumbing and Water Systems of Greater Saint Louis", + ContactName: "Adam Christopher Whyte Senior Field Technician", + Email: "dispatch-and-after-hours-coordination@gatewayplumbing.example", + Phone: "+1 (314) 555-0198 extension 123456", + Address: "New York, NY, Estados Unidos, 123 Long Address Avenue", + City: "", + State: "", +}; + interface MockState { listUrls: string[]; createdBody?: Record; @@ -88,10 +98,12 @@ async function mockVendorApi( deactivationBlocked?: boolean; deleteConflict?: boolean; legacyGateway?: boolean; + longText?: boolean; } = {}, ) { const state: MockState = { listUrls: [] }; const vendorRecords = [...activeVendors, ...inactiveVendors].map((vendor) => ({ ...vendor })); + if (options.longText) Object.assign(vendorRecords[0], LONG_TEXT_VENDOR); if (options.legacyGateway) { vendorRecords[0].Phone = "555-1234 ext 9"; vendorRecords[0].GoogleMapsUrl = "http://maps.example.test/legacy"; @@ -437,6 +449,47 @@ test.describe("Vendor directory prototype parity", () => { await expect(page.getByRole("button", { name: "Deactivate vendor" })).toHaveCount(0); }); + test("truncates every non-abbreviated textual column", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await mockVendorApi(page, { longText: true }); + await page.goto("/vendors"); + + const row = page.locator("tbody tr").first(); + const fields = [ + { cellIndex: 0, testId: "vendor-text-company", value: LONG_TEXT_VENDOR.CompanyName }, + { cellIndex: 1, testId: "vendor-text-technician", value: LONG_TEXT_VENDOR.ContactName }, + { cellIndex: 2, testId: "vendor-text-location", value: LONG_TEXT_VENDOR.Address }, + { cellIndex: 4, testId: "vendor-text-phone", value: LONG_TEXT_VENDOR.Phone }, + { cellIndex: 5, testId: "vendor-text-email", value: LONG_TEXT_VENDOR.Email }, + ]; + + for (const expected of fields) { + const cell = row.locator("td").nth(expected.cellIndex); + const field = cell.getByTestId(expected.testId); + await expect(field).toBeVisible(); + + const metrics = await field.evaluate((element) => { + const fieldRect = element.getBoundingClientRect(); + const cellRect = element.closest("td")?.getBoundingClientRect(); + return { + clientWidth: element.clientWidth, + scrollWidth: element.scrollWidth, + withinCell: + cellRect !== undefined && + fieldRect.left >= cellRect.left - 1 && + fieldRect.right <= cellRect.right + 1, + }; + }); + expect(metrics.scrollWidth).toBeGreaterThan(metrics.clientWidth); + expect(metrics.withinCell).toBe(true); + + await field.hover(); + await expect(page.getByRole("tooltip", { name: expected.value, exact: true })).toBeVisible(); + await page.mouse.move(0, 0); + await expect(page.getByRole("tooltip", { name: expected.value, exact: true })).toHaveCount(0); + } + }); + test("adds a technician to an existing company, then views and edits the aggregate", async ({ page, }) => { diff --git a/src/app/(protected)/vendors/_components/vendors-table.tsx b/src/app/(protected)/vendors/_components/vendors-table.tsx index a680c78b..e2651e39 100644 --- a/src/app/(protected)/vendors/_components/vendors-table.tsx +++ b/src/app/(protected)/vendors/_components/vendors-table.tsx @@ -51,6 +51,78 @@ function stopPropagation(event: MouseEvent): void { event.stopPropagation(); } +interface TruncatedTextProps { + value: string; + testId: string; + focusable?: boolean; + href?: string; + color?: string; + variant?: "description"; +} + +const truncatedTextSx = { + display: "block", + minWidth: 0, + maxWidth: "100%", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}; + +function TruncatedText({ + value, + testId, + focusable = false, + href, + color = "inherit", + variant, +}: TruncatedTextProps) { + const hasValue = value !== "—"; + const content = href ? ( + + {value} + + ) : variant ? ( + + {value} + + ) : ( + + {value} + + ); + + return ( + + {content} + + ); +} + +function getVendorDisplayValues(row: VendorListItem) { + const cityStateText = [row.city.trim(), row.state.trim()].filter(Boolean).join(", "); + return { + locationText: cityStateText || row.address.trim() || "—", + phoneText: row.phone.trim() || row.companyPhone.trim(), + }; +} + interface VendorTableRowProps { row: VendorListItem; onOpenDetail: (row: VendorListItem) => void; @@ -61,9 +133,7 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) const trades = splitTrades(row.tradeSpecialties); const primaryTrade = trades[0] ?? "—"; const extraTrades = trades.length > 1 ? trades.length - 1 : 0; - const cityStateText = [row.city.trim(), row.state.trim()].filter(Boolean).join(", "); - const locationText = cityStateText || row.address.trim() || "—"; - const phoneText = row.phone.trim() || row.companyPhone.trim(); + const { locationText, phoneText } = getVendorDisplayValues(row); const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`); const handleKeyDown = (event: KeyboardEvent) => { @@ -84,17 +154,25 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) onClick={() => onOpenDetail(row)} onKeyDown={handleKeyDown} > - - - {row.companyName || "—"} - + + - - + + {getInitials(row.contactName)} - - {row.contactName || "—"} - + - {locationText} + + + {primaryTrade === "—" ? ( "—" @@ -133,33 +221,21 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps) )} - - {phoneText ? ( - - {phoneText} - - ) : ( - "—" - )} + + - - {row.email ? ( - - {row.email} - - ) : ( - "—" - )} + + {row.totalJobs ?? 0} diff --git a/src/test/app/(protected)/vendors/vendors-list.test.tsx b/src/test/app/(protected)/vendors/vendors-list.test.tsx index 8d51d3cf..b3572218 100644 --- a/src/test/app/(protected)/vendors/vendors-list.test.tsx +++ b/src/test/app/(protected)/vendors/vendors-list.test.tsx @@ -1,4 +1,4 @@ -import { screen, within } from "@testing-library/react"; +import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import VendorsListPage from "@/app/(protected)/vendors/index"; @@ -181,6 +181,69 @@ describe("VendorsListPage", () => { expect(screen.queryByText(/555-0198/)).not.toBeInTheDocument(); }); + it("truncates every textual table field and reveals each full value", async () => { + setupDefaults(); + const longCompany = "Gateway Plumbing and Water Systems of Greater Saint Louis"; + const longTechnician = "Adam Christopher Whyte Senior Field Technician"; + const longAddress = "New York, NY, Estados Unidos, 123 Long Address Avenue"; + const longPhone = "+1 (314) 555-0198 extension 123456"; + const longEmail = "dispatch-and-after-hours-coordination@gatewayplumbing.example"; + const fallbackVendor: VendorListItem = { + ...activeVendor, + id: 4, + companyName: longCompany, + contactName: longTechnician, + phone: longPhone, + email: longEmail, + address: longAddress, + city: "", + state: "", + }; + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([fallbackVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + const fields = [ + { testId: "vendor-text-company", value: longCompany }, + { testId: "vendor-text-technician", value: longTechnician }, + { testId: "vendor-text-location", value: longAddress }, + { testId: "vendor-text-phone", value: longPhone }, + { testId: "vendor-text-email", value: longEmail }, + ]; + + for (const field of fields) { + const text = screen.getByTestId(field.testId); + const cell = text.closest("td"); + expect(cell).not.toBeNull(); + expect(cell).toHaveStyle({ overflow: "hidden" }); + expect(text).toHaveStyle({ + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }); + + await userEvent.hover(text); + expect(await screen.findByRole("tooltip")).toHaveTextContent(field.value); + await userEvent.unhover(text); + await waitFor(() => expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()); + } + + expect(screen.getByTestId("vendor-text-phone")).toHaveAttribute("href", `tel:${longPhone}`); + expect(screen.getByTestId("vendor-text-email")).toHaveAttribute("href", `mailto:${longEmail}`); + + const locationText = screen.getByTestId("vendor-text-location"); + fireEvent.keyDown(document, { key: "Tab" }); + locationText.focus(); + expect(locationText).toHaveFocus(); + expect(await screen.findByRole("tooltip")).toHaveTextContent(longAddress); + expect(within(screen.getByRole("cell", { name: longAddress })).getByText(longAddress)).toBe( + locationText, + ); + }); + it("reports the filtered totalCount in the footer when page rows are fewer than total", () => { setupDefaults(); const pageVendors = Array.from({ length: 3 }, (_, index) => ({