/** * 测试计划: * 1. 系统文件选择器返回不符合 accept 的文件时终止上传链路。 * 2. 类型校验通过后继续执行 beforeUpload、列表变更和实际上传。 * 3. accept 的具体图片类型同步传给系统文件选择器。 * 4. accept 的具体图片类型同步传给系统相册选择器。 * 5. 未配置 accept 时保留原 uploadFileType 选择器逻辑。 * 6. 文件选择器按 Android/Harmony/iOS 的原生类型契约传参。 * 7. iOS 相册不传 Android-only 的 restrictMimeTypes。 * 8. accept 有值时覆盖 uploadFileType 默认限制,放行被 uploadFileType 拒绝的文件。 */ import React, { createRef } from 'react'; import { act, render, waitFor } from '@testing-library/react-native'; import { Platform } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; import { launchImageLibrary } from 'react-native-image-picker'; import UploaderBody from '../UploaderBody'; import { PhoneOS, UploadFileTypes, UploadWays } from '../enum'; import type { UploaderRef } from '../interface'; import { PortalContext } from '../../../core/portal/portal-host'; jest.mock('../PreviewImageModal', () => ({ PreviewImageModal: () => null, })); jest.mock('../permission', () => ({ requestPermission: jest.fn(() => Promise.resolve({ result: true })), saveLeaveCacheTime: jest.fn(() => Promise.resolve()), })); jest.mock('../hooks/useContentStyle', () => ({ useContentStyle: () => ({}), })); jest.mock('../../ActionSheet', () => jest.fn(() => Promise.resolve())); jest.mock('../../Space', () => () => null); jest.mock('../../ShouldRender', () => () => null); jest.mock('../../Toast', () => jest.fn()); jest.mock('../ImagePreview', () => () => null); jest.mock('../TextList', () => () => null); jest.mock('../UploadButton', () => () => null); jest.mock('react-native-image-picker', () => ({ launchCamera: jest.fn(), launchImageLibrary: jest.fn(), })); const mockedPick = DocumentPicker.pick as jest.MockedFunction< typeof DocumentPicker.pick >; const mockedLaunchImageLibrary = launchImageLibrary as jest.MockedFunction< typeof launchImageLibrary >; const originalPlatformOS = Platform.OS; const originalDocumentPickerImageType = DocumentPicker.types.images; const setPlatformOS = (platform: typeof Platform.OS) => { Object.defineProperty(Platform, 'OS', { configurable: true, value: platform, }); }; const setDocumentPickerImageType = (type: string) => { Object.defineProperty(DocumentPicker.types, 'images', { configurable: true, value: type, }); }; const portalMethods = { mount: jest.fn(() => 1), update: jest.fn(), unmount: jest.fn(), }; const renderUploaderBody = (element: React.ReactElement) => render( {element} ); describe('UploaderBody 文件类型校验', () => { beforeEach(() => { jest.clearAllMocks(); }); afterEach(() => { setPlatformOS(originalPlatformOS); setDocumentPickerImageType(originalDocumentPickerImageType); }); test('文件不符合 accept 时不进入后续上传链路', async () => { mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.gif', name: 'photo.gif', type: 'image/gif', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); const beforeUpload = jest.fn(() => true); const onChange = jest.fn(); const doUpload = jest.fn(() => Promise.resolve()); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); expect(beforeUpload).not.toHaveBeenCalled(); expect(onChange).not.toHaveBeenCalled(); expect(doUpload).not.toHaveBeenCalled(); }); test('文件符合 accept 时继续执行上传链路', async () => { mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.jpeg', name: 'photo.jpeg', type: 'image/jpeg', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); const beforeUpload = jest.fn(() => true); const onChange = jest.fn(); const doUpload = jest.fn(() => Promise.resolve()); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); await waitFor(() => { expect(beforeUpload).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalled(); expect(doUpload).toHaveBeenCalledTimes(1); }); }); test('accept 覆盖默认 uploadFileType 限制,放行被 uploadFileType 拒绝的文件', async () => { mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/sheet.xls', name: 'sheet.xls', type: 'application/vnd.ms-excel', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); const beforeUpload = jest.fn(() => true); const onChange = jest.fn(); const doUpload = jest.fn(() => Promise.resolve()); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); await waitFor(() => { expect(beforeUpload).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalled(); expect(doUpload).toHaveBeenCalledTimes(1); }); }); test('文件选择器使用 accept 对应的具体 MIME 类型', async () => { setPlatformOS(PhoneOS.Android); mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.jpeg', name: 'photo.jpeg', type: 'image/jpeg', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); expect(mockedPick).toHaveBeenCalledWith( expect.objectContaining({ type: ['image/jpeg', 'image/png'], }) ); }); test('相册选择器使用 accept 对应的具体 MIME 类型', async () => { setPlatformOS(PhoneOS.Android); mockedLaunchImageLibrary.mockResolvedValueOnce({ assets: [] }); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.Image); }); expect(mockedLaunchImageLibrary).toHaveBeenCalledWith( expect.objectContaining({ restrictMimeTypes: ['image/jpeg', 'image/png'], }) ); }); test('iOS 文件选择器沿用原 UTI 大类并由选择后校验具体类型', async () => { setPlatformOS(PhoneOS.Ios); setDocumentPickerImageType('public.image'); mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.jpeg', name: 'photo.jpeg', type: 'image/jpeg', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); expect(mockedPick).toHaveBeenCalledWith( expect.objectContaining({ type: ['public.image'] }) ); }); test('Harmony 文件选择器使用 accept 对应的扩展名', async () => { setPlatformOS(PhoneOS.Harmony as typeof Platform.OS); mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.jpeg', name: 'photo.jpeg', type: 'image/jpeg', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); expect(mockedPick).toHaveBeenCalledWith( expect.objectContaining({ type: ['.jpg', '.jpeg', '.png'] }) ); }); test('iOS 相册不传 Android-only MIME 限制', async () => { setPlatformOS(PhoneOS.Ios); mockedLaunchImageLibrary.mockResolvedValueOnce({ assets: [] }); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.Image); }); expect(mockedLaunchImageLibrary).toHaveBeenCalledWith({ mediaType: 'photo', quality: 1, selectionLimit: 1, }); }); test('未配置 accept 时文件选择器沿用 uploadFileType', async () => { mockedPick.mockResolvedValueOnce([ { uri: 'file:///tmp/photo.jpeg', name: 'photo.jpeg', type: 'image/jpeg', size: 1024, fileCopyUri: null, }, ]); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.File); }); expect(mockedPick).toHaveBeenCalledWith( expect.objectContaining({ type: ['image/*'] }) ); }); test('未配置 accept 时相册选择器不增加 MIME 限制', async () => { mockedLaunchImageLibrary.mockResolvedValueOnce({ assets: [] }); const ref = createRef(); renderUploaderBody( ); await act(async () => { await ref.current?.choose(UploadWays.Image); }); expect(mockedLaunchImageLibrary).toHaveBeenCalledWith({ mediaType: 'photo', quality: 1, selectionLimit: 1, }); }); });