Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
391e127313 |
@@ -57,7 +57,7 @@ const MESSAGES: Record<PinFlowStatus, string> = {
|
||||
success: 'Connected to Plex.',
|
||||
'authorized-no-servers': 'Signed in to Plex, but no eligible servers were discovered.',
|
||||
timeout: 'Plex sign-in timed out — try again.',
|
||||
'budget-exhausted': 'Still working — this can take a while on a large first sync. Use Refresh to check again.'
|
||||
'budget-exhausted': 'Still working — this can take a while on a large first sync. Check again to see if it finished.'
|
||||
};
|
||||
|
||||
const TERMINAL: ReadonlySet<PinFlowStatus> = new Set<PinFlowStatus>([
|
||||
|
||||
@@ -266,4 +266,46 @@ describe('PlexSourceScreen', () => {
|
||||
expect(screen.queryByText(/Your Plex account is authorized/i)).toBeNull();
|
||||
expect(screen.queryByText(/no eligible servers were discovered/i)).toBeNull();
|
||||
});
|
||||
|
||||
// #367: budget-exhausted told the user to "Use Refresh to check again", but the Servers card (the
|
||||
// only place Refresh lives) doesn't render with zero servers, so the instruction pointed at nothing.
|
||||
// The message now describes checking again generically, and a global "Check again" button re-checks
|
||||
// GET /api/v1/media-sources/plex without resuming the (already spent) timed poll loop.
|
||||
it('budget-exhausted with no servers offers a working "Check again" control instead of a dead Refresh reference', async () => {
|
||||
const stateRef = { current: { isAuthorized: false, isLocked: false, servers: [] } as PlexState };
|
||||
const counter = { state: 0 };
|
||||
installFetch(stateRef, counter);
|
||||
vi.spyOn(window, 'open').mockReturnValue({} as Window);
|
||||
vi.useFakeTimers();
|
||||
|
||||
render(<PlexSourceScreen />);
|
||||
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Sign in to Plex' })).toBeTruthy());
|
||||
|
||||
stateRef.current = { isAuthorized: true, isLocked: true, servers: [] };
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sign in to Plex' }));
|
||||
await vi.waitFor(() => expect(screen.getByText(/discovering your Plex servers|Waiting/i)).toBeTruthy());
|
||||
|
||||
await vi.advanceTimersByTimeAsync(152_000); // consume the whole 150s budget
|
||||
await vi.waitFor(() => expect(screen.getByText(/Still working/i)).toBeTruthy());
|
||||
|
||||
// The message no longer references a "Refresh" control that isn't rendered (no Servers card yet).
|
||||
expect(screen.queryByText(/Use Refresh/i)).toBeNull();
|
||||
expect(screen.queryByRole('heading', { name: 'Servers' })).toBeNull();
|
||||
|
||||
// A real, working "Check again" control is offered instead.
|
||||
const checkAgainButton = screen.getByRole('button', { name: 'Check again' });
|
||||
expect(checkAgainButton).toBeTruthy();
|
||||
|
||||
const callsBeforeCheck = counter.state;
|
||||
|
||||
// The next sync completes and a server is discovered by the time the user checks again.
|
||||
stateRef.current = { isAuthorized: true, isLocked: false, servers: [{ id: 3, name: 'Attic Server', address: 'http://plex:32400' }] };
|
||||
fireEvent.click(checkAgainButton);
|
||||
|
||||
// Check again performs its own fetch (does not rely on the exhausted timed poll loop resuming).
|
||||
await vi.waitFor(() => expect(counter.state).toBeGreaterThan(callsBeforeCheck));
|
||||
await vi.waitFor(() => expect(screen.getByRole('heading', { name: 'Servers' })).toBeTruthy());
|
||||
expect(screen.getByText('Attic Server')).toBeTruthy();
|
||||
expect(screen.getByText('Connected to Plex.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,13 +28,16 @@ type BootState =
|
||||
// — the poll runs against GET /api/v1/media-sources/plex every 2s and, crucially, keeps polling while
|
||||
// authorized-but-still-locked ("finalizing / discovering servers"); the terminal success signal is
|
||||
// the lock RELEASING (see pinFlowPoll.ts). Server rows offer Refresh / Edit Libraries / Edit Path
|
||||
// Replacements. Refresh is disabled while the Plex lock is held.
|
||||
// Replacements; Refresh is disabled while the Plex lock is held. The budget-exhausted terminal (no
|
||||
// servers may exist yet) instead offers a global "Check again" affordance (#367) so its message
|
||||
// always points at a control that's actually rendered.
|
||||
export function PlexSourceScreen() {
|
||||
const [boot, setBoot] = useState<BootState>({ status: 'loading' });
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [confirmSignOut, setConfirmSignOut] = useState(false);
|
||||
const [signingOut, setSigningOut] = useState(false);
|
||||
const [refreshingId, setRefreshingId] = useState<number | null>(null);
|
||||
const [checkingAgain, setCheckingAgain] = useState(false);
|
||||
|
||||
// Pin-flow UI state.
|
||||
const [pinState, setPinState] = useState<PinFlowState | null>(null);
|
||||
@@ -162,6 +165,41 @@ export function PlexSourceScreen() {
|
||||
});
|
||||
};
|
||||
|
||||
// The global "Check again" affordance for the budget-exhausted terminal (#367): that state has no
|
||||
// per-server Refresh (the Servers card only renders once a server is discovered), so the message
|
||||
// must point at something that actually exists. This does a single one-off re-check of
|
||||
// GET /api/v1/media-sources/plex rather than resuming the timed poll loop — the 150s budget is
|
||||
// spent; re-arming it silently would contradict "exhausted" and could spin forever on a sync that
|
||||
// never finishes.
|
||||
const checkAgain = () => {
|
||||
if (checkingAgain) {
|
||||
return;
|
||||
}
|
||||
setCheckingAgain(true);
|
||||
setActionError(null);
|
||||
getPlexState()
|
||||
.then((state) => {
|
||||
if (!activeRef.current) {
|
||||
return;
|
||||
}
|
||||
setBoot({ status: 'ready', state });
|
||||
const elapsed = Date.now() - startedAtRef.current;
|
||||
const next = evaluatePinFlow(
|
||||
{ isLocked: state.isLocked, isAuthorized: state.isAuthorized, hasServers: (state.servers ?? []).length > 0 },
|
||||
{ budgetExhausted: isPinFlowBudgetExhausted(elapsed) }
|
||||
);
|
||||
setPinState(next);
|
||||
setCheckingAgain(false);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!activeRef.current) {
|
||||
return;
|
||||
}
|
||||
setCheckingAgain(false);
|
||||
setActionError(messageFromMediaSourcesError(error, 'Unable to check Plex status'));
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = (serverId: number) => {
|
||||
setRefreshingId(serverId);
|
||||
setActionError(null);
|
||||
@@ -284,6 +322,18 @@ export function PlexSourceScreen() {
|
||||
{(popupBlocked || (polling && authUrl)) && authUrl && (
|
||||
<a href={authUrl} target="_blank" rel="noreferrer noopener">Open the Plex sign-in page</a>
|
||||
)}
|
||||
{pinState?.status === 'budget-exhausted' && (
|
||||
<Button
|
||||
disabled={checkingAgain}
|
||||
loading={checkingAgain}
|
||||
onClick={checkAgain}
|
||||
size="sm"
|
||||
startIcon={<RefreshCw aria-hidden="true" size={14} />}
|
||||
variant="secondary"
|
||||
>
|
||||
Check again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!polling && !pinState && !hasPlexAccount && (
|
||||
|
||||
Reference in New Issue
Block a user