/**
* 测试计划:
* 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,
});
});
});