fix(vendors): truncate long textual table values (#154)
Some checks are pending
Frontend checks / Build and test (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Deploy / deploy (push) Waiting to run

* fix(vendors): truncate long locations in table

* fix(vendors): truncate textual table cells

---------

Co-authored-by: Codex Review Integration <codex-review@local.invalid>
This commit is contained in:
Alexandre Brandizzi 2026-08-28 12:21:15 -03:00 • committed by GitHub
parent 4df6e76192
commit 5e38c93b1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 232 additions and 40 deletions

View file

@ -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<string, unknown>;
@ -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,
}) => {

View file

@ -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 ? (
<Link
href={href}
underline="hover"
color="inherit"
data-testid={testId}
sx={{ ...truncatedTextSx, color }}
onClick={stopPropagation}
>
{value}
</Link>
) : variant ? (
<Text
variant={variant}
data-testid={testId}
sx={{ ...truncatedTextSx, color, flex: "1 1 auto" }}
>
{value}
</Text>
) : (
<Box
component="span"
data-testid={testId}
tabIndex={focusable && hasValue ? 0 : undefined}
sx={{ ...truncatedTextSx, color }}
>
{value}
</Box>
);
return (
<Tooltip title={hasValue ? value : ""} placement="top" arrow describeChild>
{content}
</Tooltip>
);
}
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}
>
<TableCell>
<Text variant="description" sx={{ color: "var(--color-foreground)" }}>
{row.companyName || "—"}
</Text>
<TableCell sx={{ overflow: "hidden" }}>
<TruncatedText
value={row.companyName || "—"}
testId="vendor-text-company"
variant="description"
color="var(--color-foreground)"
/>
</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} sx={{ alignItems: "center" }}>
<TableCell sx={{ overflow: "hidden" }}>
<Stack
direction="row"
spacing={1.25}
sx={{ alignItems: "center", minWidth: 0, overflow: "hidden" }}
>
<Avatar
sx={{
width: 28,
height: 28,
flexShrink: 0,
backgroundColor: "var(--color-bg-muted)",
color: "var(--color-primary)",
fontSize: 10.5,
@ -103,12 +181,22 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
>
{getInitials(row.contactName)}
</Avatar>
<Text variant="description" sx={{ color: "var(--color-foreground)" }}>
{row.contactName || "—"}
</Text>
<TruncatedText
value={row.contactName || "—"}
testId="vendor-text-technician"
variant="description"
color="var(--color-foreground)"
/>
</Stack>
</TableCell>
<TableCell sx={{ color: "var(--color-muted-foreground)" }}>{locationText}</TableCell>
<TableCell sx={{ color: "var(--color-muted-foreground)", overflow: "hidden" }}>
<TruncatedText
value={locationText}
testId="vendor-text-location"
focusable
color="var(--color-muted-foreground)"
/>
</TableCell>
<TableCell sx={{ color: "var(--color-muted-foreground)" }}>
{primaryTrade === "—" ? (
"—"
@ -133,33 +221,21 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
</Stack>
)}
</TableCell>
<TableCell sx={{ color: "var(--color-muted-foreground)" }}>
{phoneText ? (
<Link
href={`tel:${phoneText}`}
underline="hover"
color="inherit"
onClick={stopPropagation}
>
{phoneText}
</Link>
) : (
"—"
)}
<TableCell sx={{ color: "var(--color-muted-foreground)", overflow: "hidden" }}>
<TruncatedText
value={phoneText || "—"}
testId="vendor-text-phone"
href={phoneText ? `tel:${phoneText}` : undefined}
color="var(--color-muted-foreground)"
/>
</TableCell>
<TableCell>
{row.email ? (
<Link
href={`mailto:${row.email}`}
underline="hover"
sx={{ color: "#6b7a96" }}
onClick={stopPropagation}
>
{row.email}
</Link>
) : (
"—"
)}
<TableCell sx={{ overflow: "hidden" }}>
<TruncatedText
value={row.email || "—"}
testId="vendor-text-email"
href={row.email ? `mailto:${row.email}` : undefined}
color="#6b7a96"
/>
</TableCell>
<TableCell align="center">{row.totalJobs ?? 0}</TableCell>
<TableCell>

View file

@ -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(<VendorsListPage />, { 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) => ({