From 9f388693eb2291ab5b9671edb97c86601e393416 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 1 May 2026 19:03:34 +0700 Subject: [PATCH] fix(media hosting): restore android imgur support --- .../fivechan/android/ImgurLiveUploadTest.java | 2 +- .../MediaUploadAutomationRunnerTest.java | 6 ++-- .../fivechan/android/MediaUploadRecipes.java | 8 ++--- .../__tests__/media-hosting-settings.test.tsx | 6 ++-- src/hooks/__tests__/use-file-upload.test.ts | 31 ++++++++++++++++--- .../__tests__/provider-availability.test.ts | 2 +- .../__tests__/provider-order.test.ts | 20 ++++++------ src/lib/media-hosting/providers.ts | 2 +- .../__tests__/use-media-hosting-store.test.ts | 8 +++++ 9 files changed, 57 insertions(+), 28 deletions(-) diff --git a/android/app/src/androidTest/java/fivechan/android/ImgurLiveUploadTest.java b/android/app/src/androidTest/java/fivechan/android/ImgurLiveUploadTest.java index 6a4e50be..274e5100 100644 --- a/android/app/src/androidTest/java/fivechan/android/ImgurLiveUploadTest.java +++ b/android/app/src/androidTest/java/fivechan/android/ImgurLiveUploadTest.java @@ -40,7 +40,7 @@ public class ImgurLiveUploadTest { } @Test - @Ignore("Imgur upload is no longer supported on Android WebView; keep for manual diagnostics.") + @Ignore("Live provider diagnostic; may fail when Imgur is unavailable from the current network.") public void imgur_liveUpload_fromGeneratedPng_succeeds() throws Exception { Intent launchIntent = new Intent(appContext, MainActivity.class); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/android/app/src/androidTest/java/fivechan/android/MediaUploadAutomationRunnerTest.java b/android/app/src/androidTest/java/fivechan/android/MediaUploadAutomationRunnerTest.java index 76daa2eb..7a08a6da 100644 --- a/android/app/src/androidTest/java/fivechan/android/MediaUploadAutomationRunnerTest.java +++ b/android/app/src/androidTest/java/fivechan/android/MediaUploadAutomationRunnerTest.java @@ -18,10 +18,10 @@ import org.junit.runner.RunWith; * Instrumentation tests for MediaUploadAutomationRunner against controlled HTML fixtures. * Uses DataTransfer injection (no chooser). Simulates: delayed DOM, missing selectors, * success URL extraction, blocked. Validates timeout/error classification (input_not_found, - * blocked/captcha, upload_timed_out). Kept for opt-in diagnostics because Android app uploads no - * longer use this WebView path. + * blocked/captcha, upload_timed_out). Kept opt-in because these instrumentation tests exercise + * WebView timing behavior rather than ordinary unit logic. */ -@Ignore("Imgur WebView upload is no longer supported on Android; keep for manual diagnostics.") +@Ignore("WebView automation fixture diagnostics; run manually with connected Android tests.") @RunWith(AndroidJUnit4.class) public class MediaUploadAutomationRunnerTest { diff --git a/android/app/src/main/java/fivechan/android/MediaUploadRecipes.java b/android/app/src/main/java/fivechan/android/MediaUploadRecipes.java index 432e753d..eb7909dd 100644 --- a/android/app/src/main/java/fivechan/android/MediaUploadRecipes.java +++ b/android/app/src/main/java/fivechan/android/MediaUploadRecipes.java @@ -5,9 +5,9 @@ package fivechan.android; * Reconciled with electron/media-upload-recipes.js for imgbb and imgur. * * Android vs Electron (prevent drift): - * - Android app uploads use native catbox and WebView automation for imgbb. The imgur runner - * is retained for diagnostics/tests only. Real Uri attempts use the WebView file chooser - * callback; fixtures/fallback use DataTransfer JS injection. + * - Android app uploads use native catbox and WebView automation for imgur/imgbb. Real Uri + * attempts use the WebView file chooser callback when possible; fixtures/fallback use + * DataTransfer JS injection. * - Electron: catbox/imgur/imgbb. CDP DOM.setFileInputFiles + submit click. * - Selectors: file input, submit, success extractor, blocked indicators kept in sync for imgbb * and imgur. @@ -36,7 +36,7 @@ public final class MediaUploadRecipes { } public static boolean isAndroidUploadProvider(String provider) { - return PROVIDER_IMGBB.equals(provider); + return PROVIDER_IMGUR.equals(provider) || PROVIDER_IMGBB.equals(provider); } public static String getUploadUrl(String provider) { diff --git a/src/components/settings-modal/media-hosting-settings/__tests__/media-hosting-settings.test.tsx b/src/components/settings-modal/media-hosting-settings/__tests__/media-hosting-settings.test.tsx index c16707d6..3fb3ca55 100644 --- a/src/components/settings-modal/media-hosting-settings/__tests__/media-hosting-settings.test.tsx +++ b/src/components/settings-modal/media-hosting-settings/__tests__/media-hosting-settings.test.tsx @@ -107,18 +107,18 @@ describe('MediaHostingSettings', () => { expect(mockSetUploadMode).toHaveBeenCalledWith('none'); }); - it('disables unsupported providers on Android', async () => { + it('enables Imgur on Android', async () => { uploadModeRef.value = 'preferred'; preferredProviderRef.value = 'catbox'; mockGetPlatform.mockReturnValue('android'); render(); const imgurRadio = container.querySelector('input[value="imgur"]'); expect(imgurRadio).not.toBeNull(); - expect(imgurRadio?.disabled).toBe(true); + expect(imgurRadio?.disabled).toBe(false); await act(async () => { imgurRadio?.click(); }); - expect(mockSetPreferredProvider).not.toHaveBeenCalled(); + expect(mockSetPreferredProvider).toHaveBeenCalledWith('imgur'); }); it('clicking supported provider in preferred mode calls setPreferredProvider', async () => { diff --git a/src/hooks/__tests__/use-file-upload.test.ts b/src/hooks/__tests__/use-file-upload.test.ts index 74f39ee6..3adc212d 100644 --- a/src/hooks/__tests__/use-file-upload.test.ts +++ b/src/hooks/__tests__/use-file-upload.test.ts @@ -44,8 +44,8 @@ vi.mock('../../lib/media-hosting/provider-order', () => ({ if (opts.mode === 'none') return []; if (opts.runtime === 'web') return filterAvailable(opts.preferredProvider === 'catbox' ? ['catbox'] : []); if (opts.runtime === 'android') { - if (opts.mode === 'preferred') return filterAvailable(opts.preferredProvider === 'catbox' || opts.preferredProvider === 'imgbb' ? [opts.preferredProvider] : []); - return filterAvailable(['catbox', 'imgbb']); + if (opts.mode === 'preferred') return filterAvailable(['catbox', 'imgur', 'imgbb'].includes(opts.preferredProvider) ? [opts.preferredProvider] : []); + return filterAvailable(['catbox', 'imgur', 'imgbb']); } return filterAvailable(opts.mode === 'preferred' ? [opts.preferredProvider] : ['catbox', 'imgur', 'imgbb']); }), @@ -332,9 +332,10 @@ describe('useFileUpload', () => { expect(hook().isUploading).toBe(false); }); - it('does not call Android plugin when preferred provider is unsupported', async () => { + it('does not call Android plugin when preferred Imgur is unavailable from this network', async () => { uploadModeRef.value = 'preferred'; preferredProviderRef.value = 'imgur'; + providerAvailabilityRef.value = { imgur: 'unavailable' }; vi.mocked(Capacitor.getPlatform).mockReturnValue('android'); const { onUploadComplete, hook } = mountHook(); @@ -343,7 +344,7 @@ describe('useFileUpload', () => { }); expect(FileUploader.pickAndUploadMedia).not.toHaveBeenCalled(); - expect(window.alert).toHaveBeenCalledWith('upload_failed: imgur is not supported on android. upload_failed_preferred_guidance'); + expect(window.alert).toHaveBeenCalledWith('upload_failed: imgur is unavailable from this network. upload_failed_preferred_guidance'); expect(onUploadComplete).not.toHaveBeenCalled(); expect(hook().isUploading).toBe(false); }); @@ -368,7 +369,7 @@ describe('useFileUpload', () => { it('does not call Android plugin when random mode has no reachable providers', async () => { uploadModeRef.value = 'random'; preferredProviderRef.value = 'catbox'; - providerAvailabilityRef.value = { catbox: 'unavailable', imgbb: 'unavailable' }; + providerAvailabilityRef.value = { catbox: 'unavailable', imgur: 'unavailable', imgbb: 'unavailable' }; vi.mocked(Capacitor.getPlatform).mockReturnValue('android'); const { onUploadComplete, hook } = mountHook(); @@ -401,6 +402,25 @@ describe('useFileUpload', () => { expect(onUploadComplete).toHaveBeenCalledWith('https://i.ibb.co/example/android.png', 'android.png'); }); + it('calls Android plugin with only Imgur when Imgur is chosen and reachable', async () => { + uploadModeRef.value = 'preferred'; + preferredProviderRef.value = 'imgur'; + vi.mocked(Capacitor.getPlatform).mockReturnValue('android'); + vi.mocked(FileUploader.pickAndUploadMedia).mockResolvedValue({ + url: 'https://i.imgur.com/example.png', + fileName: 'android.png', + provider: 'imgur', + }); + const { onUploadComplete, hook } = mountHook(); + + await act(async () => { + await hook().handleUpload(); + }); + + expect(FileUploader.pickAndUploadMedia).toHaveBeenCalledWith({ providerOrder: ['imgur'] }); + expect(onUploadComplete).toHaveBeenCalledWith('https://i.imgur.com/example.png', 'android.png'); + }); + it('Android call includes provider order payload', async () => { uploadModeRef.value = 'random'; preferredProviderRef.value = 'catbox'; @@ -422,6 +442,7 @@ describe('useFileUpload', () => { }); const call = vi.mocked(FileUploader.pickAndUploadMedia).mock.calls[0][0]; expect(call?.providerOrder).toEqual(expect.arrayContaining(['catbox'])); + expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgur'])); expect(call?.providerOrder).toEqual(expect.arrayContaining(['imgbb'])); expect(onUploadComplete).toHaveBeenCalledWith('https://files.catbox.moe/random.jpg', 'random.jpg'); }); diff --git a/src/lib/media-hosting/__tests__/provider-availability.test.ts b/src/lib/media-hosting/__tests__/provider-availability.test.ts index 8a8008ea..1505eabf 100644 --- a/src/lib/media-hosting/__tests__/provider-availability.test.ts +++ b/src/lib/media-hosting/__tests__/provider-availability.test.ts @@ -36,7 +36,7 @@ describe('provider-availability', () => { expect(snapshot.catbox).toBe('available'); expect(snapshot.imgbb).toBe('available'); - expect(snapshot.imgur).toBe('unknown'); + expect(snapshot.imgur).toBe('available'); }); it('marks a provider unavailable when one of its media probes fails', async () => { diff --git a/src/lib/media-hosting/__tests__/provider-order.test.ts b/src/lib/media-hosting/__tests__/provider-order.test.ts index 02ce3526..b2b7b66c 100644 --- a/src/lib/media-hosting/__tests__/provider-order.test.ts +++ b/src/lib/media-hosting/__tests__/provider-order.test.ts @@ -55,6 +55,13 @@ describe('provider-order', () => { runtime: 'web', }), ).toEqual(['catbox']); + expect( + getProviderOrder({ + mode: 'preferred', + preferredProvider: 'imgur', + runtime: 'android', + }), + ).toEqual(['imgur']); expect( getProviderOrder({ mode: 'preferred', @@ -79,13 +86,6 @@ describe('provider-order', () => { runtime: 'web', }), ).toEqual([]); - expect( - getProviderOrder({ - mode: 'preferred', - preferredProvider: 'imgur', - runtime: 'android', - }), - ).toEqual([]); expect( getProviderOrder({ mode: 'preferred', @@ -137,14 +137,14 @@ describe('provider-order', () => { expect(order).toEqual(['catbox']); }); - it('uses catbox and imgbb for android random mode', () => { + it('uses catbox, imgur, and imgbb for android random mode', () => { const order = getProviderOrder({ mode: 'random', preferredProvider: 'catbox', runtime: 'android', }); - expect(order).toHaveLength(2); - expect([...order].sort()).toEqual(['catbox', 'imgbb']); + expect(order).toHaveLength(3); + expect([...order].sort()).toEqual(['catbox', 'imgbb', 'imgur']); }); }); }); diff --git a/src/lib/media-hosting/providers.ts b/src/lib/media-hosting/providers.ts index daaeedac..f4086665 100644 --- a/src/lib/media-hosting/providers.ts +++ b/src/lib/media-hosting/providers.ts @@ -24,7 +24,7 @@ export const MEDIA_HOSTING_PROVIDERS: readonly ProviderDefinition[] = [ label: 'Imgur', homepageUrl: 'https://imgur.com', availabilityProbeUrls: ['https://s.imgur.com/images/favicon-32x32.png', 'https://i.imgur.com/YpB7qfa.jpg'], - supportedRuntimes: ['electron'], + supportedRuntimes: ['electron', 'android'], }, { id: 'imgbb', diff --git a/src/stores/__tests__/use-media-hosting-store.test.ts b/src/stores/__tests__/use-media-hosting-store.test.ts index 154d0efc..3c222883 100644 --- a/src/stores/__tests__/use-media-hosting-store.test.ts +++ b/src/stores/__tests__/use-media-hosting-store.test.ts @@ -28,6 +28,14 @@ describe('useMediaHostingStore', () => { availabilityProbeUrls: ['https://catbox.moe/pictures/logo.png', 'https://files.catbox.moe/8ten4y.png'], supportedRuntimes: ['web', 'electron', 'android'], }); + const imgur = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'imgur'); + expect(imgur).toEqual({ + id: 'imgur', + label: 'Imgur', + homepageUrl: 'https://imgur.com', + availabilityProbeUrls: ['https://s.imgur.com/images/favicon-32x32.png', 'https://i.imgur.com/YpB7qfa.jpg'], + supportedRuntimes: ['electron', 'android'], + }); const imgbb = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'imgbb'); expect(imgbb).toEqual({ id: 'imgbb',