feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useParams } from 'react-router-dom';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { messageReportEvent } from '@coze-arch/bot-utils';
import { useMessageReportEvent } from '../../src/bot/use-message-report-event';
// Mock dependencies
vi.mock('@coze-arch/bot-utils', () => ({
messageReportEvent: {
start: vi.fn(),
interrupt: vi.fn(),
},
}));
vi.mock('react-router-dom', () => ({
useParams: vi.fn(),
}));
describe('useMessageReportEvent', () => {
const mockBotId = 'test-bot-id';
beforeEach(() => {
vi.clearAllMocks();
(useParams as any).mockReturnValue({ bot_id: mockBotId });
});
it('should start message report event with correct parameters', () => {
renderHook(() => useMessageReportEvent());
expect(messageReportEvent.start).toHaveBeenCalledWith(mockBotId);
});
it('should not start message report event when bot_id is not available', () => {
(useParams as any).mockReturnValue({ bot_id: undefined });
renderHook(() => useMessageReportEvent());
expect(messageReportEvent.start).not.toHaveBeenCalled();
});
it('should interrupt message report event on unmount', () => {
const { unmount } = renderHook(() => useMessageReportEvent());
unmount();
expect(messageReportEvent.interrupt).toHaveBeenCalled();
});
it('should restart message report event when bot_id changes', () => {
const { rerender } = renderHook(() => useMessageReportEvent());
expect(messageReportEvent.start).toHaveBeenCalledTimes(1);
expect(messageReportEvent.start).toHaveBeenCalledWith(mockBotId);
const newBotId = 'new-bot-id';
(useParams as any).mockReturnValue({ bot_id: newBotId });
rerender();
expect(messageReportEvent.start).toHaveBeenCalledTimes(2);
expect(messageReportEvent.start).toHaveBeenCalledWith(newBotId);
});
});

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import {
userStoreService,
type UserInfo,
type UserLabel,
} from '@coze-studio/user-store';
import { useUserSenderInfo } from '../../src/bot/use-user-sender-info';
// Mock dependencies
vi.mock('@coze-studio/user-store', () => ({
userStoreService: {
useUserLabel: vi.fn(),
useUserInfo: vi.fn(),
},
}));
describe('useUserSenderInfo', () => {
const mockUserLabel = {
id: 'label-1',
name: 'Test Label',
} as unknown as UserLabel;
const mockUserInfo: Partial<UserInfo> = {
avatar_url: 'https://example.com/avatar.jpg',
name: 'Test User',
user_id_str: '12345',
app_user_info: {
user_unique_name: 'test_user',
},
};
it('should return null when userInfo is not available', () => {
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
vi.mocked(userStoreService.useUserInfo).mockReturnValue(null);
const { result } = renderHook(() => useUserSenderInfo());
expect(result.current).toBeNull();
});
it('should return formatted user sender info when userInfo is available', () => {
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
vi.mocked(userStoreService.useUserInfo).mockReturnValue(
mockUserInfo as UserInfo,
);
const { result } = renderHook(() => useUserSenderInfo());
expect(result.current).toEqual({
url: mockUserInfo.avatar_url,
nickname: mockUserInfo.name,
id: mockUserInfo.user_id_str,
userUniqueName: mockUserInfo.app_user_info?.user_unique_name,
userLabel: mockUserLabel,
});
});
it('should handle missing optional fields', () => {
const partialUserInfo: Partial<UserInfo> = {
user_id_str: '12345',
app_user_info: {},
};
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
vi.mocked(userStoreService.useUserInfo).mockReturnValue(
partialUserInfo as UserInfo,
);
const { result } = renderHook(() => useUserSenderInfo());
expect(result.current).toEqual({
url: '',
nickname: '',
id: partialUserInfo.user_id_str,
userUniqueName: '',
userLabel: mockUserLabel,
});
});
});

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type PropsWithChildren } from 'react';
import { describe, it, expect } from 'vitest';
import { renderHook } from '@testing-library/react';
import {
useLayoutContext,
LayoutContext,
PlacementEnum,
} from '../src/editor-layout';
describe('editor-layout', () => {
const wrapper = ({
children,
placement = PlacementEnum.CENTER,
}: PropsWithChildren<{ placement?: PlacementEnum }>) => (
<LayoutContext value={{ placement }}>{children}</LayoutContext>
);
it('should use default center placement', () => {
const { result } = renderHook(() => useLayoutContext());
expect(result.current.placement).toBe(PlacementEnum.CENTER);
});
it('should use provided placement', () => {
const { result } = renderHook(() => useLayoutContext(), {
wrapper: ({ children }) =>
wrapper({ children, placement: PlacementEnum.LEFT }),
});
expect(result.current.placement).toBe(PlacementEnum.LEFT);
});
it('should use right placement', () => {
const { result } = renderHook(() => useLayoutContext(), {
wrapper: ({ children }) =>
wrapper({ children, placement: PlacementEnum.RIGHT }),
});
expect(result.current.placement).toBe(PlacementEnum.RIGHT);
});
});

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useLocation } from 'react-router-dom';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import {
useResetLocationState,
resetAuthLoginDataFromRoute,
} from '../../src/router/use-reset-location-state';
// Mock dependencies
vi.mock('react-router-dom', () => ({
useLocation: vi.fn(),
}));
describe('use-reset-location-state', () => {
const mockLocation = {
state: { someState: 'test' },
key: 'default',
pathname: '/',
search: '',
hash: '',
};
const mockReplaceState = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
// Mock useLocation
vi.mocked(useLocation).mockReturnValue(mockLocation);
// Mock window.history
Object.defineProperty(window, 'history', {
value: {
replaceState: mockReplaceState,
},
writable: true,
});
});
describe('resetAuthLoginDataFromRoute', () => {
it('should call history.replaceState with empty state', () => {
resetAuthLoginDataFromRoute();
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
});
});
describe('useResetLocationState', () => {
it('should clear location state and auth login data', () => {
const { result } = renderHook(() => useResetLocationState());
// Call the reset function
result.current();
// Verify location state is cleared
expect(mockLocation.state).toEqual({});
// Verify history state is cleared
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
});
it('should handle undefined location state', () => {
const mockLocationWithoutState = {} as any;
vi.mocked(useLocation).mockReturnValue(mockLocationWithoutState);
const { result } = renderHook(() => useResetLocationState());
// Call the reset function
result.current();
// Verify location state is set to empty object
expect(mockLocationWithoutState.state).toEqual({});
// Verify history state is cleared
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
});
it('should preserve location reference while clearing state', () => {
const { result } = renderHook(() => useResetLocationState());
const originalLocation = mockLocation;
// Call the reset function
result.current();
// Verify location reference is preserved
expect(mockLocation).toBe(originalLocation);
// But state is cleared
expect(mockLocation.state).toEqual({});
});
});
});

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useComponentState } from '../src/use-component-state';
describe('useComponentState', () => {
it('should initialize with the provided state', () => {
const initialState = { count: 0, text: 'hello' };
const { result } = renderHook(() => useComponentState(initialState));
expect(result.current.state).toEqual(initialState);
});
it('should perform incremental updates by default', () => {
const initialState = { count: 0, text: 'hello' };
const { result } = renderHook(() => useComponentState(initialState));
act(() => {
result.current.setState({ count: 1 });
});
expect(result.current.state).toEqual({ count: 1, text: 'hello' });
});
it('should replace entire state when replace flag is true', () => {
const initialState = { count: 0, text: 'hello', extra: true };
const { result } = renderHook(() => useComponentState(initialState));
act(() => {
result.current.setState({ count: 1, text: 'hello', extra: true }, true);
});
expect(result.current.state).toEqual({
count: 1,
text: 'hello',
extra: true,
});
});
it('should reset state to initial value', () => {
const initialState = { count: 0, text: 'hello' };
const { result } = renderHook(() => useComponentState(initialState));
act(() => {
result.current.setState({ count: 1, text: 'world' });
});
expect(result.current.state).toEqual({ count: 1, text: 'world' });
act(() => {
result.current.resetState();
});
expect(result.current.state).toEqual(initialState);
});
it('should handle multiple updates', () => {
const initialState = { count: 0, text: 'hello' };
const { result } = renderHook(() => useComponentState(initialState));
act(() => {
result.current.setState({ count: 1 });
result.current.setState({ text: 'world' });
});
expect(result.current.state).toEqual({ count: 1, text: 'world' });
});
});

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
// Mock dependencies
vi.mock('@coze-studio/bot-detail-store', () => ({
useBotDetailIsReadonly: vi.fn(),
}));
vi.mock('@coze-studio/bot-detail-store/page-runtime', () => ({
usePageRuntimeStore: vi.fn(),
}));
vi.mock('@coze-arch/bot-utils', () => ({
skillKeyToApiStatusKeyTransformer: vi.fn(),
}));
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { skillKeyToApiStatusKeyTransformer } from '@coze-arch/bot-utils';
import { TabStatus } from '@coze-arch/bot-api/developer_api';
import { useDefaultExPandCheck } from '../src/use-default-expand-check';
describe('useDefaultExPandCheck', () => {
beforeEach(() => {
vi.clearAllMocks();
(useBotDetailIsReadonly as any).mockReturnValue(false);
(skillKeyToApiStatusKeyTransformer as any).mockReturnValue(
'transformedKey',
);
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: true,
botSkillBlockCollapsibleState: {},
});
});
it('should return undefined when when is false', () => {
const { result } = renderHook(() =>
useDefaultExPandCheck(
{
blockKey: 'test' as any,
configured: true,
},
false,
),
);
expect(result.current).toBeUndefined();
});
it('should return undefined when init is false', () => {
(usePageRuntimeStore as any).mockReturnValue({
init: false,
editable: true,
botSkillBlockCollapsibleState: {},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: true,
}),
);
expect(result.current).toBeUndefined();
});
it('should return undefined when botSkillBlockCollapsibleState is empty', () => {
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: true,
botSkillBlockCollapsibleState: {},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: true,
}),
);
expect(result.current).toBeUndefined();
});
it('should return true when state is Open', () => {
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: true,
botSkillBlockCollapsibleState: {
transformedKey: TabStatus.Open,
},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: true,
}),
);
expect(result.current).toBe(true);
});
it('should return false when state is Close', () => {
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: true,
botSkillBlockCollapsibleState: {
transformedKey: TabStatus.Close,
},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: true,
}),
);
expect(result.current).toBe(false);
});
it('should return configured value when readonly', () => {
(useBotDetailIsReadonly as any).mockReturnValue(true);
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: true,
botSkillBlockCollapsibleState: {
transformedKey: TabStatus.Open,
},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: true,
}),
);
expect(result.current).toBe(true);
});
it('should return configured value when not editable', () => {
(usePageRuntimeStore as any).mockReturnValue({
init: true,
editable: false,
botSkillBlockCollapsibleState: {
transformedKey: TabStatus.Open,
},
});
const { result } = renderHook(() =>
useDefaultExPandCheck({
blockKey: 'test' as any,
configured: false,
}),
);
expect(result.current).toBe(false);
});
});

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import {
getFileListByDragOrPaste,
formatTypeFileListToTypeArray,
} from '../../../src/use-drag-and-paste-upload/helper/get-file-list-by-drag';
describe('getFileListByDragOrPaste', () => {
it('should handle drag event with files', () => {
const file1 = new File(['content1'], 'file1.txt');
const file2 = new File(['content2'], 'file2.txt');
const fileList = {
0: file1,
1: file2,
length: 2,
item: (index: number) => (index === 0 ? file1 : file2),
};
const dragEvent = {
dataTransfer: {
files: fileList,
},
} as unknown as DragEvent;
const result = getFileListByDragOrPaste(dragEvent);
expect(result).toHaveLength(2);
expect(result[0]).toBe(file1);
expect(result[1]).toBe(file2);
});
it('should handle paste event with files', () => {
const file1 = new File(['content1'], 'file1.txt');
const fileList = {
0: file1,
length: 1,
item: (index: number) => (index === 0 ? file1 : null),
};
const pasteEvent = {
clipboardData: {
files: fileList,
},
} as unknown as ClipboardEvent;
const result = getFileListByDragOrPaste(pasteEvent);
expect(result).toHaveLength(1);
expect(result[0]).toBe(file1);
});
it('should return empty array when no files are present', () => {
const dragEvent = {
dataTransfer: {
files: undefined,
},
} as unknown as DragEvent;
const result = getFileListByDragOrPaste(dragEvent);
expect(result).toHaveLength(0);
});
});
describe('formatTypeFileListToTypeArray', () => {
it('should convert FileList to array of Files', () => {
const file1 = new File(['content1'], 'file1.txt');
const file2 = new File(['content2'], 'file2.txt');
const fileList = {
0: file1,
1: file2,
length: 2,
item: (index: number) => (index === 0 ? file1 : file2),
};
const result = formatTypeFileListToTypeArray(
fileList as unknown as FileList,
);
expect(result).toHaveLength(2);
expect(result[0]).toBe(file1);
expect(result[1]).toBe(file2);
});
it('should filter out null items', () => {
const file1 = new File(['content1'], 'file1.txt');
const fileList = {
0: file1,
1: null,
length: 2,
item: (index: number) => (index === 0 ? file1 : null),
};
const result = formatTypeFileListToTypeArray(
fileList as unknown as FileList,
);
expect(result).toHaveLength(1);
expect(result[0]).toBe(file1);
});
it('should handle empty FileList', () => {
const fileList = {
length: 0,
item: () => null,
};
const result = formatTypeFileListToTypeArray(
fileList as unknown as FileList,
);
expect(result).toHaveLength(0);
});
});

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { isHasFileByDrag } from '../../../src/use-drag-and-paste-upload/helper/is-has-file-by-drag';
describe('isHasFileByDrag', () => {
it('should return true when Files type is present', () => {
const dragEvent = {
dataTransfer: {
types: ['Files', 'text/plain'],
},
} as unknown as DragEvent;
expect(isHasFileByDrag(dragEvent)).toBe(true);
});
it('should return false when Files type is not present', () => {
const dragEvent = {
dataTransfer: {
types: ['text/plain', 'text/html'],
},
} as unknown as DragEvent;
expect(isHasFileByDrag(dragEvent)).toBe(false);
});
it('should return false when dataTransfer is null', () => {
const dragEvent = {
dataTransfer: null,
} as unknown as DragEvent;
expect(isHasFileByDrag(dragEvent)).toBe(false);
});
it('should return false when types is undefined', () => {
const dragEvent = {
dataTransfer: {
types: [],
},
} as unknown as DragEvent;
expect(isHasFileByDrag(dragEvent)).toBe(false);
});
});

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { Toast } from '@coze-arch/bot-semi';
import { useDragAndPasteUpload } from '../../src/use-drag-and-paste-upload';
// Mock dependencies
vi.mock('@coze-arch/bot-semi', () => ({
Toast: {
warning: vi.fn(),
},
}));
describe('useDragAndPasteUpload', () => {
const mockRef = {
current: document.createElement('div'),
};
const mockProps = {
ref: mockRef,
onUpload: vi.fn(),
disableDrag: false,
disablePaste: false,
fileLimit: 5,
maxFileSize: 1024 * 1024, // 1MB
isFileFormatValid: (file: File) => file.type.startsWith('image/'),
getExistingFileCount: () => 0,
closeDelay: 100,
invalidFormatMessage: 'Invalid format',
invalidSizeMessage: 'File too large',
fileExceedsMessage: 'Too many files',
};
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should handle drag events correctly', () => {
const { result } = renderHook(() => useDragAndPasteUpload(mockProps));
// Initial state
expect(result.current.isDragOver).toBe(false);
// Simulate dragover
act(() => {
const dragOverEvent = new Event('dragover') as DragEvent;
Object.defineProperty(dragOverEvent, 'dataTransfer', {
value: {
types: ['Files'],
},
});
mockRef.current.dispatchEvent(dragOverEvent);
});
expect(result.current.isDragOver).toBe(true);
// Simulate dragleave
act(() => {
const dragLeaveEvent = new Event('dragleave') as DragEvent;
mockRef.current.dispatchEvent(dragLeaveEvent);
});
// Wait for closeDelay
act(() => {
vi.advanceTimersByTime(mockProps.closeDelay);
});
expect(result.current.isDragOver).toBe(false);
});
it('should handle file drop correctly', () => {
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
renderHook(() => useDragAndPasteUpload(mockProps));
// Simulate drop
act(() => {
const dropEvent = new Event('drop') as DragEvent;
Object.defineProperty(dropEvent, 'dataTransfer', {
value: {
types: ['Files'],
files: {
0: validFile,
length: 1,
item: (index: number) => (index === 0 ? validFile : null),
},
},
});
mockRef.current.dispatchEvent(dropEvent);
});
expect(mockProps.onUpload).toHaveBeenCalledWith([validFile]);
});
it('should handle paste events correctly', () => {
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
renderHook(() => useDragAndPasteUpload(mockProps));
// Simulate paste
act(() => {
const pasteEvent = new Event('paste') as ClipboardEvent;
Object.defineProperty(pasteEvent, 'clipboardData', {
value: {
files: {
0: validFile,
length: 1,
item: (index: number) => (index === 0 ? validFile : null),
},
},
});
mockRef.current.dispatchEvent(pasteEvent);
});
expect(mockProps.onUpload).toHaveBeenCalledWith([validFile]);
});
it('should validate file format', () => {
const invalidFile = new File(['content'], 'test.txt', {
type: 'text/plain',
});
renderHook(() => useDragAndPasteUpload(mockProps));
// Simulate drop with invalid file
act(() => {
const dropEvent = new Event('drop') as DragEvent;
Object.defineProperty(dropEvent, 'dataTransfer', {
value: {
types: ['Files'],
files: {
0: invalidFile,
length: 1,
item: (index: number) => (index === 0 ? invalidFile : null),
},
},
});
mockRef.current.dispatchEvent(dropEvent);
});
expect(Toast.warning).toHaveBeenCalledWith({
content: mockProps.invalidFormatMessage,
showClose: false,
});
expect(mockProps.onUpload).not.toHaveBeenCalled();
});
it('should validate file size', () => {
const largeFile = new File(['content'.repeat(1000000)], 'large.jpg', {
type: 'image/jpeg',
});
renderHook(() => useDragAndPasteUpload(mockProps));
// Simulate drop with large file
act(() => {
const dropEvent = new Event('drop') as DragEvent;
Object.defineProperty(dropEvent, 'dataTransfer', {
value: {
types: ['Files'],
files: {
0: largeFile,
length: 1,
item: (index: number) => (index === 0 ? largeFile : null),
},
},
});
mockRef.current.dispatchEvent(dropEvent);
});
expect(Toast.warning).toHaveBeenCalledWith({
content: mockProps.invalidSizeMessage,
showClose: false,
});
expect(mockProps.onUpload).not.toHaveBeenCalled();
});
it('should validate file count', () => {
const mockPropsWithExistingFiles = {
...mockProps,
getExistingFileCount: () => 4,
};
const validFiles = [
new File(['content1'], 'test1.jpg', { type: 'image/jpeg' }),
new File(['content2'], 'test2.jpg', { type: 'image/jpeg' }),
];
renderHook(() => useDragAndPasteUpload(mockPropsWithExistingFiles));
// Simulate drop with too many files
act(() => {
const dropEvent = new Event('drop') as DragEvent;
Object.defineProperty(dropEvent, 'dataTransfer', {
value: {
types: ['Files'],
files: {
0: validFiles[0],
1: validFiles[1],
length: 2,
item: (index: number) => validFiles[index] || null,
},
},
});
mockRef.current.dispatchEvent(dropEvent);
});
expect(Toast.warning).toHaveBeenCalledWith({
content: mockProps.fileExceedsMessage,
showClose: false,
});
expect(mockProps.onUpload).not.toHaveBeenCalled();
});
it('should respect disableDrag prop', () => {
const { result } = renderHook(() =>
useDragAndPasteUpload({ ...mockProps, disableDrag: true }),
);
// Simulate dragover
act(() => {
const dragOverEvent = new Event('dragover') as DragEvent;
Object.defineProperty(dragOverEvent, 'dataTransfer', {
value: {
types: ['Files'],
},
});
mockRef.current.dispatchEvent(dragOverEvent);
});
expect(result.current.isDragOver).toBe(false);
});
it('should respect disablePaste prop', () => {
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
renderHook(() =>
useDragAndPasteUpload({ ...mockProps, disablePaste: true }),
);
// Simulate paste
act(() => {
const pasteEvent = new Event('paste') as ClipboardEvent;
Object.defineProperty(pasteEvent, 'clipboardData', {
value: {
files: {
0: validFile,
length: 1,
item: (index: number) => (index === 0 ? validFile : null),
},
},
});
mockRef.current.dispatchEvent(pasteEvent);
});
expect(mockProps.onUpload).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useDragAndPasteUpload } from '../src/use-drag-and-paste-upload';
describe('useDragAndPasteUpload', () => {
it('return correctly', () => {
const ref = { current: null };
const {
result: { current },
} = renderHook(() =>
useDragAndPasteUpload({
ref,
disableDrag: false,
disablePaste: false,
onUpload: () => 0,
fileLimit: 3,
isFileFormatValid: () => true,
maxFileSize: 10 * 1024 * 1024,
closeDelay: undefined,
invalidFormatMessage: '不支持的文件类型',
invalidSizeMessage: '不支持文件大小超过 10MB',
fileExceedsMessage: '最多上传 3 个文件',
getExistingFileCount: () => 0,
}),
);
expect(current).toMatchObject({
isDragOver: false,
});
});
});

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useExposure } from '../src/use-exposure';
// Mock dependencies
vi.mock('ahooks', () => ({
useInViewport: vi.fn(),
}));
vi.mock('@coze-arch/bot-tea', () => ({
sendTeaEvent: vi.fn(),
EVENT_NAMES: {
page_view: 'page_view',
},
}));
import { useInViewport } from 'ahooks';
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
describe('useExposure', () => {
const mockTarget = { current: document.createElement('div') };
const mockEventName = EVENT_NAMES.page_view;
const mockReportParams = { key: 'value' };
beforeEach(() => {
vi.clearAllMocks();
});
it('should report when element is in view and needReport is true', () => {
(useInViewport as any).mockReturnValue([true]);
renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
}),
);
expect(sendTeaEvent).toHaveBeenCalledWith(mockEventName, mockReportParams);
});
it('should not report when element is not in view', () => {
(useInViewport as any).mockReturnValue([false]);
renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
}),
);
expect(sendTeaEvent).not.toHaveBeenCalled();
});
it('should not report when needReport is false', () => {
(useInViewport as any).mockReturnValue([true]);
renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
needReport: false,
}),
);
expect(sendTeaEvent).not.toHaveBeenCalled();
});
it('should report only once when isReportOnce is true', () => {
(useInViewport as any).mockReturnValue([true]);
const { rerender } = renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
isReportOnce: true,
}),
);
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
// Rerender should not trigger another report
rerender();
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
});
it('should report multiple times when isReportOnce is false', () => {
(useInViewport as any).mockReturnValue([true]);
const { rerender } = renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
isReportOnce: false,
}),
);
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
// Rerender should not trigger another report
rerender();
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
});
it('should pass options to useInViewport', () => {
const mockOptions = { threshold: 0.5 };
(useInViewport as any).mockReturnValue([true]);
renderHook(() =>
useExposure({
target: mockTarget,
eventName: mockEventName,
reportParams: mockReportParams,
options: mockOptions,
}),
);
expect(useInViewport).toHaveBeenCalledWith(mockTarget, mockOptions);
});
});

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useInitialValue } from '../src/use-initial-value';
describe('useInitialValue', () => {
it('should return the initial value', () => {
const initialValue = 'test';
const { result } = renderHook(() => useInitialValue(initialValue));
expect(result.current).toBe(initialValue);
});
it('should maintain the initial value even if input changes', () => {
let value = 'initial';
const { result, rerender } = renderHook(() => useInitialValue(value));
expect(result.current).toBe('initial');
value = 'changed';
rerender();
expect(result.current).toBe('initial');
});
it('should work with different types', () => {
const numberValue = 42;
const { result: numberResult } = renderHook(() =>
useInitialValue(numberValue),
);
expect(numberResult.current).toBe(42);
const objectValue = { key: 'value' };
const { result: objectResult } = renderHook(() =>
useInitialValue(objectValue),
);
expect(objectResult.current).toEqual({ key: 'value' });
const arrayValue = [1, 2, 3];
const { result: arrayResult } = renderHook(() =>
useInitialValue(arrayValue),
);
expect(arrayResult.current).toEqual([1, 2, 3]);
});
});

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useLineClamp } from '../src/use-line-clamp';
describe('useLineClamp', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return contentRef and isClamped', () => {
const { result } = renderHook(() => useLineClamp());
expect(result.current.contentRef).toBeDefined();
expect(result.current.isClamped).toBe(false);
});
it('should add and remove resize event listener', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const { unmount } = renderHook(() => useLineClamp());
expect(addEventListenerSpy).toHaveBeenCalledWith(
'resize',
expect.any(Function),
);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'resize',
expect.any(Function),
);
});
it('should update isClamped when content height changes', () => {
const mockDiv = document.createElement('div');
Object.defineProperties(mockDiv, {
scrollHeight: {
configurable: true,
get: () => 100,
},
clientHeight: {
configurable: true,
get: () => 50,
},
});
const { result } = renderHook(() => useLineClamp());
// 使用 vi.spyOn 来模拟 contentRef.current
vi.spyOn(result.current.contentRef, 'current', 'get').mockReturnValue(
mockDiv,
);
// 使用 act 包装异步操作
act(() => {
window.dispatchEvent(new Event('resize'));
});
expect(result.current.isClamped).toBe(true);
});
it('should handle null contentRef', () => {
const { result } = renderHook(() => useLineClamp());
// 使用 vi.spyOn 来模拟 contentRef.current 为 null
vi.spyOn(result.current.contentRef, 'current', 'get').mockReturnValue(null);
// 使用 act 包装异步操作
act(() => {
window.dispatchEvent(new Event('resize'));
});
expect(result.current.isClamped).toBe(false);
});
});

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useLoggedIn } from '../src/use-loggedin';
// Mock userStoreService
vi.mock('@coze-studio/user-store', () => ({
userStoreService: {
useIsLogined: vi.fn(),
},
}));
import { userStoreService } from '@coze-studio/user-store';
describe('useLoggedIn', () => {
it('should return true when user is logged in', () => {
(userStoreService.useIsLogined as any).mockReturnValue(true);
const { result } = renderHook(() => useLoggedIn());
expect(result.current).toBe(true);
});
it('should return false when user is not logged in', () => {
(userStoreService.useIsLogined as any).mockReturnValue(false);
const { result } = renderHook(() => useLoggedIn());
expect(result.current).toBe(false);
});
it('should call userStoreService.useIsLogined', () => {
renderHook(() => useLoggedIn());
expect(userStoreService.useIsLogined).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { ScreenRange, useMediaQuery } from '@coze-arch/responsive-kit';
import { useIsResponsiveByRouteConfig } from '../src/use-responsive';
// Mock dependencies
vi.mock('@coze-arch/responsive-kit', () => ({
useMediaQuery: vi.fn(),
ScreenRange: {
LG: 'lg',
MD: 'md',
},
}));
vi.mock('react-router-dom', () => ({
useLocation: vi.fn(),
}));
vi.mock('../src/use-route-config', () => ({
useRouteConfig: vi.fn(),
}));
import { useRouteConfig } from '../src/use-route-config';
describe('useIsResponsiveByRouteConfig', () => {
it('should handle responsive=true case', () => {
(useRouteConfig as any).mockReturnValue({ responsive: true });
(useMediaQuery as any).mockReturnValue(false);
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
expect(result.current).toBe(true);
});
it('should handle custom responsive config with include=true', () => {
(useRouteConfig as any).mockReturnValue({
responsive: {
rangeMax: ScreenRange.LG,
include: true,
},
});
(useMediaQuery as any).mockReturnValue(true);
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
expect(result.current).toBe(true);
});
it('should handle custom responsive config with include=false', () => {
(useRouteConfig as any).mockReturnValue({
responsive: {
rangeMax: ScreenRange.LG,
include: false,
},
});
(useMediaQuery as any).mockReturnValue(false);
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
expect(result.current).toBe(true);
});
it('should return false when responsive is undefined', () => {
(useRouteConfig as any).mockReturnValue({});
(useMediaQuery as any).mockReturnValue(true);
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
expect(result.current).toBe(false);
});
});

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, type Mock } from 'vitest';
import { renderHook } from '@testing-library/react';
import { ScreenRange } from '@coze-arch/responsive-kit';
import { useRouteConfig } from '../src/use-route-config';
// Mock useMatches hook
vi.mock('react-router-dom', () => ({
useMatches: vi.fn(),
}));
import { useMatches } from 'react-router-dom';
describe('useRouteConfig', () => {
it('should return default config when no matches', () => {
const mockUseMatches = useMatches as Mock;
mockUseMatches.mockReturnValue([]);
const defaults = {
hasSider: true,
showMobileTips: false,
};
const { result } = renderHook(() => useRouteConfig(defaults));
expect(result.current).toEqual(defaults);
});
it('should merge configs from matched routes', () => {
const mockUseMatches = useMatches as Mock;
mockUseMatches.mockReturnValue([
{
handle: {
hasSider: true,
showMobileTips: false,
},
},
{
handle: {
showMobileTips: true,
requireAuth: true,
},
},
]);
const defaults = {
hasSider: false,
showAssistant: true,
};
const { result } = renderHook(() => useRouteConfig(defaults));
expect(result.current).toEqual({
hasSider: true,
showMobileTips: true,
requireAuth: true,
showAssistant: true,
});
});
it('should handle responsive config', () => {
const mockUseMatches = useMatches as Mock;
mockUseMatches.mockReturnValue([
{
handle: {
responsive: {
rangeMax: ScreenRange.LG,
include: false,
},
},
},
]);
const { result } = renderHook(() => useRouteConfig());
expect(result.current).toEqual({
responsive: {
rangeMax: ScreenRange.LG,
include: false,
},
});
});
it('should handle empty defaults', () => {
const mockUseMatches = useMatches as Mock;
mockUseMatches.mockReturnValue([]);
const { result } = renderHook(() => useRouteConfig());
expect(result.current).toEqual({});
});
});