mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(file-upload): normalize Android Capacitor rejection and surface attempt details in UI
This commit is contained in:
@@ -236,6 +236,53 @@ describe('useFileUpload', () => {
|
||||
expect(hook().isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces Android Capacitor rejection attempts in user-visible alert (preferred mode)', async () => {
|
||||
uploadModeRef.value = 'preferred';
|
||||
preferredProviderRef.value = 'catbox';
|
||||
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
|
||||
const capacitorRejection = new Error('All providers failed: catbox: timeout') as Error & {
|
||||
data?: { attempts?: { provider: string; error: string; stage?: string; elapsedMs?: number }[] };
|
||||
};
|
||||
capacitorRejection.data = {
|
||||
attempts: [{ provider: 'catbox', error: 'timeout', stage: 'timeout', elapsedMs: 5000 }],
|
||||
};
|
||||
vi.mocked(FileUploader.pickAndUploadMedia).mockRejectedValue(capacitorRejection);
|
||||
const { onUploadComplete, hook } = mountHook();
|
||||
|
||||
await act(async () => {
|
||||
await hook().handleUpload();
|
||||
});
|
||||
|
||||
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout (stage=timeout, 5000ms)');
|
||||
expect(onUploadComplete).not.toHaveBeenCalled();
|
||||
expect(hook().isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces Android rejection with multiple provider attempts in alert', async () => {
|
||||
uploadModeRef.value = 'random';
|
||||
preferredProviderRef.value = 'catbox';
|
||||
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
|
||||
const capacitorRejection = new Error('All providers failed') as Error & {
|
||||
data?: { attempts?: { provider: string; error: string }[] };
|
||||
};
|
||||
capacitorRejection.data = {
|
||||
attempts: [
|
||||
{ provider: 'catbox', error: 'timeout' },
|
||||
{ provider: 'imgur', error: 'rate limit' },
|
||||
],
|
||||
};
|
||||
vi.mocked(FileUploader.pickAndUploadMedia).mockRejectedValue(capacitorRejection);
|
||||
const { onUploadComplete, hook } = mountHook();
|
||||
|
||||
await act(async () => {
|
||||
await hook().handleUpload();
|
||||
});
|
||||
|
||||
expect(window.alert).toHaveBeenCalledWith('upload_failed. upload_failed_all_providers: catbox: timeout; imgur: rate limit');
|
||||
expect(onUploadComplete).not.toHaveBeenCalled();
|
||||
expect(hook().isUploading).toBe(false);
|
||||
});
|
||||
|
||||
it('returns early when uploadMode is none (no upload, no alert)', async () => {
|
||||
uploadModeRef.value = 'none';
|
||||
vi.mocked(Capacitor.getPlatform).mockReturnValue('android');
|
||||
|
||||
@@ -6,9 +6,66 @@ import { formatAggregatedError, formatPreferredModeError, type ProviderAttempt }
|
||||
import { getProviderOrder } from '../lib/media-hosting/provider-order';
|
||||
import { orchestrateElectronUpload } from '../lib/media-hosting/upload-orchestrator';
|
||||
import useMediaHostingStore from '../stores/use-media-hosting-store';
|
||||
import type { ProviderId } from '../lib/media-hosting/types';
|
||||
|
||||
const FILE_SELECTION_CANCELLED_ERROR = 'File selection cancelled';
|
||||
|
||||
const VALID_PROVIDERS: ProviderId[] = ['catbox', 'imgur', 'postimages'];
|
||||
|
||||
/** Raw attempt shape from Android plugin rejection payload */
|
||||
interface RawAttempt {
|
||||
provider?: unknown;
|
||||
success?: unknown;
|
||||
error?: unknown;
|
||||
stage?: unknown;
|
||||
elapsedMs?: unknown;
|
||||
matchedSelectors?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes Android Capacitor rejection payload. The plugin may reject with
|
||||
* `{ message, code, data: { attempts: [...] } }` where attempts contain
|
||||
* provider, error, stage, elapsedMs, matchedSelectors. We extract and attach
|
||||
* attempts to the error so formatAggregatedError can surface them.
|
||||
*/
|
||||
function normalizeAndroidRejection(error: unknown): Error & { attempts?: ProviderAttempt[] } {
|
||||
const err = error as Error & { data?: { attempts?: unknown[] } };
|
||||
const raw = err?.data?.attempts;
|
||||
if (!Array.isArray(raw) || raw.length === 0) return err;
|
||||
|
||||
const attempts: ProviderAttempt[] = raw
|
||||
.map((a: unknown): ProviderAttempt | null => {
|
||||
const item = a as RawAttempt;
|
||||
const provider = item?.provider;
|
||||
if (typeof provider !== 'string' || !VALID_PROVIDERS.includes(provider as ProviderId)) return null;
|
||||
const ms = typeof item?.elapsedMs === 'number' ? item.elapsedMs : undefined;
|
||||
let sel: string[] | undefined;
|
||||
if (Array.isArray(item?.matchedSelectors)) {
|
||||
sel = (item.matchedSelectors as unknown[]).filter((s): s is string => typeof s === 'string');
|
||||
} else if (typeof item?.matchedSelectors === 'string' && item.matchedSelectors.trim()) {
|
||||
sel = item.matchedSelectors
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
const attempt: ProviderAttempt = {
|
||||
provider: provider as ProviderId,
|
||||
success: Boolean(item?.success),
|
||||
error: typeof item?.error === 'string' ? item.error : undefined,
|
||||
stage: typeof item?.stage === 'string' && item.stage ? (item.stage as ProviderAttempt['stage']) : undefined,
|
||||
elapsedMs: ms,
|
||||
matchedSelectors: sel?.length ? sel : undefined,
|
||||
};
|
||||
return attempt;
|
||||
})
|
||||
.filter((a): a is ProviderAttempt => a !== null);
|
||||
|
||||
if (attempts.length > 0) {
|
||||
(err as Error & { attempts?: ProviderAttempt[] }).attempts = attempts;
|
||||
}
|
||||
return err as Error & { attempts?: ProviderAttempt[] };
|
||||
}
|
||||
|
||||
function isElectronRuntime(): boolean {
|
||||
return window.electronApi?.isElectron === true || window.isElectron === true;
|
||||
}
|
||||
@@ -122,7 +179,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage === FILE_SELECTION_CANCELLED_ERROR) return;
|
||||
|
||||
const err = error as Error & { attempts?: ProviderAttempt[] };
|
||||
const err = normalizeAndroidRejection(error) as Error & { attempts?: ProviderAttempt[] };
|
||||
if (err.attempts && err.attempts.length > 0) {
|
||||
window.alert(formatAggregatedError(err.attempts, t));
|
||||
} else if (uploadMode === 'preferred') {
|
||||
|
||||
Reference in New Issue
Block a user