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,70 @@
/*
* 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 { ArrayUtil } from '../src/array';
describe('array', () => {
it('array2Map', () => {
const testItem1 = { id: '1', name: 'Alice', age: 20 };
const testItem2 = { id: '2', name: 'Bob', age: 25 };
const { array2Map } = ArrayUtil;
const array1 = [testItem1, testItem2];
const mapById = array2Map(array1, 'id');
expect(mapById).toEqual({
'1': testItem1,
'2': testItem2,
});
const mapByName = array2Map(array1, 'name', 'age');
expect(mapByName).toEqual({ Alice: 20, Bob: 25 });
const array = [testItem1, testItem2];
const mapByIdFunc = array2Map(
array,
'id',
item => `${item.name}-${item.age}`,
);
expect(mapByIdFunc).toEqual({ '1': 'Alice-20', '2': 'Bob-25' });
});
it('mapAndFilter', () => {
const { mapAndFilter } = ArrayUtil;
const array = [
{ id: 1, name: 'Alice', value: 100 },
{ id: 2, name: 'Bob', value: 200 },
];
// filter
const result1 = mapAndFilter(array, {
filter: item => item.name === 'Alice',
});
expect(result1).toEqual([{ id: 1, name: 'Alice', value: 100 }]);
// map
const result2 = mapAndFilter(array, {
map: item => ({ value: item.value }),
});
expect(result2).toEqual([{ value: 100 }, { value: 200 }]);
// filter & map
const result3 = mapAndFilter(array, {
filter: item => item.value > 100,
map: item => ({ id: item.id, name: item.name }),
});
expect(result3).toEqual([{ id: 2, name: 'Bob' }]);
});
});

View File

@@ -0,0 +1,114 @@
/*
* 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 dayjs from 'dayjs';
import { I18n } from '@coze-arch/i18n';
import {
getCurrentTZ,
getFormatDateType,
formatDate,
getRemainTime,
getTimestampByAdd,
formatTimestamp,
} from '../src/date';
vi.mock('@coze-arch/i18n');
vi.spyOn(I18n, 't');
describe('Date', () => {
it('#getFormatDateType', () => {
const now = dayjs();
expect(getFormatDateType(now.unix())).toEqual('HH:mm');
expect(
getFormatDateType(
dayjs(now)
.date(now.date() === 1 ? 2 : 1)
.unix(),
),
).toEqual('MM-DD HH:mm');
expect(getFormatDateType(dayjs(now).add(1, 'year').unix())).toEqual(
'YYYY-MM-DD HH:mm',
);
});
it('#dayjsForRegion Oversea should return UTC format', () => {
vi.stubGlobal('IS_OVERSEA', true);
expect(getCurrentTZ().isUTC()).toBe(true);
expect(getCurrentTZ().utcOffset()).toBe(0);
});
it('#dayjsForRegion China should return UTC+8 format', () => {
vi.stubGlobal('IS_OVERSEA', false);
expect(getCurrentTZ().isUTC()).toBe(false);
expect(getCurrentTZ().utcOffset()).toBe(60 * 8);
});
it('#formatDate', () => {
const date = formatDate(1718782764);
expect(date).toBe('2024/06/19 15:39:24');
});
it('#getRemainTime', () => {
vi.useFakeTimers();
const date = new Date('2024-08-19T15:30:00+08:00');
vi.setSystemTime(date);
expect(getRemainTime()).toBe('16h 30m');
vi.useRealTimers();
});
it('#dayjsAdd', () => {
vi.useFakeTimers();
const date = new Date('2024-08-19T15:30:00+08:00');
vi.setSystemTime(date);
const unix = getTimestampByAdd(1, 'd');
expect(unix).toBe(dayjs('2024-08-20T15:30:00+08:00').unix());
vi.useRealTimers();
});
});
describe('format timestamp', () => {
beforeEach(() => {
const MOCK_NOW = dayjs('2024-09-24 20:00:00');
vi.setSystemTime(MOCK_NOW);
});
it('just now', () => {
formatTimestamp(dayjs('2024-09-24 19:59:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_just_now');
});
it('n min', () => {
formatTimestamp(dayjs('2024-09-24 19:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_min', { n: 1 });
});
it('n hours', () => {
formatTimestamp(dayjs('2024-09-24 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_hour', { n: 2 });
});
it('n days', () => {
formatTimestamp(dayjs('2024-09-21 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_day', { n: 3 });
});
it('full date', () => {
formatTimestamp(dayjs('2024-07-21 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_date', {
yyyy: 2024,
mm: 7,
dd: 21,
});
});
});

View File

@@ -0,0 +1,45 @@
/*
* 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 { openNewWindow } from '../src/dom';
it('openNewWindow', async () => {
const testUrl = 'test_url';
const testOrigin = 'test_origin';
const newWindow = {
close: vi.fn(),
location: '',
};
vi.stubGlobal('window', {
open: vi.fn(() => newWindow),
});
vi.stubGlobal('location', {
origin: testOrigin,
});
const cb = vi.fn(() => Promise.resolve(testUrl));
const cbWithError = vi.fn(() => Promise.reject(new Error()));
await openNewWindow(cb);
expect(newWindow.close).not.toHaveBeenCalled();
expect(newWindow.location).equal(testUrl);
await openNewWindow(cbWithError);
expect(newWindow.close).toHaveBeenCalled();
expect(newWindow.location).equal(`${testOrigin}/404`);
vi.clearAllMocks();
});

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 { getReportError } from '../src/get-report-error';
describe('getReportError', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('common error', () => {
const error = new Error('123');
const result = getReportError(error, 'testError');
expect(result).toMatchObject({ error, meta: { reason: 'testError' } });
});
test('stringify error', () => {
const result = getReportError('123', 'testError');
expect(result).toMatchObject({
error: new Error('123'),
meta: { reason: 'testError' },
});
});
test('object error', () => {
const result = getReportError(
{ foo: 'bar', reason: 'i am fool' },
'testError',
);
expect(result).toMatchObject({
error: new Error(''),
meta: { reason: 'testError', reasonOfInputError: 'i am fool' },
});
});
});

View File

@@ -0,0 +1,30 @@
/*
* 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 ReactNode } from 'react';
import { renderHtmlTitle } from '../src/html';
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn(k => k) },
}));
describe('html', () => {
test('renderHtmlTitle', () => {
expect(renderHtmlTitle('test')).equal('test - platform_name');
expect(renderHtmlTitle({} as unknown as ReactNode)).equal('platform_name');
});
});

View File

@@ -0,0 +1,47 @@
/*
* 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 { loadImage } from '../src/image';
describe('image', () => {
test('loadImage with success', async () => {
vi.stubGlobal(
'Image',
class Image {
onload!: () => void;
set src(url: string) {
this.onload();
}
},
);
await expect(loadImage('test')).resolves.toBeUndefined();
vi.clearAllMocks();
});
test('loadImage with fail', async () => {
vi.stubGlobal(
'Image',
class Image {
onerror!: () => void;
set src(url: string) {
this.onerror();
}
},
);
await expect(loadImage('test')).rejects.toBeUndefined();
vi.clearAllMocks();
});
});

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
describe('Hello World', () => {
it('test', () => {
expect(1).toBe(1);
});
});

View File

@@ -0,0 +1,38 @@
/*
* 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 { isMobile } from '../src/is-mobile';
describe('is-mobile', () => {
const TARGET_WIDTH = 640;
test('isMobile with false', () => {
vi.stubGlobal('document', {
documentElement: {
clientWidth: TARGET_WIDTH + 10,
},
});
expect(isMobile()).toBeFalsy();
});
test('isMobile with true', () => {
vi.stubGlobal('document', {
documentElement: {
clientWidth: TARGET_WIDTH - 10,
},
});
expect(isMobile()).toBeTruthy();
});
});

View File

@@ -0,0 +1,155 @@
/*
* 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 { createReportEvent } from '@coze-arch/report-events';
import { messageReportEvent } from '../src/message-report';
const TEST_LOG_ID = 'test_log_id';
const TEST_BOT_ID = 'test_bot_id';
vi.mock('@coze-arch/web-context', () => ({
globalVars: {
LAST_EXECUTE_ID: 'test_log_id',
},
}));
const mockAddDurationPoint = vi.fn();
const mockSuccess = vi.fn();
const mockError = vi.fn();
vi.mock('@coze-arch/report-events', async () => {
const actual: Record<string, unknown> = await vi.importActual(
'@coze-arch/report-events',
);
return {
...actual,
createReportEvent: vi.fn(() => ({
addDurationPoint: mockAddDurationPoint,
success: mockSuccess,
error: mockError,
})),
};
});
vi.mock('@coze-arch/logger', () => ({
reporter: vi.fn(),
}));
vi.mock('@coze-arch/bot-error', () => ({}));
describe('message-report', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('Should setup correctly', () => {
const { log_id } = messageReportEvent.getLogID();
expect(log_id).equal(TEST_LOG_ID);
messageReportEvent.start(TEST_BOT_ID);
const { bot_id, log_id: logId } = messageReportEvent.getMetaCtx();
expect(bot_id).equal(TEST_BOT_ID);
expect(logId).equal(TEST_LOG_ID);
});
/// messageReceiveSuggestsEvent & receiveMessageEvent
test('messageReceiveSuggestsEvent & receiveMessageEvent should not trigger report event if `start` has not been called', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.receiveMessageEvent,
].forEach(event => {
event.success();
event.finish('' as any);
event.error({
error: new Error(),
reason: '',
});
if (event === messageReportEvent.receiveMessageEvent) {
event.receiveMessage({ message_id: '' });
}
expect(createReportEvent).not.toHaveBeenCalled();
expect(mockAddDurationPoint).not.toHaveBeenCalled();
});
});
test('messageReceiveSuggestsEvent & receiveMessageEvent should trigger reporter correctly by calling `receiveSuggest`', () => {
messageReportEvent.messageReceiveSuggestsEvent.start();
messageReportEvent.messageReceiveSuggestsEvent.receiveSuggest();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('first');
});
test('`success` should trigger reporter correctly', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach(event => {
['success', 'finish'].forEach(tag => {
event.start();
event[tag]();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('success');
expect(mockSuccess).toHaveBeenCalled();
});
});
});
test('messageReceiveSuggestsEvent & receiveMessageEvent should trigger reporter correctly by calling `error`', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach(event => {
event.start();
event.error({
error: new Error(),
reason: '',
});
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('failed');
expect(mockError).toHaveBeenCalled();
});
});
test('executeDraftBotEvent should report correctly by calling start', () => {
const event = messageReportEvent.executeDraftBotEvent;
event.start();
event.success();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('finish');
expect(mockSuccess).toHaveBeenCalled();
});
test('executeDraftBotEvent should report correctly by calling error', () => {
const event = messageReportEvent.executeDraftBotEvent;
event.start();
event.error({ error: new Error(), reason: '' });
expect(createReportEvent).toHaveBeenCalled();
expect(mockError).toHaveBeenCalled();
});
test('interrupt', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach((event, index) => {
if (index === 0) {
event.receiveSuggest();
}
event.start();
messageReportEvent.interrupt();
expect(mockSuccess).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,123 @@
/*
* 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 { ceil } from 'lodash-es';
import {
formatBytes,
formatNumber,
getEllipsisCount,
simpleformatNumber,
sleep,
formatPercent,
formatTime,
} from '../src/number';
describe('Number', () => {
it('#simpleformatNumber', () => {
expect(simpleformatNumber('100')).toEqual('100');
expect(simpleformatNumber('100.1')).toEqual('100');
expect(simpleformatNumber(100.1)).toEqual('100');
expect(simpleformatNumber(1100)).toEqual('1,100');
expect(simpleformatNumber('1100')).toEqual('1,100');
});
it('formatBytes', () => {
const k = 1024;
const decimals = 2;
const genRandomNum = (bytes: number) =>
Array(bytes)
.fill(0)
.reduce(
(prev, _, idx) =>
prev +
Math.floor(((Math.random() + 1) * (k - 1) * Math.pow(k, idx)) / 2),
0,
);
const calDigit = (num: number, unit: number) =>
parseFloat((num / Math.pow(k, unit)).toFixed(decimals));
expect(formatBytes(0)).equal('0 Byte');
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
sizes.forEach((size, idx) => {
const num = genRandomNum(idx + 1);
const digit = calDigit(num, idx);
expect(formatBytes(num)).equal(`${digit} ${size}`);
});
});
it('formatNumber', () => {
const base = 1000;
const units = ['', 'K', 'M', 'B', 'T'];
const genRandomNum = (order: number) =>
Array(order)
.fill(0)
.reduce(
(prev, _, idx) =>
prev +
Math.floor(
((Math.random() + 1) * (base - 1) * Math.pow(base, idx)) / 2,
),
0,
);
const calDigit = (num: number, unit: number) =>
ceil(Math.abs(num) / Math.pow(base, unit), 1);
units.forEach((unit, idx) => {
const num = genRandomNum(idx + 1);
const digit = calDigit(num, idx);
expect(formatNumber(num).toString()).equal(`${digit}${unit}`);
});
});
it('getEllipsisCount', () => {
const max = 1000;
const num1 = max - 1;
const num2 = max;
const num3 = max + 1;
expect(getEllipsisCount(num1, max)).equal(`${num1}`);
expect(getEllipsisCount(num2, max)).equal(`${num2}`);
expect(getEllipsisCount(num3, max)).equal(`${max}+`);
});
it('sleep', async () => {
const mockFn = vi.fn();
const interval = 3000;
vi.useFakeTimers();
const promisedSleep = sleep(interval).then(() => {
mockFn();
});
vi.advanceTimersByTime(interval);
await promisedSleep;
expect(mockFn).toHaveBeenCalled();
vi.restoreAllMocks();
});
it('formatPercent', () => {
expect(formatPercent(0.1)).equal('10%');
expect(formatPercent(0.123456)).equal('12.3%');
expect(formatPercent(0.12556)).equal('12.6%');
expect(formatPercent(1)).equal('100%');
expect(formatPercent()).equal('NaN%');
});
it('formatTime', () => {
expect(formatTime(1000)).equal('1000ms');
expect(formatTime(12000)).equal('12s');
expect(formatTime(12330)).equal('12.3s');
expect(formatTime(1000.12332)).equal('1000ms');
});
});

View File

@@ -0,0 +1,66 @@
/*
* 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.
*/
/*
表格单元格宽度适配
当 width 小于 WidthThresholds.Small 时应该返回 ColumnSize.Default。
当 width 大于等于 WidthThresholds.Small 但小于 WidthThresholds.Medium 时应该返回 ColumnSize.Small。
当 width 大于等于 WidthThresholds.Medium 但小于 WidthThresholds.Large 时应该返回 ColumnSize.Medium。
当 width 大于等于 WidthThresholds.Large 时应该返回 ColumnSize.Large。
当 minWidth 为 'auto' 时应该返回 'auto'。
当 minWidth 是一个指定数字时,应该返回 minWidth 和 columnWidth 中较大的一个。*/
import { describe, it, expect } from 'vitest';
import {
responsiveTableColumn,
ColumnSize,
WidthThresholds,
} from '../src/responsive-table-column';
describe('responsiveTableColumn', () => {
it('returns auto for minWidth auto', () => {
expect(responsiveTableColumn(1000, 'auto')).toBe('auto');
});
it('returns minWidth when minWidth is a number and greater than columnWidth', () => {
expect(responsiveTableColumn(1000, 80)).toBe(80);
});
it('returns ColumnSize.Small for width less than WidthThresholds.Small', () => {
expect(responsiveTableColumn(WidthThresholds.Small - 1, 50)).toBe(
ColumnSize.Default,
);
});
it('returns ColumnSize.Medium for width between WidthThresholds.Small and WidthThresholds.Medium', () => {
expect(responsiveTableColumn(WidthThresholds.Medium - 1, 50)).toBe(
ColumnSize.Small,
);
});
it('returns ColumnSize.Large for width between WidthThresholds.Medium and WidthThresholds.Large', () => {
expect(responsiveTableColumn(WidthThresholds.Large - 1, 50)).toBe(
ColumnSize.Medium,
);
});
it('returns ColumnSize.Large for width greater than or equal to WidthThresholds.Large', () => {
expect(responsiveTableColumn(WidthThresholds.Large, 50)).toBe(
ColumnSize.Large,
);
});
});

