refactor(web): migrate to Vitest and esm (#29974)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-22 16:35:22 +08:00
committed by GitHub
parent 42f7ecda12
commit eabdc5f0eb
268 changed files with 5455 additions and 6307 deletions
@@ -4,12 +4,7 @@ import AppCard, { type AppCardProps } from './index'
import type { App } from '@/models/explore'
import { AppModeEnum } from '@/types/app'
jest.mock('@/app/components/base/app-icon', () => ({
__esModule: true,
default: ({ children }: any) => <div data-testid="app-icon">{children}</div>,
}))
jest.mock('../../app/type-selector', () => ({
vi.mock('../../app/type-selector', () => ({
AppTypeIcon: ({ type }: any) => <div data-testid="app-type-icon">{type}</div>,
}))
@@ -42,7 +37,7 @@ const createApp = (overrides?: Partial<App>): App => ({
})
describe('AppCard', () => {
const onCreate = jest.fn()
const onCreate = vi.fn()
const renderComponent = (props?: Partial<AppCardProps>) => {
const mergedProps: AppCardProps = {
@@ -56,7 +51,7 @@ describe('AppCard', () => {
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should render app info with correct mode label when mode is CHAT', () => {
@@ -9,7 +9,7 @@ import type { CreateAppModalProps } from './index'
let mockTranslationOverrides: Record<string, string | undefined> = {}
jest.mock('react-i18next', () => ({
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
const override = mockTranslationOverrides[key]
@@ -23,22 +23,22 @@ jest.mock('react-i18next', () => ({
},
i18n: {
language: 'en',
changeLanguage: jest.fn(),
changeLanguage: vi.fn(),
},
}),
Trans: ({ children }: { children?: React.ReactNode }) => children,
initReactI18next: {
type: '3rdParty',
init: jest.fn(),
init: vi.fn(),
},
}))
// Avoid heavy emoji dataset initialization during unit tests.
jest.mock('emoji-mart', () => ({
init: jest.fn(),
SearchIndex: { search: jest.fn().mockResolvedValue([]) },
vi.mock('emoji-mart', () => ({
init: vi.fn(),
SearchIndex: { search: vi.fn().mockResolvedValue([]) },
}))
jest.mock('@emoji-mart/data', () => ({
vi.mock('@emoji-mart/data', () => ({
__esModule: true,
default: {
categories: [
@@ -47,11 +47,11 @@ jest.mock('@emoji-mart/data', () => ({
},
}))
jest.mock('next/navigation', () => ({
vi.mock('next/navigation', () => ({
useParams: () => ({}),
}))
jest.mock('@/context/app-context', () => ({
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
userProfile: { email: 'test@example.com' },
langGeniusVersionInfo: { current_version: '0.0.0' },
@@ -73,7 +73,7 @@ let mockPlanType: Plan = Plan.team
let mockUsagePlanInfo: UsagePlanInfo = createPlanInfo(1)
let mockTotalPlanInfo: UsagePlanInfo = createPlanInfo(10)
jest.mock('@/context/provider-context', () => ({
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => {
const withPlan = createMockPlan(mockPlanType)
const withUsage = createMockPlanUsage(mockUsagePlanInfo, withPlan)
@@ -85,8 +85,8 @@ jest.mock('@/context/provider-context', () => ({
type ConfirmPayload = Parameters<CreateAppModalProps['onConfirm']>[0]
const setup = (overrides: Partial<CreateAppModalProps> = {}) => {
const onConfirm = jest.fn<Promise<void>, [ConfirmPayload]>().mockResolvedValue(undefined)
const onHide = jest.fn<void, []>()
const onConfirm = vi.fn<(payload: ConfirmPayload) => Promise<void>>().mockResolvedValue(undefined)
const onHide = vi.fn()
const props: CreateAppModalProps = {
show: true,
@@ -121,7 +121,7 @@ const getAppIconTrigger = (): HTMLElement => {
describe('CreateAppModal', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockTranslationOverrides = {}
mockEnableBilling = false
mockPlanType = Plan.team
@@ -261,11 +261,11 @@ describe('CreateAppModal', () => {
// Shortcut handlers are important for power users and must respect gating rules.
describe('Keyboard Shortcuts', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
vi.useRealTimers()
})
test.each([
@@ -276,7 +276,7 @@ describe('CreateAppModal', () => {
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, ...modifier })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
@@ -288,7 +288,7 @@ describe('CreateAppModal', () => {
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).not.toHaveBeenCalled()
@@ -305,7 +305,7 @@ describe('CreateAppModal', () => {
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).not.toHaveBeenCalled()
@@ -322,7 +322,7 @@ describe('CreateAppModal', () => {
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
@@ -334,7 +334,7 @@ describe('CreateAppModal', () => {
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).not.toHaveBeenCalled()
@@ -361,7 +361,7 @@ describe('CreateAppModal', () => {
})
test('should update icon payload when selecting emoji and confirming', () => {
jest.useFakeTimers()
vi.useFakeTimers()
try {
const { onConfirm } = setup({
appIconType: 'image',
@@ -371,16 +371,19 @@ describe('CreateAppModal', () => {
fireEvent.click(getAppIconTrigger())
const emoji = document.querySelector('em-emoji[id="😀"]')
if (!(emoji instanceof HTMLElement))
throw new Error('Failed to locate emoji option in icon picker')
fireEvent.click(emoji)
// Find the emoji grid by locating the category label, then find the clickable emoji wrapper
const categoryLabel = screen.getByText('people')
const emojiGrid = categoryLabel.nextElementSibling
const clickableEmojiWrapper = emojiGrid?.firstElementChild
if (!(clickableEmojiWrapper instanceof HTMLElement))
throw new Error('Failed to locate emoji wrapper')
fireEvent.click(clickableEmojiWrapper)
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
@@ -392,47 +395,68 @@ describe('CreateAppModal', () => {
})
}
finally {
jest.useRealTimers()
vi.useRealTimers()
}
})
test('should reset emoji icon to initial props when picker is cancelled', () => {
setup({
appIconType: 'emoji',
appIcon: '🤖',
appIconBackground: '#FFEAD5',
})
vi.useFakeTimers()
try {
const { onConfirm } = setup({
appIconType: 'emoji',
appIcon: '🤖',
appIconBackground: '#FFEAD5',
})
expect(document.querySelector('em-emoji[id="🤖"]')).toBeInTheDocument()
// Open picker, select a new emoji, and confirm
fireEvent.click(getAppIconTrigger())
fireEvent.click(getAppIconTrigger())
// Find the emoji grid by locating the category label, then find the clickable emoji wrapper
const categoryLabel = screen.getByText('people')
const emojiGrid = categoryLabel.nextElementSibling
const clickableEmojiWrapper = emojiGrid?.firstElementChild
if (!(clickableEmojiWrapper instanceof HTMLElement))
throw new Error('Failed to locate emoji wrapper')
fireEvent.click(clickableEmojiWrapper)
const emoji = document.querySelector('em-emoji[id="😀"]')
if (!(emoji instanceof HTMLElement))
throw new Error('Failed to locate emoji option in icon picker')
fireEvent.click(emoji)
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
expect(document.querySelector('em-emoji[id="😀"]')).toBeInTheDocument()
// Open picker again and cancel - should reset to initial props
fireEvent.click(getAppIconTrigger())
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' }))
fireEvent.click(getAppIconTrigger())
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' }))
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
expect(document.querySelector('em-emoji[id="🤖"]')).toBeInTheDocument()
// Submit and verify the payload uses the original icon (cancel reverts to props)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
act(() => {
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
const payload = onConfirm.mock.calls[0][0]
expect(payload).toMatchObject({
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFEAD5',
})
}
finally {
vi.useRealTimers()
}
})
})
// Submitting uses a debounced handler and builds a payload from current form state.
describe('Submitting', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
vi.useRealTimers()
})
test('should call onConfirm with emoji payload and hide when create is clicked', () => {
@@ -446,7 +470,7 @@ describe('CreateAppModal', () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
@@ -470,7 +494,7 @@ describe('CreateAppModal', () => {
fireEvent.change(screen.getByPlaceholderText('app.newApp.appDescriptionPlaceholder'), { target: { value: 'Updated description' } })
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(onConfirm).toHaveBeenCalledTimes(1)
@@ -487,7 +511,7 @@ describe('CreateAppModal', () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
const payload = onConfirm.mock.calls[0][0]
@@ -511,7 +535,7 @@ describe('CreateAppModal', () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
const payload = onConfirm.mock.calls[0][0]
@@ -526,7 +550,7 @@ describe('CreateAppModal', () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
const payload = onConfirm.mock.calls[0][0]
@@ -539,7 +563,7 @@ describe('CreateAppModal', () => {
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: 'abc' } })
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
const payload = onConfirm.mock.calls[0][0]
@@ -553,12 +577,12 @@ describe('CreateAppModal', () => {
fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), { target: { value: ' ' } })
act(() => {
jest.advanceTimersByTime(300)
vi.advanceTimersByTime(300)
})
expect(screen.getByText('explore.appCustomize.nameRequired')).toBeInTheDocument()
act(() => {
jest.advanceTimersByTime(6000)
vi.advanceTimersByTime(6000)
})
expect(screen.queryByText('explore.appCustomize.nameRequired')).not.toBeInTheDocument()
expect(onConfirm).not.toHaveBeenCalled()
@@ -1,22 +1,23 @@
import type { Mock } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { AppModeEnum } from '@/types/app'
import { AccessMode } from '@/models/access-control'
// Mock external dependencies BEFORE imports
jest.mock('use-context-selector', () => ({
useContext: jest.fn(),
createContext: jest.fn(() => ({})),
vi.mock('use-context-selector', () => ({
useContext: vi.fn(),
createContext: vi.fn(() => ({})),
}))
jest.mock('@/context/web-app-context', () => ({
useWebAppStore: jest.fn(),
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: vi.fn(),
}))
jest.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: jest.fn(),
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: vi.fn(),
}))
jest.mock('@/service/use-explore', () => ({
useGetInstalledAppAccessModeByAppId: jest.fn(),
useGetInstalledAppParams: jest.fn(),
useGetInstalledAppMeta: jest.fn(),
vi.mock('@/service/use-explore', () => ({
useGetInstalledAppAccessModeByAppId: vi.fn(),
useGetInstalledAppParams: vi.fn(),
useGetInstalledAppMeta: vi.fn(),
}))
import { useContext } from 'use-context-selector'
@@ -46,7 +47,7 @@ import type { InstalledApp as InstalledAppType } from '@/models/explore'
* The internal logic of ChatWithHistory and TextGenerationApp should be tested
* in their own dedicated test files.
*/
jest.mock('@/app/components/share/text-generation', () => ({
vi.mock('@/app/components/share/text-generation', () => ({
__esModule: true,
default: ({ isInstalledApp, installedAppInfo, isWorkflow }: {
isInstalledApp?: boolean
@@ -61,7 +62,7 @@ jest.mock('@/app/components/share/text-generation', () => ({
),
}))
jest.mock('@/app/components/base/chat/chat-with-history', () => ({
vi.mock('@/app/components/base/chat/chat-with-history', () => ({
__esModule: true,
default: ({ installedAppInfo, className }: {
installedAppInfo?: InstalledAppType
@@ -74,11 +75,11 @@ jest.mock('@/app/components/base/chat/chat-with-history', () => ({
}))
describe('InstalledApp', () => {
const mockUpdateAppInfo = jest.fn()
const mockUpdateWebAppAccessMode = jest.fn()
const mockUpdateAppParams = jest.fn()
const mockUpdateWebAppMeta = jest.fn()
const mockUpdateUserCanAccessApp = jest.fn()
const mockUpdateAppInfo = vi.fn()
const mockUpdateWebAppAccessMode = vi.fn()
const mockUpdateAppParams = vi.fn()
const mockUpdateWebAppMeta = vi.fn()
const mockUpdateUserCanAccessApp = vi.fn()
const mockInstalledApp = {
id: 'installed-app-123',
@@ -116,22 +117,22 @@ describe('InstalledApp', () => {
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Mock useContext
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [mockInstalledApp],
isFetchingInstalledApps: false,
})
// Mock useWebAppStore
;(useWebAppStore as unknown as jest.Mock).mockImplementation((
;(useWebAppStore as unknown as Mock).mockImplementation((
selector: (state: {
updateAppInfo: jest.Mock
updateWebAppAccessMode: jest.Mock
updateAppParams: jest.Mock
updateWebAppMeta: jest.Mock
updateUserCanAccessApp: jest.Mock
updateAppInfo: Mock
updateWebAppAccessMode: Mock
updateAppParams: Mock
updateWebAppMeta: Mock
updateUserCanAccessApp: Mock
}) => unknown,
) => {
const state = {
@@ -145,25 +146,25 @@ describe('InstalledApp', () => {
})
// Mock service hooks with default success states
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
isFetching: false,
data: mockWebAppAccessMode,
error: null,
})
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: false,
data: mockAppParams,
error: null,
})
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
;(useGetInstalledAppMeta as Mock).mockReturnValue({
isFetching: false,
data: mockAppMeta,
error: null,
})
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: mockUserCanAccessApp,
error: null,
})
@@ -176,7 +177,7 @@ describe('InstalledApp', () => {
})
it('should render loading state when fetching app params', () => {
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: true,
data: null,
error: null,
@@ -188,7 +189,7 @@ describe('InstalledApp', () => {
})
it('should render loading state when fetching app meta', () => {
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
;(useGetInstalledAppMeta as Mock).mockReturnValue({
isFetching: true,
data: null,
error: null,
@@ -200,7 +201,7 @@ describe('InstalledApp', () => {
})
it('should render loading state when fetching web app access mode', () => {
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
isFetching: true,
data: null,
error: null,
@@ -212,7 +213,7 @@ describe('InstalledApp', () => {
})
it('should render loading state when fetching installed apps', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [mockInstalledApp],
isFetchingInstalledApps: true,
})
@@ -223,7 +224,7 @@ describe('InstalledApp', () => {
})
it('should render app not found (404) when installedApp does not exist', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
@@ -236,7 +237,7 @@ describe('InstalledApp', () => {
describe('Error States', () => {
it('should render error when app params fails to load', () => {
const error = new Error('Failed to load app params')
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: false,
data: null,
error,
@@ -248,7 +249,7 @@ describe('InstalledApp', () => {
it('should render error when app meta fails to load', () => {
const error = new Error('Failed to load app meta')
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
;(useGetInstalledAppMeta as Mock).mockReturnValue({
isFetching: false,
data: null,
error,
@@ -260,7 +261,7 @@ describe('InstalledApp', () => {
it('should render error when web app access mode fails to load', () => {
const error = new Error('Failed to load access mode')
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
isFetching: false,
data: null,
error,
@@ -272,7 +273,7 @@ describe('InstalledApp', () => {
it('should render error when user access check fails', () => {
const error = new Error('Failed to check user access')
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: null,
error,
})
@@ -282,7 +283,7 @@ describe('InstalledApp', () => {
})
it('should render no permission (403) when user cannot access app', () => {
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: { result: false },
error: null,
})
@@ -308,7 +309,7 @@ describe('InstalledApp', () => {
mode: AppModeEnum.ADVANCED_CHAT,
},
}
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [advancedChatApp],
isFetchingInstalledApps: false,
})
@@ -326,7 +327,7 @@ describe('InstalledApp', () => {
mode: AppModeEnum.AGENT_CHAT,
},
}
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [agentChatApp],
isFetchingInstalledApps: false,
})
@@ -344,7 +345,7 @@ describe('InstalledApp', () => {
mode: AppModeEnum.COMPLETION,
},
}
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [completionApp],
isFetchingInstalledApps: false,
})
@@ -362,7 +363,7 @@ describe('InstalledApp', () => {
mode: AppModeEnum.WORKFLOW,
},
}
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [workflowApp],
isFetchingInstalledApps: false,
})
@@ -377,7 +378,7 @@ describe('InstalledApp', () => {
it('should use id prop to find installed app', () => {
const app1 = { ...mockInstalledApp, id: 'app-1' }
const app2 = { ...mockInstalledApp, id: 'app-2' }
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [app1, app2],
isFetchingInstalledApps: false,
})
@@ -419,7 +420,7 @@ describe('InstalledApp', () => {
})
it('should update app info to null when installedApp is not found', async () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
@@ -464,7 +465,7 @@ describe('InstalledApp', () => {
})
it('should update user can access app to false when result is false', async () => {
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: { result: false },
error: null,
})
@@ -477,7 +478,7 @@ describe('InstalledApp', () => {
})
it('should update user can access app to false when data is null', async () => {
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: null,
error: null,
})
@@ -490,7 +491,7 @@ describe('InstalledApp', () => {
})
it('should not update app params when data is null', async () => {
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: false,
data: null,
error: null,
@@ -506,7 +507,7 @@ describe('InstalledApp', () => {
})
it('should not update app meta when data is null', async () => {
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
;(useGetInstalledAppMeta as Mock).mockReturnValue({
isFetching: false,
data: null,
error: null,
@@ -522,7 +523,7 @@ describe('InstalledApp', () => {
})
it('should not update access mode when data is null', async () => {
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
isFetching: false,
data: null,
error: null,
@@ -540,7 +541,7 @@ describe('InstalledApp', () => {
describe('Edge Cases', () => {
it('should handle empty installedApps array', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
@@ -558,7 +559,7 @@ describe('InstalledApp', () => {
name: 'Other App',
},
}
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [otherApp, mockInstalledApp],
isFetchingInstalledApps: false,
})
@@ -572,7 +573,7 @@ describe('InstalledApp', () => {
it('should handle rapid id prop changes', async () => {
const app1 = { ...mockInstalledApp, id: 'app-1' }
const app2 = { ...mockInstalledApp, id: 'app-2' }
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [app1, app2],
isFetchingInstalledApps: false,
})
@@ -597,7 +598,7 @@ describe('InstalledApp', () => {
})
it('should call service hooks with null when installedApp is not found', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
@@ -616,7 +617,7 @@ describe('InstalledApp', () => {
describe('Render Priority', () => {
it('should show error before loading state', () => {
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: true,
data: null,
error: new Error('Some error'),
@@ -628,12 +629,12 @@ describe('InstalledApp', () => {
})
it('should show error before permission check', () => {
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: false,
data: null,
error: new Error('Params error'),
})
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: { result: false },
error: null,
})
@@ -645,11 +646,11 @@ describe('InstalledApp', () => {
})
it('should show permission error before 404', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
;(useGetUserCanAccessApp as Mock).mockReturnValue({
data: { result: false },
error: null,
})
@@ -661,11 +662,11 @@ describe('InstalledApp', () => {
})
it('should show loading before 404', () => {
;(useContext as jest.Mock).mockReturnValue({
;(useContext as Mock).mockReturnValue({
installedApps: [],
isFetchingInstalledApps: false,
})
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
;(useGetInstalledAppParams as Mock).mockReturnValue({
isFetching: true,
data: null,
error: null,