feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
16
frontend/packages/foundation/account-base/README.md
Normal file
16
frontend/packages/foundation/account-base/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-foundation/account-base
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
vi.stubGlobal('IS_DEV_MODE', false);
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
ignores: ['**/__tests__/*'],
|
||||
});
|
||||
47
frontend/packages/foundation/account-base/package.json
Normal file
47
frontend/packages/foundation/account-base/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@coze-foundation/account-base",
|
||||
"version": "0.0.1",
|
||||
"description": "account & login related utils & hooks & stores",
|
||||
"license": "Apache-2.0",
|
||||
"author": "duwenhan@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"dev": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-foundation/local-storage": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/foundation-sdk": "workspace:*",
|
||||
"@coze-arch/idl": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import {
|
||||
handleAPIErrorEvent,
|
||||
removeAPIErrorEvent,
|
||||
APIErrorEvent,
|
||||
} from '@coze-arch/bot-api';
|
||||
|
||||
import { useCheckLoginBase } from '../factory';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
vi.mock('@coze-arch/bot-api', () => ({
|
||||
handleAPIErrorEvent: vi.fn(),
|
||||
removeAPIErrorEvent: vi.fn(),
|
||||
APIErrorEvent: { UNAUTHORIZED: 'UNAUTHORIZED' },
|
||||
}));
|
||||
|
||||
const mockCheckLoginImpl = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ userInfo: null, hasError: false });
|
||||
const mockGoLogin = vi.fn();
|
||||
const mockReset = vi.fn();
|
||||
const originalUserStore = {
|
||||
...useUserStore.getState(),
|
||||
reset: mockReset,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useUserStore.setState(originalUserStore);
|
||||
vi.clearAllMocks();
|
||||
mockGoLogin.mockReset();
|
||||
});
|
||||
|
||||
describe('useCheckLoginBase', () => {
|
||||
it('should call checkLoginBase when isSettled is false', () => {
|
||||
useUserStore.setState({ isSettled: false });
|
||||
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
|
||||
expect(mockCheckLoginImpl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should call checkLoginBase when isSettled is false and require auth is false', () => {
|
||||
useUserStore.setState({ isSettled: false });
|
||||
renderHook(() => useCheckLoginBase(false, mockCheckLoginImpl, mockGoLogin));
|
||||
expect(mockCheckLoginImpl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should redirect to login when needLogin is true and user is not logged in', () => {
|
||||
useUserStore.setState({ isSettled: true, userInfo: null });
|
||||
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
|
||||
expect(mockGoLogin).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not redirect when user is logged in', () => {
|
||||
useUserStore.setState({
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: '123' },
|
||||
});
|
||||
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
|
||||
expect(mockGoLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle UNAUTHORIZED event and redirect', () => {
|
||||
const { unmount } = renderHook(() =>
|
||||
useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin),
|
||||
);
|
||||
const handleUnauthorized = (handleAPIErrorEvent as Mock).mock.calls[0][1];
|
||||
|
||||
act(() => handleUnauthorized());
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
expect(mockGoLogin).toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
expect(removeAPIErrorEvent).toHaveBeenCalledWith(
|
||||
APIErrorEvent.UNAUTHORIZED,
|
||||
handleUnauthorized,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not redirect on UNAUTHORIZED if needLogin is false', () => {
|
||||
renderHook(() => useCheckLoginBase(false, mockCheckLoginImpl, mockGoLogin));
|
||||
const handleUnauthorized = (handleAPIErrorEvent as Mock).mock.calls[0][1];
|
||||
|
||||
act(() => handleUnauthorized());
|
||||
expect(mockGoLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import { useDocumentVisibility } from 'ahooks';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useLoginStatus, useAlterOnLogout } from '../index';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
// Mock ahooks
|
||||
vi.mock('ahooks', async importOriginal => {
|
||||
const original = await importOriginal();
|
||||
return {
|
||||
...original,
|
||||
useDocumentVisibility: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useUserStore
|
||||
vi.mock('../../store/user', () => ({
|
||||
useUserStore: vi.fn(),
|
||||
}));
|
||||
|
||||
const UID_KEY = 'coze_current_uid';
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
length: 0,
|
||||
key: (index: number) => null,
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'addEventListener', {
|
||||
value: vi.fn(),
|
||||
});
|
||||
Object.defineProperty(window, 'removeEventListener', {
|
||||
value: vi.fn(),
|
||||
});
|
||||
|
||||
describe('Account Hooks from index.ts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
// Default mock for useUserStore to return a function that can be called with a selector
|
||||
(useUserStore as unknown as Mock).mockImplementation(selector =>
|
||||
selector(mockUserState),
|
||||
);
|
||||
});
|
||||
|
||||
let mockUserState: any;
|
||||
|
||||
describe('useLoginStatus', () => {
|
||||
it('should return "settling" if store is not settled', () => {
|
||||
mockUserState = { isSettled: false, userInfo: null };
|
||||
const { result } = renderHook(() => useLoginStatus());
|
||||
expect(result.current).toBe('settling');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and no userInfo', () => {
|
||||
mockUserState = { isSettled: true, userInfo: null };
|
||||
const { result } = renderHook(() => useLoginStatus());
|
||||
expect(result.current).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and userInfo has no user_id_str', () => {
|
||||
mockUserState = { isSettled: true, userInfo: { name: 'Test' } };
|
||||
const { result } = renderHook(() => useLoginStatus());
|
||||
expect(result.current).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "logined" if store is settled and userInfo has user_id_str', () => {
|
||||
mockUserState = { isSettled: true, userInfo: { user_id_str: '123' } };
|
||||
const { result } = renderHook(() => useLoginStatus());
|
||||
expect(result.current).toBe('logined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAlterOnLogout', () => {
|
||||
let alertMock: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
alertMock = vi.fn();
|
||||
(useDocumentVisibility as Mock).mockReturnValue('visible');
|
||||
// Mock getState for the effect cleanup function
|
||||
(useUserStore as any).getState = vi.fn(() => mockUserState);
|
||||
});
|
||||
|
||||
it('should not call alert if document is visible or user is not logged in', () => {
|
||||
mockUserState = { isSettled: true, userInfo: null }; // Not logged in
|
||||
renderHook(() => useAlterOnLogout(alertMock));
|
||||
// Simulate visibility change to hidden and back to visible (triggering cleanup and re-run)
|
||||
act(() => {
|
||||
(useDocumentVisibility as Mock).mockReturnValue('hidden');
|
||||
});
|
||||
act(() => {
|
||||
(useDocumentVisibility as Mock).mockReturnValue('visible');
|
||||
});
|
||||
expect(alertMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call alert if user was logged in, document becomes visible, and UID in localStorage is different or null', () => {
|
||||
const currentUserId = 'user123';
|
||||
mockUserState = {
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: currentUserId },
|
||||
}; // Logged in
|
||||
localStorageMock.setItem(UID_KEY, currentUserId); // UID in localStorage matches
|
||||
|
||||
(useDocumentVisibility as Mock).mockReturnValue('hidden'); // Start hidden
|
||||
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
|
||||
|
||||
// Simulate user logging out in another tab (localStorage UID changes)
|
||||
localStorageMock.removeItem(UID_KEY);
|
||||
|
||||
// Simulate tab becoming visible
|
||||
act(() => {
|
||||
(useDocumentVisibility as Mock).mockReturnValue('visible');
|
||||
});
|
||||
rerender(); // Rerender to trigger useEffect with new visibility
|
||||
|
||||
expect(alertMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call alert if user was logged in, document becomes visible, and UID in localStorage is different', () => {
|
||||
const currentUserId = 'user123';
|
||||
const otherTabUserId = 'user456';
|
||||
mockUserState = {
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: currentUserId },
|
||||
}; // Logged in
|
||||
localStorageMock.setItem(UID_KEY, currentUserId); // UID in localStorage matches
|
||||
|
||||
(useDocumentVisibility as Mock).mockReturnValue('hidden'); // Start hidden
|
||||
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
|
||||
|
||||
localStorageMock.setItem(UID_KEY, otherTabUserId); // UID changes in another tab
|
||||
|
||||
act(() => {
|
||||
(useDocumentVisibility as Mock).mockReturnValue('visible');
|
||||
});
|
||||
rerender();
|
||||
|
||||
expect(alertMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should NOT call alert if user was logged in, document becomes visible, and UID in localStorage matches', () => {
|
||||
const currentUserId = 'user123';
|
||||
mockUserState = {
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: currentUserId },
|
||||
}; // Logged in
|
||||
localStorageMock.setItem(UID_KEY, currentUserId);
|
||||
|
||||
(useDocumentVisibility as Mock).mockReturnValue('hidden');
|
||||
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
|
||||
|
||||
act(() => {
|
||||
(useDocumentVisibility as Mock).mockReturnValue('visible');
|
||||
});
|
||||
rerender();
|
||||
|
||||
expect(alertMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { localStorageService } from '@coze-foundation/local-storage';
|
||||
|
||||
import { useSyncLocalStorageUid } from '../use-sync-local-storage-uid';
|
||||
import { useLoginStatus, useUserInfo } from '../index';
|
||||
|
||||
// Mock hooks and services
|
||||
vi.mock('../index', () => ({
|
||||
useLoginStatus: vi.fn(),
|
||||
useUserInfo: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-foundation/local-storage', () => ({
|
||||
localStorageService: {
|
||||
setUserId: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useSyncLocalStorageUid', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it('update uid when login status changes', () => {
|
||||
const mockUserInfo = { user_id_str: '123456' };
|
||||
const { rerender } = renderHook(() => useSyncLocalStorageUid(), {
|
||||
initialProps: {},
|
||||
});
|
||||
|
||||
// 初始状态:未登录
|
||||
(useLoginStatus as Mock).mockReturnValue('not_login');
|
||||
(useUserInfo as Mock).mockReturnValue(null);
|
||||
rerender();
|
||||
expect(localStorageService.setUserId).toHaveBeenCalledWith();
|
||||
|
||||
// 切换到登录状态
|
||||
(useLoginStatus as Mock).mockReturnValue('logined');
|
||||
(useUserInfo as Mock).mockReturnValue(mockUserInfo);
|
||||
rerender();
|
||||
expect(localStorageService.setUserId).toHaveBeenCalledWith(
|
||||
mockUserInfo.user_id_str,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import {
|
||||
APIErrorEvent,
|
||||
handleAPIErrorEvent,
|
||||
removeAPIErrorEvent,
|
||||
} from '@coze-arch/bot-api';
|
||||
|
||||
import { checkLoginBase } from '../utils/factory';
|
||||
import { type UserInfo } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* 用于页面初始化时,检查登录状态,并监听登录态失效的接口报错
|
||||
* 在登录态失效时,会重定向到登录页
|
||||
* @param needLogin 是否需要登录
|
||||
* @param checkLogin 检查登录状态的具体实现
|
||||
* @param goLogin 重定向到登录页的具体实现
|
||||
*/
|
||||
export const useCheckLoginBase = (
|
||||
needLogin: boolean,
|
||||
checkLoginImpl: () => Promise<{
|
||||
userInfo?: UserInfo;
|
||||
hasError?: boolean;
|
||||
}>,
|
||||
goLogin: () => void,
|
||||
) => {
|
||||
const isSettled = useUserStore(state => state.isSettled);
|
||||
|
||||
const memoizedGoLogin = useMemoizedFn(goLogin);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSettled) {
|
||||
checkLoginBase(checkLoginImpl);
|
||||
}
|
||||
}, [isSettled]);
|
||||
|
||||
useEffect(() => {
|
||||
const isLogined = !!useUserStore.getState().userInfo?.user_id_str;
|
||||
// 当前页面要求登录,登录检查结果为未登录时,重定向回登录页
|
||||
if (needLogin && isSettled && !isLogined) {
|
||||
memoizedGoLogin();
|
||||
}
|
||||
}, [needLogin, isSettled]);
|
||||
|
||||
useEffect(() => {
|
||||
let fired = false;
|
||||
const handleUnauthorized = () => {
|
||||
useUserStore.getState().reset();
|
||||
if (needLogin) {
|
||||
if (!fired) {
|
||||
fired = true;
|
||||
memoizedGoLogin();
|
||||
}
|
||||
}
|
||||
};
|
||||
// ajax 请求后端接口出现未 授权/登录 时,触发该函数
|
||||
handleAPIErrorEvent(APIErrorEvent.UNAUTHORIZED, handleUnauthorized);
|
||||
return () => {
|
||||
removeAPIErrorEvent(APIErrorEvent.UNAUTHORIZED, handleUnauthorized);
|
||||
};
|
||||
}, [needLogin]);
|
||||
};
|
||||
88
frontend/packages/foundation/account-base/src/hooks/index.ts
Normal file
88
frontend/packages/foundation/account-base/src/hooks/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
|
||||
import { useDocumentVisibility, useMemoizedFn } from 'ahooks';
|
||||
|
||||
import { type LoginStatus } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* @description 用于获取用户登录状态
|
||||
* @returns 登录状态
|
||||
*/
|
||||
export const useLoginStatus = (): LoginStatus =>
|
||||
useUserStore(state => {
|
||||
if (state.isSettled) {
|
||||
return state.userInfo?.user_id_str ? 'logined' : 'not_login';
|
||||
}
|
||||
return 'settling';
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 用于获取用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
export const useUserInfo = () => useUserStore(state => state.userInfo);
|
||||
|
||||
/**
|
||||
* @description 当前是否为错误状态
|
||||
* @returns 是否为错误状态
|
||||
*/
|
||||
export const useHasError = () => useUserStore(state => state.hasError);
|
||||
|
||||
const currentUidLSKey = 'coze_current_uid';
|
||||
/**
|
||||
* 用于打开多页签情况下,探测其它页签下发生的登出事件并在当前触发提示
|
||||
* @param alert 触发提示的具体实现
|
||||
*/
|
||||
export const useAlterOnLogout = (alert: () => void) => {
|
||||
const visibility = useDocumentVisibility();
|
||||
const loginStatus = useLoginStatus();
|
||||
|
||||
const isLogined = loginStatus === 'logined';
|
||||
const memoizedAlert = useMemoizedFn(() => {
|
||||
alert();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (visibility === 'hidden' && isLogined) {
|
||||
const lastUserId = useUserStore.getState().userInfo?.user_id_str;
|
||||
// 登录态下,每次页面从后台回到前台,重新检查一次登录用户是否发生了变化
|
||||
return () => {
|
||||
const latestUserId = localStorage.getItem(currentUidLSKey);
|
||||
if (lastUserId !== latestUserId) {
|
||||
memoizedAlert();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [visibility, isLogined]);
|
||||
|
||||
// 在登录态变化后,更新本地缓存状态
|
||||
useEffect(() => {
|
||||
if (loginStatus !== 'settling') {
|
||||
localStorage.setItem(
|
||||
currentUidLSKey,
|
||||
useUserStore.getState().userInfo?.user_id_str ?? '',
|
||||
);
|
||||
}
|
||||
}, [loginStatus]);
|
||||
};
|
||||
|
||||
export const useUserLabel = () => useUserStore(state => state.userLabel);
|
||||
|
||||
export const useUserAuthInfo = () => useUserStore(state => state.userAuthInfos);
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
|
||||
import { localStorageService } from '@coze-foundation/local-storage';
|
||||
|
||||
import { useLoginStatus, useUserInfo } from './index';
|
||||
|
||||
export const useSyncLocalStorageUid = () => {
|
||||
const userInfo = useUserInfo();
|
||||
const loginStatus = useLoginStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'logined') {
|
||||
localStorageService.setUserId(userInfo?.user_id_str);
|
||||
}
|
||||
if (loginStatus === 'not_login') {
|
||||
localStorageService.setUserId();
|
||||
}
|
||||
}, [loginStatus, userInfo?.user_id_str]);
|
||||
};
|
||||
56
frontend/packages/foundation/account-base/src/index.ts
Normal file
56
frontend/packages/foundation/account-base/src/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
// types
|
||||
export type { UserInfo, LoginStatus } from './types';
|
||||
export type {
|
||||
OAuth2RedirectConfig,
|
||||
Connector2Redirect,
|
||||
} from './types/passport';
|
||||
|
||||
// common hooks
|
||||
export {
|
||||
useLoginStatus,
|
||||
useUserInfo,
|
||||
useHasError,
|
||||
useAlterOnLogout,
|
||||
useUserLabel,
|
||||
useUserAuthInfo,
|
||||
} from './hooks';
|
||||
|
||||
export { useSyncLocalStorageUid } from './hooks/use-sync-local-storage-uid';
|
||||
|
||||
// common utils
|
||||
export {
|
||||
getUserInfo,
|
||||
getUserLabel,
|
||||
getLoginStatus,
|
||||
resetUserStore,
|
||||
setUserInfo,
|
||||
getUserAuthInfos,
|
||||
subscribeUserAuthInfos,
|
||||
usernameRegExpValidate,
|
||||
} from './utils';
|
||||
|
||||
// base hooks
|
||||
export { useCheckLoginBase } from './hooks/factory';
|
||||
|
||||
// base utils
|
||||
export {
|
||||
refreshUserInfoBase,
|
||||
logoutBase,
|
||||
checkLoginBase,
|
||||
} from './utils/factory';
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { DeveloperApi, PlaygroundApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { useUserStore, defaultState } from '../user';
|
||||
|
||||
vi.mock('@coze-arch/bot-api', () => ({
|
||||
DeveloperApi: {
|
||||
GetUserAuthList: vi.fn(),
|
||||
},
|
||||
PlaygroundApi: {
|
||||
MGetUserBasicInfo: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useUserStore', () => {
|
||||
beforeEach(() => {
|
||||
useUserStore.setState(defaultState);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have the correct default state', () => {
|
||||
const state = useUserStore.getState();
|
||||
expect(state.isSettled).toBe(false);
|
||||
expect(state.userInfo).toBeNull();
|
||||
expect(state.hasError).toBe(false);
|
||||
expect(state.userAuthInfos).toEqual([]);
|
||||
expect(state.userLabel).toBeNull();
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('reset should reset state to default and set isSettled to true', () => {
|
||||
useUserStore.setState({
|
||||
userInfo: { user_id_str: '123' } as any,
|
||||
isSettled: false,
|
||||
});
|
||||
useUserStore.getState().reset();
|
||||
const state = useUserStore.getState();
|
||||
expect(state.userInfo).toBeNull();
|
||||
expect(state.userAuthInfos).toEqual([]);
|
||||
expect(state.userLabel).toBeNull();
|
||||
expect(state.isSettled).toBe(true);
|
||||
expect(state.hasError).toBe(false);
|
||||
});
|
||||
|
||||
it('setIsSettled should update isSettled', () => {
|
||||
useUserStore.getState().setIsSettled(true);
|
||||
expect(useUserStore.getState().isSettled).toBe(true);
|
||||
useUserStore.getState().setIsSettled(false);
|
||||
expect(useUserStore.getState().isSettled).toBe(false);
|
||||
});
|
||||
|
||||
describe('setUserInfo', () => {
|
||||
it('should update userInfo', () => {
|
||||
const newUserInfo = {
|
||||
user_id_str: 'testUser',
|
||||
name: 'Test User',
|
||||
} as any;
|
||||
useUserStore.getState().setUserInfo(newUserInfo);
|
||||
expect(useUserStore.getState().userInfo).toEqual(newUserInfo);
|
||||
});
|
||||
|
||||
it('should call fetchUserLabel if user_id_str changes', async () => {
|
||||
const newUserInfo = {
|
||||
user_id_str: 'newUser123',
|
||||
name: 'New User',
|
||||
} as any;
|
||||
const initialUserInfo = {
|
||||
user_id_str: 'oldUser456',
|
||||
name: 'Old User',
|
||||
} as any;
|
||||
|
||||
// Set an initial user
|
||||
useUserStore.setState({ userInfo: initialUserInfo });
|
||||
|
||||
(PlaygroundApi.MGetUserBasicInfo as Mock).mockResolvedValueOnce({
|
||||
id_user_info_map: {
|
||||
[newUserInfo.user_id_str]: {
|
||||
user_label: { label_type: 1, text: 'Test Label' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useUserStore.getState().setUserInfo(newUserInfo);
|
||||
expect(useUserStore.getState().userInfo).toEqual(newUserInfo);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(PlaygroundApi.MGetUserBasicInfo).toHaveBeenCalledWith({
|
||||
user_ids: [newUserInfo.user_id_str],
|
||||
});
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(useUserStore.getState().userLabel).toEqual({
|
||||
label_type: 1,
|
||||
text: 'Test Label',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call fetchUserLabel if user_id_str is the same', () => {
|
||||
const userInfo = { user_id_str: 'user123', name: 'Test User' } as any;
|
||||
useUserStore.setState({ userInfo });
|
||||
|
||||
useUserStore.getState().setUserInfo(userInfo);
|
||||
expect(PlaygroundApi.MGetUserBasicInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAuthInfos', () => {
|
||||
it('should fetch and set userAuthInfos on success', async () => {
|
||||
const mockAuthInfos = [
|
||||
{ auth_type: 'email', auth_key: 'test@example.com' },
|
||||
];
|
||||
(DeveloperApi.GetUserAuthList as Mock).mockResolvedValueOnce({
|
||||
data: mockAuthInfos,
|
||||
});
|
||||
|
||||
await useUserStore.getState().getUserAuthInfos();
|
||||
|
||||
expect(DeveloperApi.GetUserAuthList).toHaveBeenCalledTimes(1);
|
||||
expect(useUserStore.getState().userAuthInfos).toEqual(mockAuthInfos);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
89
frontend/packages/foundation/account-base/src/store/user.ts
Normal file
89
frontend/packages/foundation/account-base/src/store/user.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { devtools, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
type UserAuthInfo,
|
||||
type UserLabel,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi, PlaygroundApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type UserInfo } from '../types';
|
||||
|
||||
export interface UserStoreState {
|
||||
isSettled: boolean;
|
||||
hasError: boolean;
|
||||
userInfo: UserInfo | null;
|
||||
userAuthInfos: UserAuthInfo[];
|
||||
userLabel: UserLabel | null;
|
||||
}
|
||||
|
||||
export interface UserStoreAction {
|
||||
reset: () => void;
|
||||
setIsSettled: (isSettled: boolean) => void;
|
||||
setUserInfo: (userInfo: UserInfo | null) => void;
|
||||
getUserAuthInfos: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const defaultState: UserStoreState = {
|
||||
isSettled: false,
|
||||
userInfo: null,
|
||||
hasError: false,
|
||||
userAuthInfos: [],
|
||||
userLabel: null,
|
||||
};
|
||||
|
||||
export const useUserStore = create<UserStoreState & UserStoreAction>()(
|
||||
devtools(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
...defaultState,
|
||||
reset: () => {
|
||||
set({ ...defaultState, isSettled: true });
|
||||
},
|
||||
setIsSettled: isSettled => {
|
||||
set({
|
||||
isSettled,
|
||||
});
|
||||
},
|
||||
setUserInfo: (userInfo: UserInfo | null) => {
|
||||
if (
|
||||
userInfo?.user_id_str &&
|
||||
userInfo?.user_id_str !== get().userInfo?.user_id_str
|
||||
) {
|
||||
fetchUserLabel(userInfo?.user_id_str);
|
||||
}
|
||||
set({
|
||||
userInfo,
|
||||
});
|
||||
},
|
||||
getUserAuthInfos: async () => {
|
||||
const { data = [] } = await DeveloperApi.GetUserAuthList();
|
||||
set({ userAuthInfos: data });
|
||||
},
|
||||
})),
|
||||
{
|
||||
enabled: IS_DEV_MODE,
|
||||
name: 'botStudio.userStore',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const fetchUserLabel = async (id: string) => {
|
||||
const res = await PlaygroundApi.MGetUserBasicInfo({ user_ids: [id] });
|
||||
const userLabel = res?.id_user_info_map?.[id]?.user_label;
|
||||
useUserStore.setState({ userLabel });
|
||||
};
|
||||
114
frontend/packages/foundation/account-base/src/types/index.ts
Normal file
114
frontend/packages/foundation/account-base/src/types/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 当前登录账号的用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
app_id: number;
|
||||
/**
|
||||
* @deprecated 会因为溢出丢失精度,使用 user_id_str
|
||||
*/
|
||||
user_id: number;
|
||||
user_id_str: string;
|
||||
odin_user_type: number;
|
||||
name: string;
|
||||
screen_name: string;
|
||||
avatar_url: string;
|
||||
user_verified: boolean;
|
||||
email?: string;
|
||||
email_collected: boolean;
|
||||
expend_attrs?: Record<string, unknown>;
|
||||
phone_collected: boolean;
|
||||
verified_content: string;
|
||||
verified_agency: string;
|
||||
is_blocked: number;
|
||||
is_blocking: number;
|
||||
bg_img_url: string;
|
||||
gender: number;
|
||||
media_id: number;
|
||||
user_auth_info: string;
|
||||
industry: string;
|
||||
area: string;
|
||||
can_be_found_by_phone: number;
|
||||
mobile: string;
|
||||
birthday: string;
|
||||
description: string;
|
||||
status: number;
|
||||
new_user: number;
|
||||
first_login_app: number;
|
||||
session_key: string;
|
||||
is_recommend_allowed: number;
|
||||
recommend_hint_message: string;
|
||||
followings_count: number;
|
||||
followers_count: number;
|
||||
visit_count_recent: number;
|
||||
skip_edit_profile: number;
|
||||
is_manual_set_user_info: boolean;
|
||||
device_id: number;
|
||||
country_code: number;
|
||||
has_password: number;
|
||||
share_to_repost: number;
|
||||
user_decoration: string;
|
||||
user_privacy_extend: number;
|
||||
old_user_id: number;
|
||||
old_user_id_str: string;
|
||||
sec_user_id: string;
|
||||
sec_old_user_id: string;
|
||||
vcd_account: number;
|
||||
vcd_relation: number;
|
||||
can_bind_visitor_account: boolean;
|
||||
is_visitor_account: boolean;
|
||||
is_only_bind_ins: boolean;
|
||||
user_device_record_status: number;
|
||||
is_kids_mode: number;
|
||||
source: string;
|
||||
is_employee: boolean;
|
||||
passport_enterprise_user_type: number;
|
||||
need_device_create: number;
|
||||
need_ttwid_migration: number;
|
||||
user_auth_status: number;
|
||||
user_safe_mobile_2fa: string;
|
||||
safe_mobile_country_code: number;
|
||||
lite_user_info_string: string;
|
||||
lite_user_info_demotion: number;
|
||||
app_user_info: {
|
||||
user_unique_name?: string;
|
||||
};
|
||||
need_check_bind_status: boolean;
|
||||
bui_audit_info?: {
|
||||
audit_info: {
|
||||
user_unique_name?: string;
|
||||
avatar_url?: string;
|
||||
name?: string;
|
||||
[key: string]: unknown;
|
||||
}; // Record<string, unknown>;
|
||||
// int值。1审核中,2审核通过,3审核不通过
|
||||
audit_status: 1 | 2 | 3;
|
||||
details: Record<string, unknown>;
|
||||
is_auditing: boolean;
|
||||
last_update_time: number;
|
||||
unpass_reason: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录状态
|
||||
* - settling: 登录状态检测中,一般用于首屏,会有一定的延迟
|
||||
* - not_login: 未登录
|
||||
* - logined: 已登录
|
||||
*/
|
||||
export type LoginStatus = 'settling' | 'not_login' | 'logined';
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type OAuth2StateType = 'login' | 'delete_account' | 'oauth';
|
||||
|
||||
export interface OAuth2RedirectConfig {
|
||||
/**
|
||||
* 最终的OAuth2鉴权信息将作为路由参数跳转,这个参数指定目标路由地址,注意在目标路由上使用
|
||||
* useAuthLoginDataRouteFromOAuth2来提取路由参数,并转换成用户中台三方登陆服务(authLogin)的参数;
|
||||
* 默认值为当前路径名称,即不传navigatePath参数时,当前路由一定要注册useAuthLoginDataRouteFromOAuth2才有效
|
||||
*/
|
||||
navigatePath?: string;
|
||||
/**
|
||||
* OAuth2回调后拿到的鉴权信息的使用场景,用于在一些路由组件中区分,不符合对应场景的不能用于消费
|
||||
*/
|
||||
type: OAuth2StateType;
|
||||
/**
|
||||
* 传递给OAuth2服务器的state字段,会在回调时传回,用于恢复网页状态
|
||||
*/
|
||||
|
||||
extra?: {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
origin?: string;
|
||||
[x: string]: string; // 用于安全监测
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
encrypt_state?: string; //加密state,bind_type 为 4时使用
|
||||
};
|
||||
scope?: string;
|
||||
optionalScope?: string;
|
||||
}
|
||||
|
||||
export interface AuthLoginInfo {
|
||||
app_id?: string;
|
||||
response_type?: string;
|
||||
authorize_url?: string;
|
||||
scope?: string;
|
||||
client_id?: string;
|
||||
duration?: string;
|
||||
aid?: string;
|
||||
client_key?: string;
|
||||
}
|
||||
|
||||
export type Connector2Redirect = (
|
||||
oauth2Config: OAuth2RedirectConfig,
|
||||
platform: string,
|
||||
authInfo: AuthLoginInfo,
|
||||
) => void;
|
||||
19
frontend/packages/foundation/account-base/src/typings.d.ts
vendored
Normal file
19
frontend/packages/foundation/account-base/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
|
||||
declare const IS_DEV_MODE: boolean;
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import { setUserInfoContext } from '@coze-arch/logger';
|
||||
|
||||
import { refreshUserInfoBase, logoutBase, checkLoginBase } from '../factory';
|
||||
import { type UserInfo } from '../../types';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
setUserInfoContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../store/user', () => ({
|
||||
useUserStore: {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('factory.ts utility functions', () => {
|
||||
let mockGetState: Mock;
|
||||
let mockSetState: Mock;
|
||||
let mockSetUserInfo: Mock;
|
||||
let mockReset: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSetUserInfo = vi.fn();
|
||||
mockReset = vi.fn();
|
||||
mockGetState = useUserStore.getState as Mock;
|
||||
mockSetState = useUserStore.setState as Mock;
|
||||
mockGetState.mockReturnValue({
|
||||
setUserInfo: mockSetUserInfo,
|
||||
reset: mockReset,
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshUserInfoBase', () => {
|
||||
it('should correctly refresh user information', async () => {
|
||||
const mockUserInfo = {
|
||||
user_id_str: '123',
|
||||
name: 'Test User',
|
||||
} as UserInfo;
|
||||
const mockCheckLogin = vi.fn().mockResolvedValue(mockUserInfo);
|
||||
|
||||
await refreshUserInfoBase(mockCheckLogin);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockCheckLogin).toHaveBeenCalled();
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(mockUserInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutBase', () => {
|
||||
it('should correctly execute logout operation', async () => {
|
||||
const mockLogout = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await logoutBase(mockLogout);
|
||||
|
||||
expect(mockLogout).toHaveBeenCalled();
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLoginBase', () => {
|
||||
const mockSetUserInfoContext = setUserInfoContext as Mock;
|
||||
|
||||
it('should correctly handle successful login state', async () => {
|
||||
const mockUserInfo = {
|
||||
user_id_str: '123',
|
||||
name: 'Test User',
|
||||
} as UserInfo;
|
||||
const mockCheckLoginImpl = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ userInfo: mockUserInfo });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetUserInfoContext).toHaveBeenCalledWith(mockUserInfo);
|
||||
expect(mockSetState).toHaveBeenCalledWith({
|
||||
userInfo: mockUserInfo,
|
||||
isSettled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly handle login error state', async () => {
|
||||
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ hasError: true });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: true });
|
||||
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly handle not logged in state', async () => {
|
||||
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ userInfo: null });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetState).toHaveBeenCalledWith({
|
||||
userInfo: null,
|
||||
isSettled: true,
|
||||
});
|
||||
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
getUserInfo,
|
||||
getLoginStatus,
|
||||
resetUserStore,
|
||||
setUserInfo,
|
||||
getUserLabel,
|
||||
getUserAuthInfos,
|
||||
subscribeUserAuthInfos,
|
||||
usernameRegExpValidate,
|
||||
} from '../index';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
// Mock useUserStore
|
||||
vi.mock('../../store/user', () => ({
|
||||
useUserStore: {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(), // Though not directly used by all utils, good to have for setUserInfo
|
||||
subscribe: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock I18n
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => key), // Simple mock that returns the key
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Utility functions from utils/index.ts', () => {
|
||||
let mockGetState: Mock;
|
||||
let mockSubscribe: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetState = useUserStore.getState as Mock;
|
||||
mockSubscribe = useUserStore.subscribe as Mock;
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
it('should return userInfo from userStore.getState()', () => {
|
||||
const mockUser = { user_id_str: 'testUser', name: 'Test User' };
|
||||
mockGetState.mockReturnValue({ userInfo: mockUser });
|
||||
expect(getUserInfo()).toEqual(mockUser);
|
||||
expect(mockGetState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLoginStatus', () => {
|
||||
it('should return "settling" if store is not settled', () => {
|
||||
mockGetState.mockReturnValue({ isSettled: false, userInfo: null });
|
||||
expect(getLoginStatus()).toBe('settling');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and no userInfo', () => {
|
||||
mockGetState.mockReturnValue({ isSettled: true, userInfo: null });
|
||||
expect(getLoginStatus()).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and userInfo has no user_id_str', () => {
|
||||
mockGetState.mockReturnValue({
|
||||
isSettled: true,
|
||||
userInfo: { name: 'Test' },
|
||||
});
|
||||
expect(getLoginStatus()).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "logined" if store is settled and userInfo has user_id_str', () => {
|
||||
mockGetState.mockReturnValue({
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: '123' },
|
||||
});
|
||||
expect(getLoginStatus()).toBe('logined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetUserStore', () => {
|
||||
it('should call reset on userStore.getState()', () => {
|
||||
const mockReset = vi.fn();
|
||||
mockGetState.mockReturnValue({ reset: mockReset });
|
||||
resetUserStore();
|
||||
expect(mockReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUserInfo', () => {
|
||||
it('should call setUserInfo on userStore.getState() with the provided user info', () => {
|
||||
const mockSetUserInfo = vi.fn();
|
||||
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
|
||||
const newUser = { user_id_str: 'newUser', name: 'New User' };
|
||||
setUserInfo(newUser);
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(newUser);
|
||||
});
|
||||
|
||||
it('should call setUserInfo on userStore.getState() with null', () => {
|
||||
const mockSetUserInfo = vi.fn();
|
||||
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
|
||||
setUserInfo(null);
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserLabel', () => {
|
||||
it('should return userLabel from userStore.getState()', () => {
|
||||
const mockLabel = { label_type: 1, text: 'VIP' };
|
||||
mockGetState.mockReturnValue({ userLabel: mockLabel });
|
||||
expect(getUserLabel()).toEqual(mockLabel);
|
||||
expect(mockGetState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAuthInfos', () => {
|
||||
it('should call getUserAuthInfos on userStore.getState()', async () => {
|
||||
const mockGetUserAuthInfos = vi.fn().mockResolvedValue([]);
|
||||
mockGetState.mockReturnValue({ getUserAuthInfos: mockGetUserAuthInfos });
|
||||
await getUserAuthInfos();
|
||||
expect(mockGetUserAuthInfos).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeUserAuthInfos', () => {
|
||||
it('should call userStore.subscribe with a selector for userAuthInfos and the callback', () => {
|
||||
const callback = vi.fn();
|
||||
const mockUserAuthInfos = [
|
||||
{ auth_type: 'email', auth_key: 'test@example.com' },
|
||||
];
|
||||
// Mock the subscribe implementation to immediately call the listener with selected state
|
||||
mockSubscribe.mockImplementation(
|
||||
(selector, cb) => vi.fn(), // Return unsubscribe function
|
||||
);
|
||||
|
||||
subscribeUserAuthInfos(callback);
|
||||
|
||||
expect(mockSubscribe).toHaveBeenCalledTimes(1);
|
||||
// Check that the selector passed to subscribe correctly extracts userAuthInfos
|
||||
const selectorArg = mockSubscribe.mock.calls[0][0];
|
||||
expect(selectorArg({ userAuthInfos: mockUserAuthInfos })).toEqual(
|
||||
mockUserAuthInfos,
|
||||
);
|
||||
expect(mockSubscribe.mock.calls[0][1]).toBe(callback);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usernameRegExpValidate', () => {
|
||||
it('should return null for valid usernames', () => {
|
||||
expect(usernameRegExpValidate('validUser123')).toBeNull();
|
||||
expect(usernameRegExpValidate('another_valid_user')).toBeNull();
|
||||
expect(usernameRegExpValidate('USER')).toBeNull();
|
||||
expect(usernameRegExpValidate('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return "username_invalid_letter" for usernames with invalid characters', () => {
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('invalid-char')).toBe(
|
||||
'username_invalid_letter',
|
||||
);
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('invalid char')).toBe(
|
||||
'username_invalid_letter',
|
||||
);
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('!@#$%^')).toBe('username_invalid_letter');
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
|
||||
});
|
||||
|
||||
it('should return "username_too_short" for usernames shorter than minLength (4)', () => {
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
|
||||
expect(usernameRegExpValidate('abc')).toBe('username_too_short');
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
|
||||
expect(usernameRegExpValidate('us')).toBe('username_too_short');
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_too_short');
|
||||
});
|
||||
|
||||
it('should return "username_invalid_letter" if invalid char before checking length', () => {
|
||||
// This case tests if the invalid character check takes precedence
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('a-b')).toBe('username_invalid_letter'); // Length 3, but invalid char
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { setUserInfoContext } from '@coze-arch/logger';
|
||||
|
||||
import { type UserInfo } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* 主动触发刷新用户信息
|
||||
* @param checkLogin 登录检查函数
|
||||
*/
|
||||
export const refreshUserInfoBase = async (
|
||||
checkLogin: () => Promise<UserInfo>,
|
||||
) => {
|
||||
useUserStore.setState({
|
||||
hasError: false,
|
||||
});
|
||||
const userInfo = await checkLogin();
|
||||
useUserStore.getState().setUserInfo(userInfo);
|
||||
};
|
||||
|
||||
export const logoutBase = async (logout: () => Promise<void>) => {
|
||||
await logout();
|
||||
useUserStore.getState().reset();
|
||||
};
|
||||
|
||||
export const checkLoginBase = async (
|
||||
checkLoginImpl: () => Promise<{
|
||||
userInfo?: UserInfo;
|
||||
hasError?: boolean;
|
||||
}>,
|
||||
) => {
|
||||
useUserStore.setState({
|
||||
hasError: false,
|
||||
});
|
||||
const { userInfo, hasError } = await checkLoginImpl();
|
||||
if (hasError) {
|
||||
useUserStore.setState({
|
||||
hasError: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (userInfo) {
|
||||
setUserInfoContext(userInfo);
|
||||
}
|
||||
useUserStore.setState({
|
||||
userInfo,
|
||||
isSettled: true,
|
||||
});
|
||||
};
|
||||
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal file
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
import { type UserAuthInfo } from '@coze-arch/idl/developer_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type UserInfo, type LoginStatus } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns UserInfo 用户信息
|
||||
*/
|
||||
export const getUserInfo = () => useUserStore.getState().userInfo;
|
||||
|
||||
/**
|
||||
* 获取登录状态
|
||||
* @returns LoginStatus 登录状态
|
||||
*/
|
||||
export const getLoginStatus = (): LoginStatus => {
|
||||
const state = useUserStore.getState();
|
||||
if (state.isSettled) {
|
||||
return state.userInfo?.user_id_str ? 'logined' : 'not_login';
|
||||
}
|
||||
return 'settling';
|
||||
};
|
||||
|
||||
export const resetUserStore = () => useUserStore.getState().reset();
|
||||
|
||||
export const setUserInfo = (userInfo: UserInfo | null) =>
|
||||
useUserStore.getState().setUserInfo(userInfo);
|
||||
|
||||
export const getUserLabel = () => useUserStore.getState().userLabel;
|
||||
|
||||
export const getUserAuthInfos = () =>
|
||||
useUserStore.getState().getUserAuthInfos();
|
||||
|
||||
export const subscribeUserAuthInfos = (
|
||||
callback: (state: UserAuthInfo[], prev: UserAuthInfo[]) => void,
|
||||
) => useUserStore.subscribe(state => state.userAuthInfos, callback);
|
||||
|
||||
const usernameRegExp = /^[0-9A-Za-z_]+$/;
|
||||
const minLength = 4;
|
||||
export const usernameRegExpValidate = (value: string) => {
|
||||
if (!usernameRegExp.exec(value)) {
|
||||
return I18n.t('username_invalid_letter');
|
||||
}
|
||||
if (value.length < minLength) {
|
||||
return I18n.t('username_too_short');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"target": "ES2020",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "**/__tests__"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/foundation-sdk/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/idl/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../local-storage/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/foundation/account-base/tsconfig.json
Normal file
15
frontend/packages/foundation/account-base/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
frontend/packages/foundation/account-base/tsconfig.misc.json
Normal file
20
frontend/packages/foundation/account-base/tsconfig.misc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["**/__tests__", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
frontend/packages/foundation/account-base/vitest.config.ts
Normal file
26
frontend/packages/foundation/account-base/vitest.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
test: {
|
||||
setupFiles: ['./__tests__/setup-vitest.ts'],
|
||||
includeSource: ['./src/**/__tests__/**'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user