fix(settings): a failed photo provider test reads "Immich: reason" on the desktop

On the desktop, a failed photo provider connection test toasted "Could not
connect to Immich : Auth failed" with a stray space before the colon, and
"Could not connect to Immich " with a trailing space when the server gave
no reason. The phone already read "Could not connect to Immich: Auth
failed".

usePhotoProviderConnections took a testErrorGap option that put a space
between the sentence and ": reason", and the desktop passed ' '. The option
is gone, so both views join the reason directly.

Regression test FE-COMP-PHOTOPROVIDERS-027 (PhotoProvidersSection.test.tsx)
was red on the old code: "Unable to find an element with the text: Could
not connect to Immich: Auth failed" (the toast read "Could not connect to
Immich : Auth failed").

Changed assertions:
- usePhotoProviderConnections.test.ts FE-COMP-PHOTOCONN-010: the desktop
  toast was 'memories.connectionError(Immich) : bad key', now
  'memories.connectionError(Immich): bad key'; it also checks that a
  failure without a reason toasts the bare sentence.
This commit is contained in:
Maurice
2026-10-09 17:38:20 +02:00
parent 12962480d5
commit 0b90fc8cf5
5 changed files with 31 additions and 13 deletions
@@ -1,4 +1,4 @@
// FE-COMP-PHOTOPROVIDERS-001 to FE-COMP-PHOTOPROVIDERS-026
// FE-COMP-PHOTOPROVIDERS-001 to FE-COMP-PHOTOPROVIDERS-027
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -263,6 +263,23 @@ describe('PhotoProvidersSection', () => {
expect(await screen.findByText(/Auth failed/i)).toBeInTheDocument();
});
it('FE-COMP-PHOTOPROVIDERS-027: a failed test reads "provider: reason" with no stray space', async () => {
const user = userEvent.setup();
server.use(
http.post('/api/addons/immich/test', () => HttpResponse.json({ connected: false, error: 'Auth failed' })),
);
seedMemoriesEnabled();
render(
<>
<ToastContainer />
<PhotoProvidersSection />
</>,
);
await screen.findByText('Immich');
await user.click(screen.getByRole('button', { name: /test connection/i }));
expect(await screen.findByText('Could not connect to Immich: Auth failed')).toBeInTheDocument();
});
it('FE-COMP-PHOTOPROVIDERS-016: Test button is disabled while test is in progress', async () => {
const user = userEvent.setup();
let resolveTest!: () => void;
@@ -27,7 +27,7 @@ export default function PhotoProvidersSection(): React.ReactElement {
isProviderSaveDisabled,
handleSaveProvider,
handleTestProvider,
} = usePhotoProviderConnections({ statusRouteOwnsBadge: false, testErrorGap: ' ' })
} = usePhotoProviderConnections({ statusRouteOwnsBadge: false })
const renderPhotoProviderSection = (provider: PhotoProviderAddon): React.ReactElement => {
const fields = getProviderFields(provider)
@@ -81,8 +81,8 @@ function stubGets(byPath: Record<string, Resp | (() => Promise<Resp>)>) {
});
}
function render(statusRouteOwnsBadge: boolean, testErrorGap = '') {
return renderHook(() => usePhotoProviderConnections({ statusRouteOwnsBadge, testErrorGap }));
function render(statusRouteOwnsBadge: boolean) {
return renderHook(() => usePhotoProviderConnections({ statusRouteOwnsBadge }));
}
beforeEach(() => {
@@ -216,16 +216,19 @@ describe('usePhotoProviderConnections', () => {
expect(result.current.providerTesting.immich).toBe(false);
});
it('FE-COMP-PHOTOCONN-010: a failed probe joins the server reason with the gap each shell asks for', async () => {
it('FE-COMP-PHOTOCONN-010: a failed probe joins the server reason as ": reason" on both shells', async () => {
seedProviders([PROVIDER]);
stubGets({ '/addons/immich/settings': { data: {} }, '/addons/immich/status': { data: {} } });
vi.spyOn(apiClient, 'post').mockResolvedValue({ data: { connected: false, error: 'bad key' } });
const phone = render(true, '');
const phone = render(true);
await act(() => phone.result.current.handleTestProvider(PROVIDER));
expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich): bad key');
const desktop = render(false, ' ');
const desktop = render(false);
await act(() => desktop.result.current.handleTestProvider(PROVIDER));
expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich) : bad key');
expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich): bad key');
vi.mocked(apiClient.post).mockResolvedValue({ data: { connected: false } });
await act(() => desktop.result.current.handleTestProvider(PROVIDER));
expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich)');
vi.mocked(apiClient.post).mockRejectedValue(new Error('x'));
await act(() => desktop.result.current.handleTestProvider(PROVIDER));
expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich)');
@@ -56,8 +56,6 @@ export interface PhotoProviderConnectionOptions {
* race. The phone does this; the desktop takes whichever answer lands last.
*/
statusRouteOwnsBadge: boolean;
/** What goes between the failed test sentence and the server's ": reason" ('' on the phone, ' ' on the desktop). */
testErrorGap: string;
}
/**
@@ -66,7 +64,7 @@ export interface PhotoProviderConnectionOptions {
* (secrets never prefilled), the connection badge, and save and test. The desktop
* section and its phone twin render their own markup over this.
*/
export function usePhotoProviderConnections({ statusRouteOwnsBadge, testErrorGap }: PhotoProviderConnectionOptions) {
export function usePhotoProviderConnections({ statusRouteOwnsBadge }: PhotoProviderConnectionOptions) {
const { t } = useTranslation();
const toast = useToast();
const { isEnabled: addonEnabled, addons } = useAddonStore();
@@ -220,7 +218,7 @@ export function usePhotoProviderConnections({ statusRouteOwnsBadge, testErrorGap
toast.success(t('memories.connectionSuccess', { provider_name: provider.name }));
} else {
const detail = res.data?.error ? `: ${String(res.data.error)}` : '';
toast.error(`${t('memories.connectionError', { provider_name: provider.name })}${testErrorGap}${detail}`);
toast.error(`${t('memories.connectionError', { provider_name: provider.name })}${detail}`);
}
} catch {
toast.error(t('memories.connectionError', { provider_name: provider.name }));
@@ -30,7 +30,7 @@ export default function MPhotoProvidersSection(): React.ReactElement {
isProviderSaveDisabled,
handleSaveProvider,
handleTestProvider,
} = usePhotoProviderConnections({ statusRouteOwnsBadge: true, testErrorGap: '' })
} = usePhotoProviderConnections({ statusRouteOwnsBadge: true })
const renderPhotoProviderSection = (provider: PhotoProviderAddon): React.ReactElement => {
const fields = getProviderFields(provider)