diff --git a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx index 3d0c06f06..65f2a18dc 100644 --- a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx +++ b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx @@ -486,3 +486,90 @@ describe('VirtualPrinterCard - access code inherits from target', () => { expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); }); }); + +// The collapsed header used to be a single flex row of flex-shrink-0 items, +// so it was as wide as its contents and spilled past the card's border -- +// the reporter of #2808 saw an IP address and the enable toggle floating on +// the page background. jsdom does no layout, so these pin the CSS contract +// that makes wrapping possible rather than the pixels: a flexible, wrappable +// metadata group, and no item that both refuses to shrink and claims it will +// truncate. +describe('VirtualPrinterCard - header does not overflow the card', () => { + const printers = [ + { + id: 9, + // The name a printer adopted without one gets (discovery.py) -- 25 + // unbreakable characters, and half of why this overflowed. + name: 'Printer at 192.168.30.210', + ip_address: '192.168.30.210', + access_code: 'TGTCODE1', + serial_number: '01P00A391800001', + model: 'H2C', + is_active: true, + }, + ]; + + const twoVlanPrinter = () => + createMockPrinter({ + name: 'H2C', + mode: 'proxy', + model_name: 'H2C', + target_printer_id: 9, + // Both are populated only when Bambuddy and the printer sit on + // different subnets -- the reporter's case. + bind_ip: '192.168.20.175', + remote_interface_ip: '192.168.30.210', + }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(createMockPrinter()); + vi.mocked(api.getPrinters).mockResolvedValue(printers as unknown as Awaited>); + }); + + it('keeps both IPs visible instead of truncating them away', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('192.168.20.175')).toBeInTheDocument(); + }); + // An operator opens this page to check exactly these two values, so the + // fix has to wrap them, not hide them. + expect(screen.getByText('192.168.30.210')).toBeInTheDocument(); + }); + + it('puts the metadata in a shrinkable, wrapping group', async () => { + render(); + + const bindIp = await screen.findByText('192.168.20.175'); + const group = bindIp.parentElement as HTMLElement; + + expect(group.className).toContain('flex-wrap'); + // Without min-w-0 a flex item cannot shrink below its content, which is + // what defeated the name's `truncate` before. + expect(group.className).toContain('min-w-0'); + expect(group.className).toContain('flex-1'); + }); + + it('has no header item that is both unshrinkable and truncating', async () => { + render(); + + const bindIp = await screen.findByText('192.168.20.175'); + const group = bindIp.parentElement as HTMLElement; + + const selfCancelling = Array.from(group.children).filter( + (el) => el.className.includes('flex-shrink-0') && el.className.includes('truncate') + ); + expect(selfCancelling).toHaveLength(0); + }); + + it('clips at the card border as a backstop', async () => { + const { container } = render(); + + await screen.findByText('192.168.20.175'); + // Scoped to this card on purpose: cards elsewhere render menus that paint + // outside their own bounds, so this must not migrate into `Card`. + const card = container.querySelector('.rounded-xl') as HTMLElement; + expect(card.className).toContain('overflow-hidden'); + }); +}); diff --git a/frontend/src/components/VirtualPrinterCard.tsx b/frontend/src/components/VirtualPrinterCard.tsx index 0d0ff5f5d..ff888dec9 100644 --- a/frontend/src/components/VirtualPrinterCard.tsx +++ b/frontend/src/components/VirtualPrinterCard.tsx @@ -255,7 +255,14 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) return ( <> - + {/* + Clip at the border as a backstop, so a value longer than anything + anticipated above lands inside the card instead of on the page + background (#2808). Scoped here rather than added to `Card` itself: + half the cards in the app render dropdowns and menus that deliberately + paint outside their bounds, and this one has none. + */} + {/* Collapsed header - always visible, clickable to expand */}
- {printer.name} - {modeLabel} - {printer.model_name && ( - {printer.model_name} - )} - {targetPrinterName && ( - - {localMode === 'proxy' && } - {targetPrinterName} - - )} - {localBindIp && ( - {localBindIp} - )} - {localRemoteInterfaceIp && ( - {localRemoteInterfaceIp} - )} + {/* + Metadata wraps rather than overflowing (#2808). Every item in this + row used to be flex-shrink-0, so nothing could give and the row was + as wide as its contents -- which the Card doesn't clip, so the last + IP and the toggle were painted outside the card's border. The name's + `truncate` didn't save it either: a flex item defaults to + min-width:auto, so it couldn't shrink below its text and the + ellipsis never engaged (`flex-shrink-0 truncate` on the target name + was self-cancelling for the same reason). + + It needs three things at once to overflow, which is why it took a + two-VLAN setup to surface: bind_ip and remote_interface_ip are both + set only when Bambuddy and the printer are on different subnets, a + printer adopted without a name is called "Printer at " (25 + characters of unbreakable text), and the cards sit in a 3-column + grid. Wrapping keeps every value readable -- these are the addresses + an operator came to this page to check, so truncating them away + would trade one bug for a quieter one. + */} +
+ {printer.name} + {modeLabel} + {printer.model_name && ( + {printer.model_name} + )} + {targetPrinterName && ( + + {localMode === 'proxy' && } + {targetPrinterName} + + )} + {localBindIp && ( + {localBindIp} + )} + {localRemoteInterfaceIp && ( + {localRemoteInterfaceIp} + )} +
e.stopPropagation()}>