test(printers): repair speed + AMS load/unload tests after #1661 refactor

PR #1661 swapped the visible speed badge ("100%") for an icon-only Gauge
  button and replaced the kebab "Slot options" button with hover-card
  actions inside FilamentHoverCard. The two existing test files weren't
  updated alongside the refactor and stayed broken on dev.

  - Add data-testid="speed-control" to the Gauge button and rewrite the
    Speed tests around it; assertions on the percentage text are gone
    because that text no longer renders. Parametrize the four-mode API
    call test.
  - Add data-testid="filament-slot" to FilamentHoverCard's trigger
    wrapper and rewrite the AMS load/unload tests around
    fireEvent.mouseEnter → portaled actions. Replace the "hides menu
    while RUNNING" assertion with "Load/Unload buttons exist but are
    disabled" — matches the new UX shape.
This commit is contained in:
maziggy
2026-06-18 11:54:48 +02:00
parent 0f99b54d7e
commit a1cd86880c
6 changed files with 80 additions and 119 deletions
@@ -1,13 +1,15 @@
/**
* Tests for the AMS slot load / unload buttons on PrintersPage (#891).
*
* Verifies that the menu in each AMS slot popover exposes Load and Unload,
* that clicking them POSTs to the right endpoint with the right tray_id, and
* that the buttons are hidden while the printer is RUNNING.
* The printer-card refactor in #1661 replaced the kebab "Slot options"
* button with a hover card (FilamentHoverCard) whose actions render on
* hover. Tests now `fireEvent.mouseEnter` on the slot trigger
* (`data-testid="filament-slot"`) and wait for the portaled card to
* appear in document.body before clicking Load / Unload.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../utils';
import { PrintersPage } from '../../pages/PrintersPage';
@@ -88,6 +90,14 @@ const mockRunningStatus = {
state: 'RUNNING',
};
/** Hover-card visibility flips after an 80ms timeout — wait it out. */
async function hoverSlot(slot: Element) {
fireEvent.mouseEnter(slot);
await waitFor(() => {
expect(screen.getByText('Load')).toBeInTheDocument();
});
}
describe('PrintersPage - AMS load/unload (#891)', () => {
beforeEach(() => {
server.use(
@@ -112,18 +122,12 @@ describe('PrintersPage - AMS load/unload (#891)', () => {
render(<PrintersPage />);
await waitFor(() => {
// The slot menu button is hidden until we hover. Pull it directly out of the DOM.
expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
expect(screen.getAllByTestId('filament-slot').length).toBeGreaterThan(0);
});
// Slot 2 (third one, slotIdx=2) → expected tray_id = 0*4 + 2 = 2
const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
await user.click(menuButtons[2]);
await waitFor(() => {
expect(screen.getByText('Load')).toBeInTheDocument();
});
const slots = screen.getAllByTestId('filament-slot');
await hoverSlot(slots[2]);
await user.click(screen.getByText('Load'));
await waitFor(() => {
@@ -147,16 +151,11 @@ describe('PrintersPage - AMS load/unload (#891)', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
});
const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
await user.click(menuButtons[0]);
await waitFor(() => {
expect(screen.getByText('Unload')).toBeInTheDocument();
expect(screen.getAllByTestId('filament-slot').length).toBeGreaterThan(0);
});
const slots = screen.getAllByTestId('filament-slot');
await hoverSlot(slots[0]);
await user.click(screen.getByText('Unload'));
await waitFor(() => {
@@ -164,20 +163,29 @@ describe('PrintersPage - AMS load/unload (#891)', () => {
});
});
it('hides the slot menu while the printer is RUNNING', async () => {
it('disables Load / Unload while the printer is RUNNING', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockRunningStatus)),
);
render(<PrintersPage />);
// Wait for the page to render the printer card.
await waitFor(() => {
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
expect(screen.getAllByTestId('filament-slot').length).toBeGreaterThan(0);
});
// No "Slot options" menu trigger should be present at all while running.
expect(document.querySelectorAll('[title="Slot options"]').length).toBe(0);
// Hover to reveal the actions — they should be present but disabled
// while the printer is running (replaces the pre-#1661 behavior where
// the trigger button was hidden entirely).
const slots = screen.getAllByTestId('filament-slot');
fireEvent.mouseEnter(slots[0]);
await waitFor(() => {
expect(screen.getByText('Load')).toBeInTheDocument();
});
expect(screen.getByText('Load').closest('button')).toBeDisabled();
expect(screen.getByText('Unload').closest('button')).toBeDisabled();
});
it('external spool slot exposes Load and posts tray_id=254', async () => {
@@ -201,16 +209,11 @@ describe('PrintersPage - AMS load/unload (#891)', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
});
const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
await user.click(menuButtons[0]);
await waitFor(() => {
expect(screen.getByText('Load')).toBeInTheDocument();
expect(screen.getAllByTestId('filament-slot').length).toBeGreaterThan(0);
});
const slots = screen.getAllByTestId('filament-slot');
await hoverSlot(slots[0]);
await user.click(screen.getByText('Load'));
await waitFor(() => {
@@ -1,8 +1,11 @@
/**
* Tests for the print speed control feature on the PrintersPage.
*
* Verifies that the speed badge renders, the dropdown menu opens on click,
* speed options are displayed, and selecting an option calls the API.
* The printer-card refactor in #1661 replaced the visible "100%" / "50%"
* speed badge with an icon-only Gauge button. These tests now target the
* button via data-testid="speed-control" instead of the percentage text.
* The dropdown still shows the same translated labels (Silent (50%),
* Standard (100%), Sport (124%), Ludicrous (166%)).
*/
import { describe, it, expect, beforeEach } from 'vitest';
@@ -79,8 +82,8 @@ describe('PrintersPage - Print Speed Control', () => {
);
});
describe('speed badge rendering', () => {
it('shows speed badge with current speed percentage when printing', async () => {
describe('speed control button', () => {
it('renders and is enabled when printer is printing', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json(mockPrintingStatus);
@@ -90,54 +93,13 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
// speed_level 2 = Standard = 100%
expect(screen.getByText('100%')).toBeInTheDocument();
const button = screen.getByTestId('speed-control');
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
});
});
it('shows speed badge with 50% for silent mode', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json({ ...mockPrintingStatus, speed_level: 1 });
})
);
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('50%')).toBeInTheDocument();
});
});
it('shows speed badge with 124% for sport mode', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json({ ...mockPrintingStatus, speed_level: 3 });
})
);
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('124%')).toBeInTheDocument();
});
});
it('shows speed badge with 166% for ludicrous mode', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json({ ...mockPrintingStatus, speed_level: 4 });
})
);
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('166%')).toBeInTheDocument();
});
});
it('disables speed badge button when printer is idle', async () => {
it('is disabled when printer is idle', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json(mockIdleStatus);
@@ -147,12 +109,9 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
const button = screen.getByTestId('speed-control');
expect(button).toBeDisabled();
});
// The button containing the speed percentage should be disabled
const speedBadge = screen.getByText('100%').closest('button');
expect(speedBadge).toBeDisabled();
});
});
@@ -169,11 +128,10 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByTestId('speed-control')).toBeEnabled();
});
const speedBadge = screen.getByText('100%').closest('button')!;
await user.click(speedBadge);
await user.click(screen.getByTestId('speed-control'));
await waitFor(() => {
expect(screen.getByText('Silent (50%)')).toBeInTheDocument();
@@ -195,11 +153,10 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByTestId('speed-control')).toBeEnabled();
});
const speedBadge = screen.getByText('100%').closest('button')!;
await user.click(speedBadge);
await user.click(screen.getByTestId('speed-control'));
await waitFor(() => {
const options = [
@@ -213,7 +170,7 @@ describe('PrintersPage - Print Speed Control', () => {
});
});
it('calls the API when a speed option is selected', async () => {
it('calls the API with the correct mode when a speed option is selected', async () => {
const user = userEvent.setup();
let capturedMode: number | null = null;
@@ -231,18 +188,15 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByTestId('speed-control')).toBeEnabled();
});
// Open the speed menu
const speedBadge = screen.getByText('100%').closest('button')!;
await user.click(speedBadge);
await user.click(screen.getByTestId('speed-control'));
await waitFor(() => {
expect(screen.getByText('Sport (124%)')).toBeInTheDocument();
});
// Select "Sport" speed
await user.click(screen.getByText('Sport (124%)'));
await waitFor(() => {
@@ -265,33 +219,38 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByTestId('speed-control')).toBeEnabled();
});
const speedBadge = screen.getByText('100%').closest('button')!;
await user.click(speedBadge);
await user.click(screen.getByTestId('speed-control'));
await waitFor(() => {
expect(screen.getByText('Silent (50%)')).toBeInTheDocument();
});
// Select an option
await user.click(screen.getByText('Silent (50%)'));
// Menu should close - speed labels should no longer be visible
await waitFor(() => {
expect(screen.queryByText('Silent (50%)')).not.toBeInTheDocument();
});
});
it('optimistically updates the speed display when selecting a new speed', async () => {
it.each([
{ mode: 1, label: 'Silent (50%)' },
{ mode: 2, label: 'Standard (100%)' },
{ mode: 3, label: 'Sport (124%)' },
{ mode: 4, label: 'Ludicrous (166%)' },
])('selecting $label sends mode=$mode', async ({ mode, label }) => {
const user = userEvent.setup();
let capturedMode: number | null = null;
server.use(
http.get('/api/v1/printers/:id/status', () => {
return HttpResponse.json(mockPrintingStatus); // speed_level: 2 (100%)
return HttpResponse.json({ ...mockPrintingStatus, speed_level: 2 });
}),
http.post('/api/v1/printers/:id/print-speed', () => {
http.post('/api/v1/printers/:id/print-speed', async ({ request }) => {
const url = new URL(request.url);
capturedMode = Number(url.searchParams.get('mode'));
return HttpResponse.json({ success: true, message: 'Speed set' });
})
);
@@ -299,22 +258,19 @@ describe('PrintersPage - Print Speed Control', () => {
render(<PrintersPage />);
await waitFor(() => {
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByTestId('speed-control')).toBeEnabled();
});
// Open the speed menu and select Ludicrous
const speedBadge = screen.getByText('100%').closest('button')!;
await user.click(speedBadge);
await user.click(screen.getByTestId('speed-control'));
await waitFor(() => {
expect(screen.getByText('Ludicrous (166%)')).toBeInTheDocument();
expect(screen.getByText(label)).toBeInTheDocument();
});
await user.click(screen.getByText('Ludicrous (166%)'));
await user.click(screen.getByText(label));
// The badge should optimistically update to show 166%
await waitFor(() => {
expect(screen.getByText('166%')).toBeInTheDocument();
expect(capturedMode).toBe(mode);
});
});
});
@@ -177,6 +177,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
return (
<div
ref={triggerRef}
data-testid="filament-slot"
className={`relative ${className}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
+1
View File
@@ -4054,6 +4054,7 @@ function PrinterCard({
{(() => (
<div className="relative">
<button
data-testid="speed-control"
onClick={() => setShowSpeedMenu(showSpeedMenu === printer.id ? null : printer.id)}
disabled={!isPrinting || !hasPermission('printers:control')}
className={`${iconControlClass} ${
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BEllJfyV.js"></script>
<script type="module" crossorigin src="/assets/index-DzdK7n82.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-iyQgdQ5p.css">
</head>
<body>