mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Fix queued ETA visibility and add rendering tests
This commit is contained in:
@@ -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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
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(<QueuePage />);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
</span>
|
||||
</span>
|
||||
{item.print_time_seconds && (
|
||||
<>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<Timer className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
|
||||
{formatDuration(item.print_time_seconds)}
|
||||
</span>
|
||||
{!item.waiting_reason && (
|
||||
<span
|
||||
className="text-bambu-green font-medium"
|
||||
title={t('printers.estimatedCompletion')}
|
||||
>
|
||||
ETA {formatQueueItemETA(item.print_time_seconds, timeFormat, t)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<Timer className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
|
||||
{formatDuration(item.print_time_seconds)}
|
||||
</span>
|
||||
)}
|
||||
{queueItemEta && (
|
||||
<span
|
||||
data-testid="queue-item-eta"
|
||||
className="text-bambu-green font-medium"
|
||||
title={t('printers.estimatedCompletion')}
|
||||
>
|
||||
ETA {queueItemEta}
|
||||
</span>
|
||||
)}
|
||||
{item.filament_used_grams && (
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
|
||||
@@ -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<typeof formatETA>[2],
|
||||
): string | null {
|
||||
if (printTimeSeconds == null || printTimeSeconds <= 0) return null;
|
||||
|
||||
return formatETA(printTimeSeconds / 60, timeFormat, t);
|
||||
}
|
||||
Reference in New Issue
Block a user