fix(tours): refresh the wanderer watch after a fixed token and truly disable its switch offline

The watch row only re-read its state when the connection came or went, so after
a passing connection test or a newly saved token it kept saying "Token refused"
until a reload. The connection card now hands it a key built from the saved
connection and its last check, and the row re-reads the watch whenever that
changes.

Without a connection the switch was only dimmed: the button stayed in the tab
order and was announced as active. ToggleSwitch gains a disabled prop that
disables the button and marks it aria-disabled, and the reason stays reachable
through a focusable wrapper carrying the tooltip.
This commit is contained in:
Maurice
2026-10-20 14:46:24 +02:00
parent 2e1dca9fe9
commit fbba4fd73b
8 changed files with 85 additions and 24 deletions
@@ -75,18 +75,32 @@ describe('watchStatusOf (WAN-02)', () => {
describe('useWandererWatch (WAN-02)', () => {
it('loads with the card and again when the connection comes or goes', async () => {
const { result, rerender } = renderHook(({ connected }) => useWandererWatch(connected), {
initialProps: { connected: true },
const { result, rerender } = renderHook(({ refreshKey }) => useWandererWatch(refreshKey), {
initialProps: { refreshKey: 'true||' },
});
await waitFor(() => expect(result.current.available).toBe(true));
expect(result.current.enabled).toBe(false);
expect(result.current.status).toBeNull();
rerender({ connected: false });
rerender({ refreshKey: 'true||' });
expect(repo.getWatch).toHaveBeenCalledTimes(1);
rerender({ refreshKey: 'false||' });
await waitFor(() => expect(repo.getWatch).toHaveBeenCalledTimes(2));
});
it('drops a refused token once a connection test passes, without a reload', async () => {
repo.getWatch.mockResolvedValueOnce({ ...off, enabled: true, blocked: true, pausedUntil: '2999-01-01T00:00:00Z' });
repo.getWatch.mockResolvedValueOnce({ ...off, enabled: true, lastRunAt: '2026-10-20T10:00:00Z' });
const { result, rerender } = renderHook(({ refreshKey }) => useWandererWatch(refreshKey), {
initialProps: { refreshKey: 'true|2026-10-20T09:00:00Z|false' },
});
await waitFor(() => expect(result.current.status?.text).toBe('wanderer.watch.blocked'));
rerender({ refreshKey: 'true|2026-10-20T11:00:00Z|true' });
await waitFor(() => expect(result.current.status?.tone).toBe('neutral'));
expect(result.current.status?.text).toMatch(/^wanderer\.watch\.lastRun/);
});
it('switches at once, and says so either way', async () => {
const { result } = renderHook(() => useWandererWatch(true));
const { result } = renderHook(() => useWandererWatch('true||'));
await waitFor(() => expect(result.current.available).toBe(true));
await act(async () => {
await result.current.toggle();
@@ -105,7 +119,7 @@ describe('useWandererWatch (WAN-02)', () => {
it('leaves the switch alone without a connection, or when the watch could not be read', async () => {
repo.getWatch.mockResolvedValueOnce({ ...off, connected: false });
const { result } = renderHook(() => useWandererWatch(false));
const { result } = renderHook(() => useWandererWatch('false||'));
await waitFor(() => expect(repo.getWatch).toHaveBeenCalled());
await act(async () => {
await result.current.toggle();
@@ -113,14 +127,14 @@ describe('useWandererWatch (WAN-02)', () => {
expect(repo.setWatch).not.toHaveBeenCalled();
repo.getWatch.mockRejectedValueOnce(new Error('offline'));
const failed = renderHook(() => useWandererWatch(true));
const failed = renderHook(() => useWandererWatch('true||'));
await waitFor(() => expect(repo.getWatch).toHaveBeenCalledTimes(2));
expect(failed.result.current.available).toBe(false);
});
it('lists the hints and opens the tour one is about', async () => {
repo.getWatch.mockResolvedValue({ ...off, enabled: true, hints: [hint] });
const { result } = renderHook(() => useWandererWatch(true));
const { result } = renderHook(() => useWandererWatch('true||'));
await waitFor(() => expect(result.current.hints).toEqual([hint]));
act(() => result.current.openHints());
expect(result.current.hintsOpen).toBe(true);