View File

@@ -0,0 +1,38 @@
/*
* 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 { retryImport } from '../src/retry-import';
describe('retry-import tests', () => {
it('retryImport', async () => {
const maxRetryCount = 3;
let maxCount = 0;
const mockImport = () =>
new Promise<number>((resolve, reject) => {
setTimeout(() => {
if (maxCount >= maxRetryCount) {
resolve(maxCount);
return;
}
maxCount++;
reject(new Error('load error!'));
}, 1000);
});
expect(await retryImport<number>(() => mockImport(), maxRetryCount)).toBe(
3,
);
});
});

View File

@@ -0,0 +1,74 @@
/*
* 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 { expect, it } from 'vitest';
import { logger } from '@coze-arch/logger';
import { safeJSONParse, typeSafeJSONParse } from '../src/safe-json-parse';
vi.mock('@coze-arch/logger', () => ({
logger: {
persist: {
error: vi.fn(),
},
},
reporter: {
errorEvent: vi.fn(),
},
}));
describe('safe-json-parse', () => {
test('safeJSONParse without error', () => {
const test1 = '{}';
const res1 = safeJSONParse(test1);
expect(res1).toStrictEqual({});
const test2 = '[]';
const res2 = safeJSONParse(test2);
expect(res2).toStrictEqual([]);
expect(logger.persist.error).not.toHaveBeenCalled();
});
test('safeJSONParse with error', () => {
const test = '';
const res1 = safeJSONParse(test);
expect(res1).equal(undefined);
expect(logger.persist.error).toHaveBeenCalledTimes(1);
const expectValue = 'empty_value';
const res2 = safeJSONParse(test, expectValue);
expect(res2).equal(expectValue);
expect(logger.persist.error).toHaveBeenCalledTimes(2);
});
});
describe('type safe json parse', () => {
it('parse obj return as input', () => {
const ob = {};
expect(typeSafeJSONParse(ob)).toBe(ob);
});
it('parse legally', () => {
const ob = { a: 1 };
expect(typeSafeJSONParse(JSON.stringify(ob))).toMatchObject(ob);
});
it('throw error when illegal', () => {
const str = '{';
expect(typeSafeJSONParse(str)).toBeUndefined();
});
});

View File

@@ -0,0 +1,23 @@
/*
* 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.
*/
global.IS_OVERSEA = false;
global.APP_ID = '';
global.IMAGE_FALLBACK_HOST = '';
global.BYTE_UPLOADER_REGION = '';
global.SAMI_WS_ORIGIN = '';
global.SAMI_CHAT_WS_URL = '';
global.SAMI_APP_KEY = '';

