feat(media hosting): disable unreachable providers

This commit is contained in:
Tommaso Casaburi
2026-05-01 22:52:55 +07:00
parent 78e5d58d08
commit 7e1513b688
45 changed files with 596 additions and 130 deletions
@@ -17,11 +17,18 @@ vi.mock('@capacitor/core', () => ({
Capacitor: { getPlatform: () => mockGetPlatform() },
}));
const providerAvailabilityRef = vi.hoisted(() => ({
value: {} as Record<string, 'unknown' | 'checking' | 'available' | 'unavailable'>,
}));
vi.mock('../../../../hooks/use-provider-availability', () => ({
useProviderAvailability: () => providerAvailabilityRef.value,
}));
const mockSetUploadMode = vi.fn();
const mockSetPreferredProvider = vi.fn();
const uploadModeRef = vi.hoisted(() => ({ value: 'random' as 'random' | 'preferred' | 'none' }));
const preferredProviderRef = vi.hoisted(() => ({
value: 'catbox' as 'catbox' | 'imgur',
value: 'catbox' as 'catbox' | 'imgur' | 'imgbb',
}));
vi.mock('../../../../stores/use-media-hosting-store', async (importOriginal) => {
const mod = await importOriginal<typeof import('../../../../stores/use-media-hosting-store')>();
@@ -51,6 +58,7 @@ describe('MediaHostingSettings', () => {
vi.clearAllMocks();
uploadModeRef.value = 'random';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = {};
mockGetPlatform.mockReturnValue('web');
(window as unknown as { electronApi?: unknown }).electronApi = undefined;
container = document.createElement('div');
@@ -99,18 +107,63 @@ describe('MediaHostingSettings', () => {
expect(mockSetUploadMode).toHaveBeenCalledWith('none');
});
it('clicking provider in preferred mode calls setPreferredProvider', async () => {
it('disables unsupported providers on Android', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
mockGetPlatform.mockReturnValue('android');
render();
const imgurRadio = container.querySelector<HTMLInputElement>('input[value="imgur"]');
expect(imgurRadio).not.toBeNull();
expect(imgurRadio?.disabled).toBe(false);
expect(imgurRadio?.disabled).toBe(true);
await act(async () => {
imgurRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('imgur');
expect(mockSetPreferredProvider).not.toHaveBeenCalled();
});
it('clicking supported provider in preferred mode calls setPreferredProvider', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'imgur';
mockGetPlatform.mockReturnValue('android');
render();
const catboxRadio = container.querySelector<HTMLInputElement>('input[value="catbox"]');
expect(catboxRadio).not.toBeNull();
expect(catboxRadio?.disabled).toBe(false);
await act(async () => {
catboxRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('catbox');
});
it('enables ImgBB on Android', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
mockGetPlatform.mockReturnValue('android');
render();
const imgbbRadio = container.querySelector<HTMLInputElement>('input[value="imgbb"]');
expect(imgbbRadio).not.toBeNull();
expect(imgbbRadio?.disabled).toBe(false);
await act(async () => {
imgbbRadio?.click();
});
expect(mockSetPreferredProvider).toHaveBeenCalledWith('imgbb');
});
it('disables providers that are unavailable from this network', async () => {
uploadModeRef.value = 'preferred';
preferredProviderRef.value = 'catbox';
providerAvailabilityRef.value = { imgbb: 'unavailable' };
mockGetPlatform.mockReturnValue('android');
render();
const imgbbRadio = container.querySelector<HTMLInputElement>('input[value="imgbb"]');
expect(imgbbRadio).not.toBeNull();
expect(imgbbRadio?.disabled).toBe(true);
expect(imgbbRadio?.closest('label')?.title).toBe('media_hosting_provider_unavailable');
expect(container.textContent).toContain('media_hosting_provider_unavailable');
await act(async () => {
imgbbRadio?.click();
});
expect(mockSetPreferredProvider).not.toHaveBeenCalled();
});
it('clicking Random updates store via setUploadMode', async () => {
@@ -2,7 +2,8 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import useMediaHostingStore, { MEDIA_HOSTING_PROVIDERS } from '../../../stores/use-media-hosting-store';
import type { ProviderId } from '../../../lib/media-hosting/types';
import { isWebRuntime } from '../../../lib/media-hosting/show-upload-controls';
import { getMediaHostingRuntime } from '../../../lib/media-hosting/show-upload-controls';
import { useProviderAvailability } from '../../../hooks/use-provider-availability';
import styles from '../interface-settings/interface-settings.module.css';
const RADIO_NAME = 'media-hosting-provider';
@@ -14,7 +15,9 @@ const MediaHostingSettings = () => {
const preferredProvider = useMediaHostingStore((state) => state.preferredProvider);
const setUploadMode = useMediaHostingStore((state) => state.setUploadMode);
const setPreferredProvider = useMediaHostingStore((state) => state.setPreferredProvider);
const isWeb = isWebRuntime();
const runtime = getMediaHostingRuntime();
const isWeb = runtime === 'web';
const availability = useProviderAvailability(runtime);
return (
<div className={styles.interfaceSettings}>
@@ -40,25 +43,30 @@ const MediaHostingSettings = () => {
</label>
{uploadMode === 'preferred' && (
<div role='radiogroup' aria-label={t('media_hosting_preferred_provider_label')}>
{MEDIA_HOSTING_PROVIDERS.map((provider) => (
<div key={provider.id} className={styles.setting}>
<label>
<input
type='radio'
name={`${RADIO_NAME}-provider`}
value={provider.id}
checked={preferredProvider === provider.id}
onChange={() => setPreferredProvider(provider.id as ProviderId)}
disabled={isWeb}
/>
{provider.label} (
<a href={provider.homepageUrl} target='_blank' rel='noopener noreferrer'>
{provider.homepageUrl}
</a>
)
</label>
</div>
))}
{MEDIA_HOSTING_PROVIDERS.map((provider) => {
const providerUnavailable = availability[provider.id] === 'unavailable';
const providerDisabled = isWeb || !provider.supportedRuntimes.includes(runtime) || providerUnavailable;
return (
<div key={provider.id} className={styles.setting}>
<label title={providerUnavailable ? t('media_hosting_provider_unavailable') : undefined}>
<input
type='radio'
name={`${RADIO_NAME}-provider`}
value={provider.id}
checked={preferredProvider === provider.id}
onChange={() => setPreferredProvider(provider.id as ProviderId)}
disabled={providerDisabled}
/>
{provider.label} (
<a href={provider.homepageUrl} target='_blank' rel='noopener noreferrer'>
{provider.homepageUrl}
</a>
)
</label>
{providerUnavailable && <div className={styles.settingTip}>{t('media_hosting_provider_unavailable')}</div>}
</div>
);
})}
</div>
)}
</div>