From 0034fdc425b54e5562c54f1a14df44b3b561f222 Mon Sep 17 00:00:00 2001
From: mpl1337 <19016821+mpl1337@users.noreply.github.com>
Date: Sat, 1 Aug 2026 13:56:18 +0200
Subject: [PATCH] Fix queued ETA visibility and add rendering tests
---
.../src/__tests__/pages/QueuePage.test.tsx | 150 +++++++++++++++++-
frontend/src/__tests__/utils/queueEta.test.ts | 27 ----
frontend/src/pages/QueuePage.tsx | 39 +++--
frontend/src/utils/queueEta.ts | 18 ---
4 files changed, 173 insertions(+), 61 deletions(-)
delete mode 100644 frontend/src/__tests__/utils/queueEta.test.ts
delete mode 100644 frontend/src/utils/queueEta.ts
diff --git a/frontend/src/__tests__/pages/QueuePage.test.tsx b/frontend/src/__tests__/pages/QueuePage.test.tsx
index f4261ce29..9b451d6dd 100644
--- a/frontend/src/__tests__/pages/QueuePage.test.tsx
+++ b/frontend/src/__tests__/pages/QueuePage.test.tsx
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { screen, waitFor } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../utils';
import { QueuePage } from '../../pages/QueuePage';
@@ -197,6 +197,154 @@ describe('QueuePage', () => {
});
});
+ it('shows one if-started-now ETA for an eligible pending item', async () => {
+ render();
+
+ const name = await screen.findByText('Test Print 1');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+ expect(
+ within(row as HTMLElement).getAllByTestId('queue-item-eta'),
+ ).toHaveLength(1);
+ });
+
+ it('shows one if-started-now ETA for a staged item', async () => {
+ server.use(
+ http.get('/api/v1/queue/', () => {
+ return HttpResponse.json([
+ {
+ ...mockQueueItems[0],
+ archive_name: 'Staged Print',
+ manual_start: true,
+ },
+ ]);
+ }),
+ );
+
+ render();
+
+ const name = await screen.findByText('Staged Print');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+ expect(
+ within(row as HTMLElement).getAllByTestId('queue-item-eta'),
+ ).toHaveLength(1);
+ });
+
+ it('shows exactly one live ETA for a printing item', async () => {
+ server.use(
+ http.get('/api/v1/printers/:id/status', ({ params }) => {
+ return HttpResponse.json({
+ id: Number(params.id),
+ name: 'Test Printer',
+ connected: true,
+ state: 'RUNNING',
+ progress: 50,
+ remaining_time: 60,
+ layer_num: 50,
+ total_layers: 100,
+ filename: 'active.3mf',
+ });
+ }),
+ );
+
+ render();
+
+ const name = await screen.findByText('Active Print');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+
+ await waitFor(() => {
+ expect(
+ within(row as HTMLElement).getAllByText(/^ETA\s/),
+ ).toHaveLength(1);
+ });
+
+ expect(
+ within(row as HTMLElement).queryByTestId('queue-item-eta'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('does not show an ETA for a waiting item', async () => {
+ server.use(
+ http.get('/api/v1/queue/', () => {
+ return HttpResponse.json([
+ {
+ ...mockQueueItems[0],
+ archive_name: 'Waiting Print',
+ waiting_reason: 'Waiting for matching printer',
+ },
+ ]);
+ }),
+ );
+
+ render();
+
+ const name = await screen.findByText('Waiting Print');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+ expect(
+ within(row as HTMLElement).queryByTestId('queue-item-eta'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('does not show an if-started-now ETA for a scheduled item', async () => {
+ server.use(
+ http.get('/api/v1/queue/', () => {
+ return HttpResponse.json([
+ {
+ ...mockQueueItems[0],
+ archive_name: 'Scheduled Print',
+ scheduled_time: new Date(
+ Date.now() + 5 * 60 * 60 * 1000,
+ ).toISOString(),
+ },
+ ]);
+ }),
+ );
+
+ render();
+
+ const name = await screen.findByText('Scheduled Print');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+ expect(
+ within(row as HTMLElement).queryByTestId('queue-item-eta'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('does not render a dangling ETA for an invalid duration', async () => {
+ server.use(
+ http.get('/api/v1/queue/', () => {
+ return HttpResponse.json([
+ {
+ ...mockQueueItems[0],
+ archive_name: 'Invalid Duration Print',
+ print_time_seconds: -60,
+ },
+ ]);
+ }),
+ );
+
+ render();
+
+ const name = await screen.findByText('Invalid Duration Print');
+ const row = name.closest('.group');
+
+ expect(row).not.toBeNull();
+ expect(
+ within(row as HTMLElement).queryByTestId('queue-item-eta'),
+ ).not.toBeInTheDocument();
+ expect(
+ within(row as HTMLElement).queryByText(/^ETA(?:\s|$)/),
+ ).not.toBeInTheDocument();
+ });
+
it('shows completed items in history', async () => {
const user = userEvent.setup();
render();
diff --git a/frontend/src/__tests__/utils/queueEta.test.ts b/frontend/src/__tests__/utils/queueEta.test.ts
deleted file mode 100644
index 807ac5950..000000000
--- a/frontend/src/__tests__/utils/queueEta.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { formatETA } from '../../utils/date';
-import { formatQueueItemETA } from '../../utils/queueEta';
-
-describe('formatQueueItemETA', () => {
- beforeEach(() => {
- vi.useFakeTimers();
- vi.setSystemTime(new Date('2026-07-31T18:00:00Z'));
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- it('calculates the ETA from the current time and job duration', () => {
- expect(formatQueueItemETA(90 * 60, '24h')).toBe(
- formatETA(90, '24h'),
- );
- });
-
- it('returns null without a usable print duration', () => {
- expect(formatQueueItemETA(null)).toBeNull();
- expect(formatQueueItemETA(undefined)).toBeNull();
- expect(formatQueueItemETA(0)).toBeNull();
- expect(formatQueueItemETA(-60)).toBeNull();
- });
-});
diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx
index 02c984060..6c1579279 100644
--- a/frontend/src/pages/QueuePage.tsx
+++ b/frontend/src/pages/QueuePage.tsx
@@ -66,7 +66,6 @@ import { api, ApiError } from '../api/client';
import { PipelineRunsView } from './PipelineRunsPage';
import { type TimeFormat, formatETA, formatDuration, formatRelativeTime, parseUTCDate } from '../utils/date';
import { getBedTypeInfo } from '../utils/bedType';
-import { formatQueueItemETA } from '../utils/queueEta';
import type { PrintQueueItem, PrintQueueBulkUpdate, Permission, CalibrationMode } from '../api/client';
import { Card } from '../components/Card';
import { Button } from '../components/Button';
@@ -429,6 +428,17 @@ function SortableQueueItem({
const isPending = item.status === 'pending';
const isHistory = ['completed', 'failed', 'skipped', 'cancelled'].includes(item.status);
+ // This is an "if started now" estimate, not a cumulative queue forecast.
+ // Do not show it for active, blocked, scheduled, or invalid queue items.
+ const queueItemEta =
+ isPending &&
+ !item.waiting_reason &&
+ !item.scheduled_time &&
+ item.print_time_seconds != null &&
+ item.print_time_seconds > 0
+ ? formatETA(item.print_time_seconds / 60, timeFormat, t)
+ : null;
+
const isMobileSelectable = isPending && onToggleSelect;
return (
@@ -599,20 +609,19 @@ function SortableQueueItem({
{item.print_time_seconds && (
- <>
-
-
- {formatDuration(item.print_time_seconds)}
-
- {!item.waiting_reason && (
-
- ETA {formatQueueItemETA(item.print_time_seconds, timeFormat, t)}
-
- )}
- >
+
+
+ {formatDuration(item.print_time_seconds)}
+
+ )}
+ {queueItemEta && (
+
+ ETA {queueItemEta}
+
)}
{item.filament_used_grams && (
diff --git a/frontend/src/utils/queueEta.ts b/frontend/src/utils/queueEta.ts
deleted file mode 100644
index 5282af121..000000000
--- a/frontend/src/utils/queueEta.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { formatETA, type TimeFormat } from './date';
-
-/**
- * Formats the estimated completion time for a queue item if it were
- * started at the current time.
- *
- * This is deliberately a per-job estimate rather than a cumulative
- * queue forecast.
- */
-export function formatQueueItemETA(
- printTimeSeconds: number | null | undefined,
- timeFormat: TimeFormat = 'system',
- t?: Parameters[2],
-): string | null {
- if (printTimeSeconds == null || printTimeSeconds <= 0) return null;
-
- return formatETA(printTimeSeconds / 60, timeFormat, t);
-}