mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
94ac7673a5
The settings pages had grown into a mix of styles: folds, dialogs behind rows, full-width rows next to boxed ones, pills for every figure and buttons in four sizes. Every page now uses the same kit. - Cards keep their tinted head band; every part of a card is a rounded block with its own small head (SettingsBlock), and rows sit in rounded boxes. - Nothing folds and nothing hides behind a dialog: the MCP config, OAuth scopes, mail how-to, AI privacy note and addon sub-options are all shown. - Each page is split into two columns balanced by length (SettingsColumns). - Long explanations moved into an info mark next to the label (HintMark). - One button size everywhere (text-control); tab names are one word. - Admin: the old Settings tab is split into Security and Services (?tab=settings still opens Security, and the phone keeps its single Settings section for both); the Integration group is gone, live flights and mail import sit next to the addons they configure. - Addons: one card per type with rows, no search, filter or fold. - Quota, TREK API facts, mail import status and Google's daily limit are compact lines and strips instead of tiles and pills. - Language is a select; routing has its own card under the defaults; storage saves from the foot of its categories card. - Unused keys, FoldSummary and the page-width wrapper are removed.
568 lines
22 KiB
TypeScript
568 lines
22 KiB
TypeScript
// FE-COMP-PHOTOPROVIDERS-001 to FE-COMP-PHOTOPROVIDERS-028
|
||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { http, HttpResponse } from 'msw';
|
||
import { server } from '../../../tests/helpers/msw/server';
|
||
import { useAuthStore } from '../../store/authStore';
|
||
import { useAddonStore } from '../../store/addonStore';
|
||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||
import { buildUser } from '../../../tests/helpers/factories';
|
||
import { ToastContainer } from '../shared/Toast';
|
||
import PhotoProvidersSection from './PhotoProvidersSection';
|
||
|
||
const fakeProvider = {
|
||
id: 'immich',
|
||
name: 'Immich',
|
||
type: 'photo_provider',
|
||
enabled: true,
|
||
// Widened so per-test overrides can swap test_post for test_get.
|
||
config: {
|
||
settings_get: '/addons/immich/settings',
|
||
settings_put: '/addons/immich/settings',
|
||
status_get: '/addons/immich/status',
|
||
test_post: '/addons/immich/test',
|
||
} as Record<string, string>,
|
||
fields: [
|
||
{ key: 'url', label: 'url', input_type: 'text', placeholder: 'https://...', required: true, secret: false, settings_key: 'url', payload_key: 'url', sort_order: 0 },
|
||
{ key: 'api_key', label: 'api_key', input_type: 'text', placeholder: null, required: true, secret: true, settings_key: 'api_key', payload_key: 'api_key', sort_order: 1 },
|
||
],
|
||
};
|
||
|
||
// A simpler provider with only a non-secret required field (url), useful for Save tests
|
||
const fakeProviderSimple = {
|
||
...fakeProvider,
|
||
fields: [fakeProvider.fields[0]], // only the url field
|
||
};
|
||
|
||
function seedMemoriesEnabled(providers = [fakeProvider]) {
|
||
seedStore(useAddonStore, {
|
||
addons: [
|
||
{ id: 'memories', type: 'memories', enabled: true },
|
||
...providers,
|
||
],
|
||
isEnabled: (id: string) => id === 'memories' || providers.some(p => p.id === id),
|
||
});
|
||
}
|
||
|
||
beforeEach(() => {
|
||
resetAllStores();
|
||
vi.clearAllMocks();
|
||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
||
seedStore(useAddonStore, {
|
||
addons: [],
|
||
isEnabled: () => false,
|
||
});
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () => HttpResponse.json({ url: 'https://photos.example.com', connected: false })),
|
||
http.get('/api/addons/immich/status', () => HttpResponse.json({ connected: false })),
|
||
http.put('/api/addons/immich/settings', () => HttpResponse.json({ success: true })),
|
||
http.post('/api/addons/immich/test', () => HttpResponse.json({ connected: true })),
|
||
);
|
||
});
|
||
|
||
describe('PhotoProvidersSection', () => {
|
||
it('FE-COMP-PHOTOPROVIDERS-001: renders nothing when memories addon is disabled', () => {
|
||
const { container } = render(<PhotoProvidersSection />);
|
||
expect(container).toBeEmptyDOMElement();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-002: renders nothing when there are no active photo providers', async () => {
|
||
seedStore(useAddonStore, {
|
||
addons: [{ id: 'memories', type: 'memories', enabled: true }],
|
||
isEnabled: (id: string) => id === 'memories',
|
||
});
|
||
const { container } = render(<PhotoProvidersSection />);
|
||
// Give the component a moment to potentially render something
|
||
await new Promise(r => setTimeout(r, 50));
|
||
expect(container.querySelector('section, [class*="section"]')).toBeNull();
|
||
expect(screen.queryByText('Immich')).toBeNull();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-003: renders a section card for each active provider', async () => {
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
expect(await screen.findByText('Immich')).toBeInTheDocument();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-004: renders field inputs for each provider field', async () => {
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
const inputs = screen.getAllByRole('textbox');
|
||
expect(inputs.length).toBeGreaterThanOrEqual(2);
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-005: non-secret field is prefilled with value from settings API', async () => {
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
expect(await screen.findByDisplayValue('https://photos.example.com')).toBeInTheDocument();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-006: secret field is NOT prefilled (blank value)', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () =>
|
||
HttpResponse.json({ url: 'https://photos.example.com', api_key: 'super-secret-key', connected: false }),
|
||
),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
// api_key field should remain blank
|
||
const inputs = screen.getAllByRole('textbox');
|
||
const apiKeyInput = inputs.find(i => (i as HTMLInputElement).value === '');
|
||
expect(apiKeyInput).toBeDefined();
|
||
expect((apiKeyInput as HTMLInputElement).value).toBe('');
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-007: secret field shows masked placeholder when connected', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () =>
|
||
HttpResponse.json({ url: 'https://photos.example.com', connected: true }),
|
||
),
|
||
http.get('/api/addons/immich/status', () => HttpResponse.json({ connected: true })),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
await waitFor(() => {
|
||
const inputs = screen.getAllByRole('textbox');
|
||
const maskedInput = inputs.find(i => (i as HTMLInputElement).placeholder === '••••••••');
|
||
expect(maskedInput).toBeDefined();
|
||
});
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-008: Save button is disabled when required non-secret field is empty', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () => HttpResponse.json({ url: '', connected: false })),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
await waitFor(() => {
|
||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||
expect(saveBtn).toBeDisabled();
|
||
});
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-009: Save button is enabled when all required fields are filled', async () => {
|
||
const user = userEvent.setup();
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
// url is prefilled, but api_key (required + secret) must also be filled
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
const inputs = screen.getAllByRole('textbox');
|
||
const apiKeyInput = inputs.find(i => (i as HTMLInputElement).value === '') as HTMLInputElement;
|
||
await user.type(apiKeyInput, 'some-api-key');
|
||
await waitFor(() => {
|
||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||
expect(saveBtn).not.toBeDisabled();
|
||
});
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-010: clicking Save calls PUT settings endpoint', async () => {
|
||
const user = userEvent.setup();
|
||
let putCalled = false;
|
||
server.use(
|
||
http.put('/api/addons/immich/settings', () => {
|
||
putCalled = true;
|
||
return HttpResponse.json({ success: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
const saveBtn = await screen.findByRole('button', { name: /save/i });
|
||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||
await user.click(saveBtn);
|
||
await waitFor(() => expect(putCalled).toBe(true));
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-011: successful save shows success toast', async () => {
|
||
const user = userEvent.setup();
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(
|
||
<>
|
||
<ToastContainer />
|
||
<PhotoProvidersSection />
|
||
</>,
|
||
);
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
const saveBtn = await screen.findByRole('button', { name: /save/i });
|
||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||
await user.click(saveBtn);
|
||
await screen.findByText(/immich settings saved/i);
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-012: failed save shows error toast', async () => {
|
||
const user = userEvent.setup();
|
||
server.use(
|
||
http.put('/api/addons/immich/settings', () => HttpResponse.json({ error: 'Server error' }, { status: 500 })),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(
|
||
<>
|
||
<ToastContainer />
|
||
<PhotoProvidersSection />
|
||
</>,
|
||
);
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
const saveBtn = await screen.findByRole('button', { name: /save/i });
|
||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||
await user.click(saveBtn);
|
||
await screen.findByText(/could not save immich/i);
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-013: clicking Test Connection calls the test endpoint', async () => {
|
||
const user = userEvent.setup();
|
||
let testCalled = false;
|
||
server.use(
|
||
http.post('/api/addons/immich/test', () => {
|
||
testCalled = true;
|
||
return HttpResponse.json({ connected: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||
await user.click(testBtn);
|
||
await waitFor(() => expect(testCalled).toBe(true));
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-014: successful test shows "Connected" badge', async () => {
|
||
const user = userEvent.setup();
|
||
server.use(
|
||
http.post('/api/addons/immich/test', () => HttpResponse.json({ connected: true })),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||
await user.click(testBtn);
|
||
// Exact text on purpose: the badge always renders, and a loose /connected/i
|
||
// also matches the "Not connected" it starts out as.
|
||
expect(await screen.findByText('Connected')).toBeInTheDocument();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-015: failed test shows error toast', 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');
|
||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||
await user.click(testBtn);
|
||
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-028: a settings read landing after the status probe does not flip the badge to connected', async () => {
|
||
let releaseSettings!: () => void;
|
||
const settingsGate = new Promise<void>(r => (releaseSettings = r));
|
||
let statusAnswered = false;
|
||
server.use(
|
||
// Stored credentials make the settings read say connected; the probe says the server is down.
|
||
http.get('/api/addons/immich/settings', async () => {
|
||
await settingsGate;
|
||
return HttpResponse.json({ url: 'https://photos.example.com', connected: true });
|
||
}),
|
||
http.get('/api/addons/immich/status', () => {
|
||
statusAnswered = true;
|
||
return HttpResponse.json({ connected: false });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
await waitFor(() => expect(statusAnswered).toBe(true));
|
||
releaseSettings();
|
||
await waitFor(() => expect(screen.getByDisplayValue('https://photos.example.com')).toBeInTheDocument());
|
||
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
||
expect(screen.queryByText('Connected')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-016: Test button is disabled while test is in progress', async () => {
|
||
const user = userEvent.setup();
|
||
let resolveTest!: () => void;
|
||
server.use(
|
||
http.post('/api/addons/immich/test', async () => {
|
||
await new Promise<void>(resolve => {
|
||
resolveTest = resolve;
|
||
});
|
||
return HttpResponse.json({ connected: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled();
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||
await user.click(testBtn);
|
||
await waitFor(() => expect(testBtn).toBeDisabled());
|
||
resolveTest();
|
||
await waitFor(() => expect(testBtn).not.toBeDisabled());
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-017: Save button is disabled while saving', async () => {
|
||
const user = userEvent.setup();
|
||
let resolveSave!: () => void;
|
||
server.use(
|
||
http.put('/api/addons/immich/settings', async () => {
|
||
await new Promise<void>(resolve => {
|
||
resolveSave = resolve;
|
||
});
|
||
return HttpResponse.json({ success: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
const saveBtn = await screen.findByRole('button', { name: /save/i });
|
||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||
await user.click(saveBtn);
|
||
await waitFor(() => expect(saveBtn).toBeDisabled());
|
||
resolveSave();
|
||
await waitFor(() => expect(saveBtn).not.toBeDisabled());
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-018: multiple providers each get their own Section card', async () => {
|
||
const secondProvider = {
|
||
id: 'piwigo',
|
||
name: 'Piwigo',
|
||
type: 'photo_provider',
|
||
enabled: true,
|
||
config: {
|
||
settings_get: '/addons/piwigo/settings',
|
||
settings_put: '/addons/piwigo/settings',
|
||
status_get: '/addons/piwigo/status',
|
||
test_post: '/addons/piwigo/test',
|
||
},
|
||
fields: [
|
||
{ key: 'url', label: 'url', input_type: 'text', placeholder: 'https://...', required: true, secret: false, settings_key: 'url', payload_key: 'url', sort_order: 0 },
|
||
],
|
||
};
|
||
server.use(
|
||
http.get('/api/addons/piwigo/settings', () => HttpResponse.json({ url: '', connected: false })),
|
||
http.get('/api/addons/piwigo/status', () => HttpResponse.json({ connected: false })),
|
||
);
|
||
seedMemoriesEnabled([fakeProvider, secondProvider]);
|
||
render(<PhotoProvidersSection />);
|
||
await screen.findByText('Immich');
|
||
expect(await screen.findByText('Piwigo')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
// ── Checkbox fields, missing values and failing probes (019–023) ──────────────
|
||
|
||
const providerWithCheckbox = {
|
||
...fakeProvider,
|
||
fields: [
|
||
fakeProvider.fields[0],
|
||
{
|
||
key: 'verify_ssl',
|
||
label: 'verify_ssl',
|
||
input_type: 'checkbox',
|
||
placeholder: null,
|
||
required: false,
|
||
secret: false,
|
||
settings_key: 'verify_ssl',
|
||
payload_key: 'verify_ssl',
|
||
sort_order: 1,
|
||
},
|
||
],
|
||
};
|
||
|
||
/** The ToggleSwitch rendered for a checkbox-typed provider field. */
|
||
function checkboxToggle(): HTMLElement {
|
||
return screen.getAllByRole('button').find(b => b.hasAttribute('aria-pressed')) as HTMLElement;
|
||
}
|
||
|
||
describe('PhotoProvidersSection – checkbox fields and failures', () => {
|
||
it('FE-COMP-PHOTOPROVIDERS-019: a checkbox field starts off and is sent as a boolean', async () => {
|
||
const user = userEvent.setup();
|
||
let body: Record<string, unknown> | null = null;
|
||
server.use(
|
||
http.put('/api/addons/immich/settings', async ({ request }) => {
|
||
body = (await request.json()) as Record<string, unknown>;
|
||
return HttpResponse.json({ success: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled([providerWithCheckbox]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
expect(checkboxToggle()).toHaveAttribute('aria-pressed', 'false');
|
||
|
||
await user.click(checkboxToggle());
|
||
expect(checkboxToggle()).toHaveAttribute('aria-pressed', 'true');
|
||
|
||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||
|
||
await waitFor(() => expect(body).toEqual({ url: 'https://photos.example.com', verify_ssl: true }));
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-020: a stored checkbox value hydrates the toggle', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () =>
|
||
HttpResponse.json({ url: 'https://photos.example.com', verify_ssl: true, connected: false }),
|
||
),
|
||
);
|
||
seedMemoriesEnabled([providerWithCheckbox]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
await screen.findByDisplayValue('https://photos.example.com');
|
||
await waitFor(() => expect(checkboxToggle()).toHaveAttribute('aria-pressed', 'true'));
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-021: a settings payload without the field leaves the input empty', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () => HttpResponse.json({ connected: false })),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
await screen.findByText('Immich');
|
||
await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue(''));
|
||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-022: a failing status probe leaves the provider disconnected', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () => HttpResponse.json({ url: 'https://photos.example.com' })),
|
||
http.get('/api/addons/immich/status', () => HttpResponse.json({ error: 'down' }, { status: 500 })),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
await screen.findByText('Immich');
|
||
await waitFor(() => expect(screen.getByText('Not connected')).toBeInTheDocument());
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-023: a test request that errors out falls back to the plain error', async () => {
|
||
const user = userEvent.setup();
|
||
server.use(
|
||
http.post('/api/addons/immich/test', () => HttpResponse.json({ error: 'boom' }, { status: 500 })),
|
||
);
|
||
seedMemoriesEnabled([fakeProviderSimple]);
|
||
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')).toBeInTheDocument();
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-024: a provider without a test POST route probes over GET instead', async () => {
|
||
const user = userEvent.setup();
|
||
let probed = false;
|
||
server.use(
|
||
http.get('/api/addons/immich/probe', () => {
|
||
probed = true;
|
||
return HttpResponse.json({ connected: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled([{
|
||
...fakeProviderSimple,
|
||
config: {
|
||
settings_get: '/addons/immich/settings',
|
||
settings_put: '/addons/immich/settings',
|
||
status_get: '/addons/immich/status',
|
||
test_get: '/addons/immich/probe',
|
||
},
|
||
}]);
|
||
render(
|
||
<>
|
||
<ToastContainer />
|
||
<PhotoProvidersSection />
|
||
</>,
|
||
);
|
||
|
||
await screen.findByText('Immich');
|
||
await user.click(screen.getByRole('button', { name: /test connection/i }));
|
||
|
||
await waitFor(() => expect(probed).toBe(true));
|
||
await screen.findByText('Connected to Immich');
|
||
});
|
||
});
|
||
|
||
// ── The Immich self-signed switch, as the server describes it (025, 026) ─────
|
||
|
||
/** The Immich card exactly as the provider rows seed it (#2475). */
|
||
const immichProvider = {
|
||
...fakeProvider,
|
||
fields: [
|
||
{ key: 'immich_url', label: 'providerUrl', input_type: 'url', placeholder: 'https://immich.example.com', required: true, secret: false, settings_key: 'immich_url', payload_key: 'immich_url', sort_order: 0 },
|
||
{ key: 'immich_api_key', label: 'providerApiKey', input_type: 'password', placeholder: 'API Key', required: true, secret: true, settings_key: null, payload_key: 'immich_api_key', sort_order: 1 },
|
||
{ key: 'immich_allow_insecure_tls', label: 'skipSSLVerification', input_type: 'checkbox', placeholder: null, required: false, secret: false, settings_key: 'allow_insecure_tls', payload_key: 'allow_insecure_tls', sort_order: 2 },
|
||
],
|
||
};
|
||
|
||
describe('PhotoProvidersSection: Immich self-signed certificates', () => {
|
||
it('FE-COMP-PHOTOPROVIDERS-025: a stored switch shows as on under its Synology wording', async () => {
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () =>
|
||
HttpResponse.json({ immich_url: 'https://immich.lan', connected: true, auto_upload: false, allow_insecure_tls: true }),
|
||
),
|
||
);
|
||
seedMemoriesEnabled([immichProvider]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
expect(await screen.findByText('Skip SSL certificate verification')).toBeInTheDocument();
|
||
await waitFor(() => expect(checkboxToggle()).toHaveAttribute('aria-pressed', 'true'));
|
||
});
|
||
|
||
it('FE-COMP-PHOTOPROVIDERS-026: turning it on sends it with the connection test, as a boolean', async () => {
|
||
const user = userEvent.setup();
|
||
let body: Record<string, unknown> | null = null;
|
||
server.use(
|
||
http.get('/api/addons/immich/settings', () =>
|
||
HttpResponse.json({ immich_url: 'https://immich.lan', connected: false, auto_upload: false, allow_insecure_tls: false }),
|
||
),
|
||
http.post('/api/addons/immich/test', async ({ request }) => {
|
||
body = (await request.json()) as Record<string, unknown>;
|
||
return HttpResponse.json({ connected: true });
|
||
}),
|
||
);
|
||
seedMemoriesEnabled([immichProvider]);
|
||
render(<PhotoProvidersSection />);
|
||
|
||
await screen.findByDisplayValue('https://immich.lan');
|
||
await waitFor(() => expect(checkboxToggle()).toHaveAttribute('aria-pressed', 'false'));
|
||
await user.type(screen.getByPlaceholderText('API Key'), 'secret-key');
|
||
await user.click(checkboxToggle());
|
||
await user.click(screen.getByRole('button', { name: /test connection/i }));
|
||
|
||
await waitFor(() =>
|
||
expect(body).toEqual({ immich_url: 'https://immich.lan', immich_api_key: 'secret-key', allow_insecure_tls: true }),
|
||
);
|
||
});
|
||
});
|