View File

@@ -0,0 +1,34 @@
/*
* 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 { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import { skillKeyToApiStatusKeyTransformer } from '../src/skill';
vi.stubGlobal('IS_DEV_MODE', false);
vi.mock('@coze-agent-ide/tool', () => ({
SkillKeyEnum: {
TEXT_TO_SPEECH: 'tts',
},
}));
describe('skill', () => {
test('skillKeyToApiStatusKeyTransformer', () => {
const test = SkillKeyEnum.TEXT_TO_SPEECH;
expect(skillKeyToApiStatusKeyTransformer(test)).equal(`${test}_tab_status`);
});
});

View File

@@ -0,0 +1,99 @@
/*
* 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 { uploadFileV2 } from '../src/upload-file-v2';
vi.mock('@coze-arch/bot-api', () => ({
DeveloperApi: {
GetUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
},
}));
vi.mock('@coze-studio/uploader-adapter', () => {
class MockUploader {
start = () => 0;
addFile = () => '12312341';
test: 'test';
on(event: string, cb: (data: any) => void) {
if (event === 'complete') {
cb({ uploadResult: { Uri: 'test_url' } });
} else if (event === 'error') {
cb({ extra: 'error' });
} else if (event === 'progress') {
cb(50);
}
}
}
return {
getUploader: vi.fn((props: any, isOverSea?: boolean) => new MockUploader()),
};
});
describe('upload-file', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('upLoadFile should resolve Url of result if upload success', () =>
new Promise((resolve, reject) => {
global.IS_OVERSEA = true;
uploadFileV2({
fileItemList: [{ file: new File([], 'test_file'), fileType: 'image' }],
userId: '123',
timeout: undefined,
signal: new AbortSignal(),
onSuccess: event => {
try {
expect(event.uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
onUploadAllSuccess(event) {
try {
expect(event[0].uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
});
global.IS_OVERSEA = false;
uploadFileV2({
fileItemList: [{ file: new File([], 'test_file'), fileType: 'image' }],
userId: '123',
timeout: undefined,
signal: new AbortSignal(),
onSuccess: event => {
try {
expect(event.uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
onUploadAllSuccess(event) {
try {
expect(event[0].uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
});
}));
});

View File

@@ -0,0 +1,110 @@
/*
* 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 Mock } from 'vitest';
import { userStoreService } from '@coze-studio/user-store';
import { upLoadFile } from '../src/upload-file';
vi.mock('@coze-arch/bot-api', () => ({
DeveloperApi: {
GetUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
},
}));
vi.mock('@coze-studio/user-store', () => ({
userStoreService: {
getUserInfo: vi.fn(() => ({
user_id_str: '',
})),
},
}));
vi.mock('@coze-studio/uploader-adapter', () => {
class MockUploader {
userId: string;
constructor({ userId }) {
this.userId = userId;
}
on(event: string, cb: (data: any) => void) {
if (event === 'complete' && this.userId) {
cb({ uploadResult: { Uri: 'test_url' } });
} else if (event === 'error' && !this.userId) {
cb({ extra: 'error' });
} else if (event === 'progress') {
cb(50);
}
}
}
return {
getUploader: vi.fn(
(props: any, isOverSea?: boolean) => new MockUploader(props),
),
};
});
describe('upload-file', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('upLoadFile should resolve Url of result if upload success', async () => {
// mock `userId` non-empty to invoke upload success
(userStoreService.getUserInfo as Mock).mockReturnValue({
user_id_str: 'test',
});
const res = await upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
});
expect(res).equal('test_url');
global.IS_OVERSEA = false;
(userStoreService.getUserInfo as Mock).mockReturnValue({
user_id_str: 'test',
});
const res2 = await upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
});
expect(res2).equal('test_url');
});
test('upLoadFile should reject extra info of result if upload failed', () => {
// mock `userId` empty to invoke upload failed
(userStoreService.getUserInfo as Mock).mockReturnValue({ user_id_str: '' });
expect(
upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
}),
).rejects.toThrow('error');
});
test('upLoadFile should use getUploadAuthToken if biz is not bot or workflow ', () => {
// mock `userId` empty to invoke upload failed
(userStoreService.getUserInfo as Mock).mockReturnValue({ user_id_str: '' });
expect(
upLoadFile({
biz: 'community',
file: new File([], 'test_file'),
fileType: 'image',
getUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
}),
).rejects.toThrow('error');
});
});

View File

@@ -0,0 +1,100 @@
/*
* 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 { appendUrlParam, getParamsFromQuery, openUrl } from '../src/url';
import { getIsMobile, getIsSafari } from '../src/platform';
vi.mock('../src/platform', () => ({
getIsMobile: vi.fn(),
getIsSafari: vi.fn(),
}));
const mockParseQuery = (queryStr: string) =>
Object.fromEntries(
queryStr
.split('&')
.map(str => {
const parts = str.split('=');
return [parts[0] ?? '', parts?.[1] ?? ''];
})
.filter(entries => !!entries[0]),
);
vi.mock('query-string', () => ({
default: {
parseUrl: (url: string) => {
const [rawUrl, queryStr = ''] = url.split('?');
return {
url: rawUrl,
query: mockParseQuery(queryStr),
};
},
parse: (queryStr: string) => mockParseQuery(queryStr.slice(1)),
stringifyUrl: ({ url, query }: { url: string; query: string }) =>
`${url}${Object.entries(query).length ? '?' : ''}${Object.entries(query)
.map(entry => entry.join('='))
.join('&')}`,
},
}));
describe('URL', () => {
beforeEach(() => {
vi.stubGlobal('location', { href: '' });
vi.stubGlobal('window', {
open: vi.fn(),
});
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it('#getParamsFromQuery', () => {
vi.stubGlobal('location', { search: '' });
expect(getParamsFromQuery({ key: '' })).toEqual('');
expect(getParamsFromQuery({ key: 'a' })).toEqual('');
vi.stubGlobal('location', { search: '?a=b' });
expect(getParamsFromQuery({ key: '' })).toEqual('');
expect(getParamsFromQuery({ key: 'a' })).toEqual('b');
});
it('#appendUrlParam', () => {
expect(appendUrlParam('http://test.com', 'k1', 'v1')).equal(
'http://test.com?k1=v1',
);
expect(appendUrlParam('http://test.com?k1=v1', 'k2', 'v2')).equal(
'http://test.com?k1=v1&k2=v2',
);
expect(appendUrlParam('http://test.com?k1=v1', 'k1', '')).equal(
'http://test.com',
);
});
it('#openUrl', () => {
openUrl(undefined);
expect(window.open).not.toHaveBeenCalled();
expect(location.href).toBe('');
vi.mocked(getIsMobile).mockReturnValue(true);
vi.mocked(getIsSafari).mockReturnValue(true);
openUrl('https://example.com');
expect(location.href).toBe('https://example.com');
expect(window.open).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,39 @@
/*
* 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 { setMobileBody, setPCBody } from '../src/viewport';
describe('viewport', () => {
it('#setMobileBody', () => {
setMobileBody();
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
expect(bodyStyle.minWidth).toEqual('0');
expect(bodyStyle.minHeight).toEqual('0');
expect(htmlStyle.minWidth).toEqual('0');
expect(htmlStyle.minHeight).toEqual('0');
});
it('#setPCBody', () => {
setPCBody();
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
expect(bodyStyle.minWidth).toEqual('1200px');
expect(bodyStyle.minHeight).toEqual('600px');
expect(htmlStyle.minWidth).toEqual('1200px');
expect(htmlStyle.minHeight).toEqual('600px');
});
});