mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 08:03:13 +00:00
fix(vendors): truncate long textual table values (#154)
* 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:
parent
4df6e76192
commit
5e38c93b1e
3 changed files with 232 additions and 40 deletions
53
e2e/vendors/vendors.spec.ts
vendored
53
e2e/vendors/vendors.spec.ts
vendored
|
|
@ -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,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue