From 1040e3ecfd111b4edd206d28a03ee07c4f4e0bde Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Wed, 18 Mar 2026 20:08:04 +0800 Subject: [PATCH] fix lint and type error --- .../layouts/__tests__/MainLayout.spec.ts | 27 ++++++++++--------- frontend/src/pages/__tests__/login.spec.ts | 13 ++++++--- frontend/src/pages/__tests__/register.spec.ts | 23 +++++++++------- .../src/pages/profile/__tests__/index.spec.ts | 13 ++++++--- .../pages/profile/__tests__/password.spec.ts | 13 ++++++--- frontend/src/pages/profile/index.vue | 11 +++++--- .../src/pages/users/__tests__/index.spec.ts | 2 +- .../pages/users/__tests__/user-form.spec.ts | 2 +- frontend/src/utils/request.ts | 1 + 9 files changed, 67 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/layouts/__tests__/MainLayout.spec.ts b/frontend/src/components/layouts/__tests__/MainLayout.spec.ts index 06d124c..2d607ed 100644 --- a/frontend/src/components/layouts/__tests__/MainLayout.spec.ts +++ b/frontend/src/components/layouts/__tests__/MainLayout.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { mount } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' @@ -56,6 +56,8 @@ describe('MainLayout', () => { LogoutOutlined: { template: '' }, SettingOutlined: { template: '' }, KeyOutlined: { template: '' }, + // Stub dropdown to render overlay inline (avoids Teleport in jsdom) + ADropdown: { template: '
' }, Brand: { template: '
' }, }, }, @@ -143,26 +145,27 @@ describe('MainLayout', () => { const wrapper = await mountLayout() const vm = wrapper.getCurrentComponent() const router = vm.appContext.config.globalProperties.$router + const pushSpy = vi.spyOn(router, 'push') - // Find and click the profile menu item - const menuItems = wrapper.findAll('.ant-dropdown-menu-item, .ant-menu-item') + // ADropdown is stubbed to render overlay inline, so menu items are in the wrapper + const menuItems = wrapper.findAll('.ant-menu-item') const profileItem = menuItems.find((item) => item.text().includes('个人信息')) - if (profileItem) { - await profileItem.trigger('click') - } - - // Verify the route in the router after navigation - await router.push('/profile') - expect(router.currentRoute.value.path).toBe('/profile') + expect(profileItem).toBeTruthy() + await profileItem!.trigger('click') + expect(pushSpy).toHaveBeenCalledWith('/profile') }) it('navigates to /profile/password when clicking password menu item', async () => { const wrapper = await mountLayout() const vm = wrapper.getCurrentComponent() const router = vm.appContext.config.globalProperties.$router + const pushSpy = vi.spyOn(router, 'push') - await router.push('/profile/password') - expect(router.currentRoute.value.path).toBe('/profile/password') + const menuItems = wrapper.findAll('.ant-menu-item') + const passwordItem = menuItems.find((item) => item.text().includes('修改密码')) + expect(passwordItem).toBeTruthy() + await passwordItem!.trigger('click') + expect(pushSpy).toHaveBeenCalledWith('/profile/password') }) it('renders profile breadcrumb on /profile path', async () => { diff --git a/frontend/src/pages/__tests__/login.spec.ts b/frontend/src/pages/__tests__/login.spec.ts index 08fcbe7..48c2bd6 100644 --- a/frontend/src/pages/__tests__/login.spec.ts +++ b/frontend/src/pages/__tests__/login.spec.ts @@ -41,6 +41,11 @@ vi.mock('@/components/Brand.vue', () => ({ default: { template: '
' }, })) +function getSetupState(w: ReturnType) { + // @ts-expect-error setupState is Vue internal, not in public types + return w.getCurrentComponent().setupState +} + describe('Login Page', () => { let wrapper: ReturnType @@ -89,7 +94,7 @@ describe('Login Page', () => { async function submitLogin( fields: { username: string; password: string; remember?: boolean }, ) { - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.username = fields.username vm.formState.password = fields.password if (fields.remember !== undefined) vm.formState.remember = fields.remember @@ -188,7 +193,7 @@ describe('Login Page', () => { it('shows error message on ApiError', async () => { const { api } = await import('@/utils/request') vi.mocked(api.post).mockRejectedValueOnce(new ApiError('用户名或密码错误', 401)) - const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() await submitLogin({ username: 'user', password: 'wrong' }) @@ -200,7 +205,7 @@ describe('Login Page', () => { it('shows network error message on generic error', async () => { const { api } = await import('@/utils/request') vi.mocked(api.post).mockRejectedValueOnce(new Error('Network failure')) - const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() await submitLogin({ username: 'user', password: 'pass' }) @@ -211,7 +216,7 @@ describe('Login Page', () => { it('navigates to register page', async () => { await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.goToRegister() expect(mockPush).toHaveBeenCalledWith('/register') diff --git a/frontend/src/pages/__tests__/register.spec.ts b/frontend/src/pages/__tests__/register.spec.ts index 6cafc1c..06ac84c 100644 --- a/frontend/src/pages/__tests__/register.spec.ts +++ b/frontend/src/pages/__tests__/register.spec.ts @@ -39,6 +39,11 @@ vi.mock('@/components/Brand.vue', () => ({ default: { template: '
' }, })) +function getSetupState(w: ReturnType) { + // @ts-expect-error setupState is Vue internal, not in public types + return w.getCurrentComponent().setupState +} + describe('Register Page', () => { let wrapper: ReturnType @@ -88,11 +93,11 @@ describe('Register Page', () => { it('calls /api/v1/register and redirects to /login on success without token', async () => { const { api } = await import('@/utils/request') vi.mocked(api.post).mockResolvedValueOnce({}) - const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as any) + const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) await mountPage() const store = useUserStore() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.username = 'newuser' vm.formState.email = 'new@test.com' vm.formState.password = 'password123' @@ -122,11 +127,11 @@ describe('Register Page', () => { user: { id: 2, username: 'newuser', email: 'new@test.com', role: 'user', status: 1 }, } vi.mocked(api.post).mockResolvedValueOnce(mockResponse) - vi.spyOn(message, 'success').mockImplementation(() => ({}) as any) + vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) await mountPage() const store = useUserStore() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.username = 'newuser' vm.formState.email = 'new@test.com' vm.formState.password = 'password123' @@ -145,10 +150,10 @@ describe('Register Page', () => { it('shows error message on ApiError', async () => { const { api } = await import('@/utils/request') vi.mocked(api.post).mockRejectedValueOnce(new ApiError('用户名已存在', 409)) - const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.username = 'existing' vm.formState.email = 'e@test.com' vm.formState.password = 'password123' @@ -164,10 +169,10 @@ describe('Register Page', () => { it('shows network error message on generic error', async () => { const { api } = await import('@/utils/request') vi.mocked(api.post).mockRejectedValueOnce(new Error('Network failure')) - const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.username = 'user' vm.formState.email = 'u@test.com' vm.formState.password = 'password123' @@ -182,7 +187,7 @@ describe('Register Page', () => { it('navigates to login page', async () => { await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.goToLogin() expect(mockPush).toHaveBeenCalledWith('/login') diff --git a/frontend/src/pages/profile/__tests__/index.spec.ts b/frontend/src/pages/profile/__tests__/index.spec.ts index 3cb9e22..49a991c 100644 --- a/frontend/src/pages/profile/__tests__/index.spec.ts +++ b/frontend/src/pages/profile/__tests__/index.spec.ts @@ -33,6 +33,11 @@ vi.mock('vue-router', () => ({ useRoute: () => ({ path: '/profile' }), })) +function getSetupState(w: ReturnType) { + // @ts-expect-error setupState is Vue internal, not in public types + return w.getCurrentComponent().setupState +} + describe('Profile Page', () => { let wrapper: ReturnType @@ -74,7 +79,7 @@ describe('Profile Page', () => { it('pre-fills email form field', async () => { await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) expect(vm.formState.email).toBe('test@example.com') }) @@ -86,7 +91,7 @@ describe('Profile Page', () => { const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.email = 'new@example.com' vm.formRef = { validate: vi.fn().mockResolvedValue(true) } await vm.handleSubmit() @@ -103,7 +108,7 @@ describe('Profile Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formRef = { validate: vi.fn().mockResolvedValue(true) } await vm.handleSubmit() await flushPromises() @@ -118,7 +123,7 @@ describe('Profile Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formRef = { validate: vi.fn().mockResolvedValue(true) } await vm.handleSubmit() await flushPromises() diff --git a/frontend/src/pages/profile/__tests__/password.spec.ts b/frontend/src/pages/profile/__tests__/password.spec.ts index 3dbd187..4164cd8 100644 --- a/frontend/src/pages/profile/__tests__/password.spec.ts +++ b/frontend/src/pages/profile/__tests__/password.spec.ts @@ -35,6 +35,11 @@ vi.mock('vue-router', () => ({ useRoute: () => ({ path: '/profile/password' }), })) +function getSetupState(w: ReturnType) { + // @ts-expect-error setupState is Vue internal, not in public types + return w.getCurrentComponent().setupState +} + describe('Password Change Page', () => { let wrapper: ReturnType @@ -78,7 +83,7 @@ describe('Password Change Page', () => { const successSpy = vi.spyOn(message, 'success').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.old_password = 'oldpass' vm.formState.new_password = 'newpass123' vm.formState.new_password_confirmation = 'newpass123' @@ -104,7 +109,7 @@ describe('Password Change Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formState.old_password = 'wrong' vm.formState.new_password = 'newpass123' vm.formState.new_password_confirmation = 'newpass123' @@ -123,7 +128,7 @@ describe('Password Change Page', () => { const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formRef = { validate: vi.fn().mockResolvedValue(true) } await vm.handleSubmit() await flushPromises() @@ -138,7 +143,7 @@ describe('Password Change Page', () => { vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) await mountPage() - const vm = wrapper.getCurrentComponent().setupState + const vm = getSetupState(wrapper) vm.formRef = { validate: vi.fn().mockResolvedValue(true) } await vm.handleSubmit() await flushPromises() diff --git a/frontend/src/pages/profile/index.vue b/frontend/src/pages/profile/index.vue index bde5e7b..8a7a950 100644 --- a/frontend/src/pages/profile/index.vue +++ b/frontend/src/pages/profile/index.vue @@ -18,15 +18,20 @@ const rules = { ], } -onMounted(() => { +onMounted(async () => { if (userStore.user) { formState.email = userStore.user.email || '' } - userStore.fetchCurrentUser().then(() => { + try { + await userStore.fetchCurrentUser() if (userStore.user) { formState.email = userStore.user.email || '' } - }) + } catch (error: unknown) { + if (error instanceof ApiError) { + message.error(error.message) + } + } }) const handleSubmit = async () => { diff --git a/frontend/src/pages/users/__tests__/index.spec.ts b/frontend/src/pages/users/__tests__/index.spec.ts index abd8ff4..7cea096 100644 --- a/frontend/src/pages/users/__tests__/index.spec.ts +++ b/frontend/src/pages/users/__tests__/index.spec.ts @@ -137,7 +137,7 @@ describe('useUserManageStore', () => { it('sets loading to false and shows error message on failure', async () => { const { api } = await import('@/utils/request') vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error')) - const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) + const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as never) const store = useUserManageStore() await store.fetchUsers() diff --git a/frontend/src/pages/users/__tests__/user-form.spec.ts b/frontend/src/pages/users/__tests__/user-form.spec.ts index 7303676..7ee9d98 100644 --- a/frontend/src/pages/users/__tests__/user-form.spec.ts +++ b/frontend/src/pages/users/__tests__/user-form.spec.ts @@ -175,7 +175,7 @@ describe('UserFormModal', () => { // 验证 PUT payload 包含 status 且不包含 ext expect(api.put).toHaveBeenCalledOnce() - const [url, payload] = vi.mocked(api.put).mock.calls[0] + const [url, payload] = vi.mocked(api.put).mock.calls[0]! expect(url).toContain('/api/v1/users/') expect(payload).toHaveProperty('status') expect(payload).not.toHaveProperty('ext') diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index fa4cfe1..a0bb6f7 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -36,6 +36,7 @@ async function request(url: string, options: RequestOptions = {}): url = `${url}${separator}${query}` } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { params: _params, timeout: _timeout, ...fetchOptions } = options // 超时控制