test(web): restore feature owners in browser specs (#46025)

This commit is contained in:
Stephen Zhou authored and GitHub committed 2026-11-11 08:23:56 +00:00
1 parent 530a8575e4
commit 012cbd902f
8 files changed
+190 -249

No files matched your search

@@ -8,6 +8,7 @@ import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { AgentPermission } from '@/features/agent-v2/acl'
import agentRoster from '@/i18n/locales/en-US/agent-roster.json'
import app from '@/i18n/locales/en-US/app.json'
import common from '@/i18n/locales/en-US/common.json'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { QueryClientTestProvider } from '@/test/console/query-provider'
@@ -19,16 +20,8 @@ const api = vi.hoisted(() => ({
agents: vi.fn<typeof consoleClient.agent.get>(),
delete: vi.fn<typeof consoleClient.agent.byAgentId.delete>(),
}))
vi.mock('react-i18next', async (importOriginal) => importOriginal())
vi.mock('@/features/agent-v2/permissions', () => ({
useCanCreateAgents: () => false,
useCanImportAgents: () => false,
}))
vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => path }))
vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: () => '' }) }))
vi.mock('@/app/components/app/use-export-app-dsl', () => ({
useExportAppDsl: () => ({ exportAppDsl: vi.fn(), isExporting: false }),
}))
vi.unmock('react-i18next')
// boundary: generated Console transport is the server edge below the real roster owners.
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
const { withConsoleResponses } = await import('@/test/console/service-responses')
@@ -80,7 +73,7 @@ async function setup(initialAgents: AgentAppPartial[]) {
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { agentRoster, common } },
resources: { 'en-US': { agentRoster, app, common } },
keySeparator: false,
interpolation: { escapeValue: false },
})
@@ -97,6 +90,9 @@ async function setup(initialAgents: AgentAppPartial[]) {
</QueryClientTestProvider>,
)
await expect.element(screen.getByRole('list', { name: 'Agent list', exact: true })).toBeVisible()
await expect
.element(screen.getByRole('link', { name: 'Learn more about Agent' }))
.toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/build/new-agent/overview')
const actions = (name: string) => screen.getByRole('button', { name: `More actions for ${name}` })
const openDelete = async (name: string) => {
await screen.getByRole('link', { name, exact: true }).hover()
@@ -1,11 +1,17 @@
import { zAgentAppComposerResponse } from '@dify/contracts/api/console/agent/zod.gen'
import { QueryClientProvider } from '@tanstack/react-query'
import { createInstance } from 'i18next'
import { createStore, Provider } from 'jotai'
import { useState } from 'react'
import { I18nextProvider } from 'react-i18next'
import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import agentV2 from '@/i18n/locales/en-US/agent-v-2.json'
import billing from '@/i18n/locales/en-US/billing.json'
import common from '@/i18n/locales/en-US/common.json'
import workflowHistory from '@/i18n/locales/en-US/workflow-history.json'
import { consoleQuery } from '@/service/console'
import { createSystemFeaturesFixture } from '@/test/console/system-features'
import { createAgentFixture } from '@/test/fixtures/agent'
@@ -13,7 +19,8 @@ import { createAppSiteFixture } from '@/test/fixtures/app'
import { createTestQueryClient } from '@/test/query-client'
import { AgentConfigurePublishBar } from '../publish-bar'
vi.mock('@/app/notifications', () => ({ toast: { error: vi.fn() } }))
vi.unmock('react-i18next')
// boundary: generated Console transport supplies the referenced-workflow server response.
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
const { withConsoleResponses } = await import('@/test/console/service-responses')
@@ -25,16 +32,17 @@ vi.mock('@/service/console', async (importOriginal) => {
}
})
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({ formatTimeFromNow: () => 'just now' }),
}))
vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: () => '' }) }))
vi.mock('../../version-restore', () => ({ AgentVersionRestore: () => null }))
const successTitle = 'agentV2.agentDetail.configure.publishSuccess.updateTitle'
const dismissLabel = 'agentV2.agentDetail.configure.publishSuccess.dismiss'
const successTitle = 'Changes published'
const dismissLabel = 'Dismiss publish guidance'
async function setup() {
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { agentV2, billing, common, workflowHistory } },
keySeparator: false,
interpolation: { escapeValue: false },
})
const queryClient = createTestQueryClient()
const store = createStore()
const input = { params: { agent_id: 'agent-1' } }
@@ -98,7 +106,11 @@ async function setup() {
</QueryClientProvider>
)
}
const screen = await render(<Harness />)
const screen = await render(
<I18nextProvider i18n={i18n}>
<Harness />
</I18nextProvider>,
)
return { screen, completePublish, queryClient }
}
@@ -110,15 +122,15 @@ it('preserves keyboard focus through loading, publication, and dismissal', async
const editor = screen.getByRole('textbox', { name: 'Other editor' })
await editor.click()
await userEvent.tab()
await expect.element(screen.getByRole('button', { name: /versionHistory$/ })).toHaveFocus()
await expect.element(screen.getByRole('button', { name: 'Open version history' })).toHaveFocus()
await userEvent.tab()
const publish = screen.getByRole('button', { name: /agentV2.agentDetail.publish/ })
const publish = screen.getByRole('button', { name: 'Publish', exact: true })
await expect.element(publish).toHaveFocus()
const bar = publish.element().closest('[tabindex="-1"]')!
const liveRegion = bar.querySelector('[role="status"][aria-atomic="true"]')!
expect(liveRegion.textContent).toBe('')
await userEvent.keyboard('{Enter}')
await expect.element(screen.getByRole('button', { name: /publishing/ })).toHaveFocus()
await expect.element(screen.getByRole('button', { name: 'Publishing…' })).toHaveFocus()
completePublish()
await expect.poll(() => liveRegion.textContent).toBe(successTitle)
await expect.poll(() => document.activeElement).toBe(bar)
@@ -127,15 +139,15 @@ it('preserves keyboard focus through loading, publication, and dismissal', async
const dismiss = screen.getByRole('button', { name: dismissLabel })
await expect.element(dismiss).toHaveFocus()
await userEvent.tab()
await expect.element(screen.getByRole('link', { name: /accessMethods$/ })).toHaveFocus()
await expect.element(screen.getByRole('link', { name: 'View all access methods' })).toHaveFocus()
await userEvent.tab()
await expect.element(screen.getByRole('link', { name: /openWebApp$/ })).toHaveFocus()
await expect.element(screen.getByRole('link', { name: 'Open Web App' })).toHaveFocus()
await userEvent.tab({ shift: true })
await userEvent.tab({ shift: true })
await userEvent.keyboard('{Enter}')
await expect.poll(() => document.activeElement).toBe(bar)
expect(liveRegion.textContent).toBe('')
await userEvent.tab()
await expect.element(screen.getByRole('button', { name: /versionHistory$/ })).toHaveFocus()
await expect.element(screen.getByRole('button', { name: 'Open version history' })).toHaveFocus()
queryClient.clear()
})
@@ -2,9 +2,14 @@ import type {
AgentLogListResponse,
AgentLogSourceListResponse,
} from '@dify/contracts/api/console/agent/types.gen'
import { QueryClient } from '@tanstack/react-query'
import { createInstance } from 'i18next'
import { I18nextProvider } from 'react-i18next'
import { page } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import agentV2 from '@/i18n/locales/en-US/agent-v-2.json'
import appLog from '@/i18n/locales/en-US/app-log.json'
import common from '@/i18n/locales/en-US/common.json'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { QueryClientTestProvider } from '@/test/console/query-provider'
import { AgentLogsPage } from '../page'
@@ -13,19 +18,8 @@ const mocks = vi.hoisted(() => ({
sources: vi.fn(),
}))
vi.mock('react-i18next', async () => {
const { createReactI18nextMock } = await import('@/test/i18n-mock')
const { default: agentV2 } = await import('@/i18n/locales/en-US/agent-v-2.json')
const { default: appLog } = await import('@/i18n/locales/en-US/app-log.json')
const { default: common } = await import('@/i18n/locales/en-US/common.json')
return createReactI18nextMock({ ...agentV2, ...appLog, ...common })
})
vi.mock('@/context/i18n', async (importOriginal) => ({
...(await importOriginal<typeof import('@/context/i18n')>()),
useDocLink: () => (path: string) => path,
}))
vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: () => '' }) }))
vi.unmock('react-i18next')
// boundary: generated Console transport supplies the log-list server responses.
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
const { withConsoleResponses } = await import('@/test/console/service-responses')
@@ -52,18 +46,30 @@ it('keeps log filters and pagination reachable at a 320 by 256 viewport', async
await page.viewport(320, 256)
mocks.logs.mockResolvedValue(emptyLogs)
mocks.sources.mockResolvedValue(emptySources)
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { agentV2, appLog, common } },
keySeparator: false,
interpolation: { escapeValue: false },
})
const { queryClient } = createConsoleQueryWrapper()
try {
const screen = await render(
<QueryClientTestProvider queryClient={queryClient}>
<div style={{ width: 224, height: 256 }}>
<AgentLogsPage agentId="agent-1" />
</div>
<I18nextProvider i18n={i18n}>
<div style={{ width: 224, height: 256 }}>
<AgentLogsPage agentId="agent-1" />
</div>
</I18nextProvider>
</QueryClientTestProvider>,
)
await expect.element(screen.getByText('No logs found')).toBeVisible()
await expect
.element(screen.getByRole('link', { name: 'Learn more' }))
.toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/monitor/logs')
const section = screen.getByRole('region', { name: 'Logs' }).first().element()
const panel = section.firstElementChild as HTMLElement
const controls = [
@@ -1,9 +1,14 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import { createInstance } from 'i18next'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { I18nextProvider } from 'react-i18next'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
import common from '@/i18n/locales/en-US/common.json'
import contacts from '@/i18n/locales/en-US/contacts.json'
import { seedSystemFeatures } from '@/test/console/query-data'
import { QueryClientTestProvider } from '@/test/console/query-provider'
import { ContactsImPlatformRuntimeProvider } from '../composition'
import { ContactsImPlatformManagementSurface } from '../management-surface'
import { channelSummary } from './fixtures/channel-client'
@@ -12,6 +17,8 @@ import { channel, defaultResponse, json } from './fixtures/http'
const network = vi.hoisted(() => ({
handle: vi.fn<(request: Request) => Promise<Response> | Response>(),
}))
vi.unmock('react-i18next')
// boundary: the HTTP adapter is the server edge below the real contacts runtime.
vi.mock('@/service/base', async () => {
const { httpErrorFromResponse } = await import('@/service/http-error')
return {
@@ -22,10 +29,6 @@ vi.mock('@/service/base', async () => {
},
}
})
vi.mock('@/utils/client', () => ({ isClient: true, isServer: false }))
vi.mock('@/context/i18n', () => ({
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
}))
// These surfaces previously lost their scrims when moved under Settings' transparent modal.
// Independent component tests miss that nested-dialog condition. Real rendering must prove
@@ -67,23 +70,32 @@ async function setup() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
})
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { common, contacts } },
keySeparator: false,
interpolation: { escapeValue: false },
})
seedSystemFeatures(queryClient, { deployment_edition: 'CLOUD' })
await render(
<QueryClientProvider client={queryClient}>
<NuqsTestingAdapter hasMemory>
<ContactsImPlatformRuntimeProvider
organization={{
canManage: true,
organizationId: 'workspace-1',
workspaceId: 'workspace-1',
}}
>
<MenuDialog title="Workspace settings" onClose={vi.fn()}>
<ContactsImPlatformManagementSurface />
</MenuDialog>
</ContactsImPlatformRuntimeProvider>
</NuqsTestingAdapter>
</QueryClientProvider>,
<QueryClientTestProvider queryClient={queryClient}>
<I18nextProvider i18n={i18n}>
<NuqsTestingAdapter hasMemory>
<ContactsImPlatformRuntimeProvider
organization={{
canManage: true,
organizationId: 'workspace-1',
workspaceId: 'workspace-1',
}}
>
<MenuDialog title="Workspace settings" onClose={vi.fn()}>
<ContactsImPlatformManagementSurface />
</MenuDialog>
</ContactsImPlatformRuntimeProvider>
</NuqsTestingAdapter>
</I18nextProvider>
</QueryClientTestProvider>,
)
await expect.element(page.getByRole('group', { name: 'Feishu', exact: true })).toBeVisible()
expect(visibleViewportScrims()).toBe(0)
@@ -97,40 +109,43 @@ it.each(['IM configuration', 'Email configuration', 'replacement', 'deletion', '
const client = await setup()
try {
let role: 'dialog' | 'alertdialog' = 'dialog'
let title = /contacts\.imPlatform\.bindingDialog\.configureTitle/
let title = /^Configure /i
if (surface === 'IM configuration' || surface === 'Email configuration') {
const provider = surface === 'IM configuration' ? 'Feishu' : 'Email'
await page
.getByRole('group', { name: provider, exact: true })
.getByRole('button', { name: /contacts\.imPlatform\.action\.configureChannel/ })
.getByRole('button', { name: `Configure ${provider}` })
.click()
if (surface === 'Email configuration') title = /contacts\.imPlatform\.email\.title/
title = new RegExp(`^Configure ${provider}$`)
} else if (surface === 'replacement') {
await page
.getByRole('group', { name: 'Slack', exact: true })
.getByRole('button', { name: /contacts\.imPlatform\.action\.replace/ })
.getByRole('button', { name: 'Slack — Replace' })
.click()
role = 'alertdialog'
title = /contacts\.imPlatform\.replacement\.title/
title = /^Replace .+ with .+\?$/
} else if (surface === 'deletion') {
await page
.getByRole('group', { name: 'Feishu', exact: true })
.getByRole('button', { name: /contacts\.imPlatform\.action\.deleteChannel/ })
.getByRole('button', { name: 'Delete Feishu' })
.click()
role = 'alertdialog'
title = /contacts\.imPlatform\.delete\.title/
title = /^Delete Feishu channel\?$/
} else {
await page.getByRole('button', { name: 'contacts.imPlatform.action.viewDetails' }).click()
title = /contacts\.imPlatform\.details\.title/
await page.getByRole('button', { name: 'View details' }).click()
title = /^Sync details$/
}
const popup = page.getByRole(role, { name: title })
await expect.element(popup).toBeVisible()
await expect.poll(visibleViewportScrims).toBe(1)
if (surface === 'IM configuration') {
await popup.getByRole('button', { name: 'contacts.imPlatform.bindingDialog.back' }).click()
if (surface === 'Email configuration') {
await expect
.element(popup.getByLabelText('contacts.imPlatform.bindingDialog.field.appId'))
.toBeVisible()
.element(popup.getByRole('link', { name: 'Learn more' }))
.toHaveAttribute('href', 'https://docs.dify.ai/en/cloud/use-dify/nodes/human-input')
}
if (surface === 'IM configuration') {
await popup.getByRole('button', { name: 'Back' }).click()
await expect.element(popup.getByLabelText('App ID')).toBeVisible()
expect(visibleViewportScrims()).toBe(1)
}
await userEvent.keyboard('{Escape}')
@@ -1,7 +1,10 @@
import type { ComponentProps } from 'react'
import { createInstance } from 'i18next'
import { I18nextProvider } from 'react-i18next'
import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { IpPolicyDialog } from '@/features/network-access/policy-editor'
import common from '@/i18n/locales/en-US/common.json'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { createDeferred } from '@/test/deferred'
@@ -9,6 +12,7 @@ const requests = vi.hoisted(() => ({
currentIp: vi.fn<() => Promise<{ client_ip: string }>>(),
}))
// boundary: the Browser Console transport is the current-IP server edge.
vi.mock('@/service/console/browser', () => ({
consoleBrowserLink: {
call: (path: readonly string[]) => {
@@ -19,22 +23,26 @@ vi.mock('@/service/console/browser', () => ({
},
}))
vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<typeof import('react-i18next')>()
const { createReactI18nextMock } = await import('@/test/i18n-mock')
const { default: translations } = await import('@/i18n/locales/en-US/common.json')
return { ...actual, ...createReactI18nextMock(translations) }
})
vi.unmock('react-i18next')
beforeEach(() => {
requests.currentIp.mockReset()
})
async function renderEditor(props: Partial<ComponentProps<typeof IpPolicyDialog>> = {}) {
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { common } },
keySeparator: false,
interpolation: { escapeValue: false },
})
const { queryClient, wrapper: ConsoleProvider } = createConsoleQueryWrapper()
const screen = await render(
<ConsoleProvider>
<IpPolicyDialog open mode="edit" initialName="Office" onOpenChange={vi.fn()} {...props} />
<I18nextProvider i18n={i18n}>
<IpPolicyDialog open mode="edit" initialName="Office" onOpenChange={vi.fn()} {...props} />
</I18nextProvider>
</ConsoleProvider>,
)
return { screen, queryClient }
@@ -1,5 +1,12 @@
import { createInstance } from 'i18next'
import { I18nextProvider } from 'react-i18next'
import { page } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import common from '@/i18n/locales/en-US/common.json'
import knowledgeCreate from '@/i18n/locales/en-US/knowledge-create.json'
import knowledgeSettings from '@/i18n/locales/en-US/knowledge-settings.json'
import knowledgeSpace from '@/i18n/locales/en-US/knowledge-space.json'
import modelProvider from '@/i18n/locales/en-US/model-provider.json'
import { consoleQuery } from '@/service/console'
import {
createCatalogModelFixture,
@@ -11,41 +18,26 @@ import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { QueryTestingAdapter } from '@/test/query-state-testing-adapter'
import { CreateKnowledgePage } from '../page'
// boundary: the Next router is unavailable in the isolated Browser test runtime.
vi.mock('@/next/navigation', () => ({
useRouter: () => ({ replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}))
// boundary: Browser setup does not load the product namespaces needed for translated model labels.
vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
const { createReactI18nextMock } = await import('@/test/i18n-mock')
const { default: creation } = await import('@/i18n/locales/en-US/knowledge-create.json')
const { default: space } = await import('@/i18n/locales/en-US/knowledge-space.json')
const { default: settings } = await import('@/i18n/locales/en-US/knowledge-settings.json')
const { default: provider } = await import('@/i18n/locales/en-US/model-provider.json')
const { default: common } = await import('@/i18n/locales/en-US/common.json')
return {
...actual,
...createReactI18nextMock(
Object.fromEntries(
Object.entries({
knowledgeCreate: creation,
knowledgeSpace: space,
knowledgeSettings: settings,
modelProvider: provider,
common,
}).flatMap(([namespace, translations]) =>
Object.entries(translations).map(([key, value]) => [`${namespace}.${key}`, value]),
),
),
),
}
})
vi.unmock('react-i18next')
it('keeps model menus usable inside the scrolling creation form on a small viewport', async () => {
// Real layout and hit testing cover the scrolling form, fixed footer, and portalled menu.
await page.viewport(640, 600)
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: {
'en-US': { common, knowledgeCreate, knowledgeSettings, knowledgeSpace, modelProvider },
},
keySeparator: false,
interpolation: { escapeValue: false },
})
const { queryClient, wrapper: QueryProvider } = createConsoleQueryWrapper({
systemFeatures: {
deployment_edition: 'ENTERPRISE',
@@ -98,9 +90,11 @@ it('keeps model menus usable inside the scrolling creation form on a small viewp
})
const screen = await render(
<QueryProvider>
<QueryTestingAdapter>
<CreateKnowledgePage />
</QueryTestingAdapter>
<I18nextProvider i18n={i18n}>
<QueryTestingAdapter>
<CreateKnowledgePage />
</QueryTestingAdapter>
</I18nextProvider>
</QueryProvider>,
)
const settings = screen.getByRole('button', { name: 'Model settings', exact: true })
@@ -1,47 +1,20 @@
import type { ReactNode } from 'react'
import type { SyncPolicyValue } from '../sync-policy-field'
import { createInstance } from 'i18next'
import { useState } from 'react'
import { I18nextProvider } from 'react-i18next'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import commonEn from '@/i18n/locales/en-US/common.json'
import knowledgeSourcesEn from '@/i18n/locales/en-US/knowledge-sources.json'
import knowledgeSpaceEn from '@/i18n/locales/en-US/knowledge-space.json'
import commonZh from '@/i18n/locales/zh-Hans/common.json'
import knowledgeSourcesZh from '@/i18n/locales/zh-Hans/knowledge-sources.json'
import knowledgeSpaceZh from '@/i18n/locales/zh-Hans/knowledge-space.json'
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
import { SyncPolicyField } from '../sync-policy-field'
const i18nLanguage = vi.hoisted(() => ({ current: 'en-US' }))
// boundary: Browser setup cannot switch the i18n language while preserving these interval labels.
vi.mock('react-i18next', async () => {
const { createReactI18nextMock } = await import('@/test/i18n-mock')
const mock = createReactI18nextMock({
'common.operation.cancel': 'Cancel',
'knowledgeSources.syncPolicy': 'Sync policy',
'knowledgeSources.syncPolicyApply': 'Apply',
'knowledgeSources.syncPolicyCustom': 'Custom interval',
'knowledgeSources.syncPolicyCustomDescription':
'First sync runs right after creation, then {{interval}}.',
'knowledgeSources.syncPolicyCustomHelp': 'From 1 hour to 30 days. Will sync {{interval}}.',
'knowledgeSources.syncPolicyCustomValue': 'Custom · {{interval}}',
'knowledgeSources.syncPolicyDaily': 'Every 24 hours',
'knowledgeSources.syncPolicyEditCustom': 'Edit custom interval',
'knowledgeSources.syncPolicyEvery': 'Every',
'knowledgeSources.syncPolicyEveryValue': 'Every {{interval}}',
'knowledgeSources.syncPolicyManual': 'Manual sync',
'knowledgeSpace.syncPolicyUnit.days': 'days',
'knowledgeSpace.syncPolicyUnit.hours': 'hours',
})
return {
...mock,
useTranslation: ((...args: Parameters<typeof mock.useTranslation>) => {
const result = mock.useTranslation(...args)
return {
...result,
i18n: {
...result.i18n,
language: i18nLanguage.current,
resolvedLanguage: i18nLanguage.current,
},
}
}) as typeof mock.useTranslation,
}
})
vi.unmock('react-i18next')
function SyncPolicyFieldHarness() {
const [value, setValue] = useState<SyncPolicyValue>({ mode: 'interval' })
@@ -53,16 +26,38 @@ function SyncPolicyFieldHarness() {
)
}
async function renderWithI18n(node: ReactNode, locale: 'en-US' | 'zh-Hans' = 'en-US') {
const i18n = createInstance()
await i18n.init({
lng: locale,
fallbackLng: false,
resources: {
'en-US': {
common: commonEn,
knowledgeSources: knowledgeSourcesEn,
knowledgeSpace: knowledgeSpaceEn,
},
'zh-Hans': {
common: commonZh,
knowledgeSources: knowledgeSourcesZh,
knowledgeSpace: knowledgeSpaceZh,
},
},
keySeparator: false,
interpolation: { escapeValue: false },
})
return render(<I18nextProvider i18n={i18n}>{node}</I18nextProvider>)
}
describe('SyncPolicyField', () => {
beforeEach(async () => {
await page.viewport(640, 600)
onTestFinished(guardUnexpectedFetch())
i18nLanguage.current = 'en-US'
})
it('shows sync timing for default and preset intervals but not manual sync', async () => {
const user = userEvent
await render(<SyncPolicyFieldHarness />)
await renderWithI18n(<SyncPolicyFieldHarness />)
const trigger = page.getByRole('combobox', { name: 'Sync policy' })
await expect.element(trigger).toHaveTextContent('Every 24 hours')
@@ -77,31 +72,30 @@ describe('SyncPolicyField', () => {
.toBeVisible()
await user.click(trigger)
await user.click(page.getByRole('option', { name: 'Manual sync' }))
await expect.element(trigger).toHaveTextContent('Manual sync')
await user.click(page.getByRole('option', { name: 'Manual only' }))
await expect.element(trigger).toHaveTextContent('Manual only')
await expect
.element(page.getByText(/^First sync runs right after creation/))
.not.toBeInTheDocument()
})
it('separates numbers from units in Simplified Chinese interval labels', async () => {
i18nLanguage.current = 'zh-Hans'
const user = userEvent
await render(<SyncPolicyFieldHarness />)
await renderWithI18n(<SyncPolicyFieldHarness />, 'zh-Hans')
await user.click(page.getByRole('combobox', { name: 'Sync policy' }))
await user.click(page.getByRole('combobox', { name: '同步策略' }))
await expect.element(page.getByRole('option', { name: 'Every 6 小时' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: 'Every 3 天' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: '每 6 小时' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: '每 3 天' })).toBeInTheDocument()
})
it('offers the designed presets and applies a custom day interval explicitly', async () => {
const user = userEvent
await render(<SyncPolicyFieldHarness />)
await renderWithI18n(<SyncPolicyFieldHarness />)
const trigger = page.getByRole('combobox', { name: 'Sync policy' })
await user.click(trigger)
await expect.element(page.getByRole('option', { name: 'Manual sync' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: 'Manual only' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: 'Every 6 hours' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: 'Every 12 hours' })).toBeInTheDocument()
await expect.element(page.getByRole('option', { name: 'Every 24 hours' })).toBeInTheDocument()
@@ -163,7 +157,7 @@ describe('SyncPolicyField', () => {
</form>
)
}
await render(<SourceForm />)
await renderWithI18n(<SourceForm />)
await user.click(page.getByRole('combobox', { name: 'Sync policy' }))
await user.click(page.getByRole('option', { name: 'Custom interval' }))
@@ -184,7 +178,7 @@ describe('SyncPolicyField', () => {
it('closes the custom interval popover on Escape and resets its draft when reopened', async () => {
const user = userEvent
await render(<SyncPolicyFieldHarness />)
await renderWithI18n(<SyncPolicyFieldHarness />)
const trigger = page.getByRole('combobox', { name: 'Sync policy' })
await user.click(trigger)
@@ -205,7 +199,7 @@ describe('SyncPolicyField', () => {
it('discards edits on cancel and clamps the value to 30 days', async () => {
const user = userEvent
await render(<SyncPolicyFieldHarness />)
await renderWithI18n(<SyncPolicyFieldHarness />)
const trigger = page.getByRole('combobox', { name: 'Sync policy' })
await user.click(trigger)
-84
View File
@@ -294,96 +294,12 @@ const MOCK_ALLOWLIST: MockAllowance[] = [
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/__tests__/delete-agent.browser.spec.tsx',
specifier: 'react-i18next',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/__tests__/delete-agent.browser.spec.tsx',
specifier: '@/features/agent-v2/permissions',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/__tests__/delete-agent.browser.spec.tsx',
specifier: '@/context/i18n',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/__tests__/delete-agent.browser.spec.tsx',
specifier: '@/hooks/use-timestamp',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/__tests__/delete-agent.browser.spec.tsx',
specifier: '@/app/components/app/use-export-app-dsl',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.browser.spec.tsx',
specifier: '@/app/notifications',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.browser.spec.tsx',
specifier: '@/hooks/use-format-time-from-now',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.browser.spec.tsx',
specifier: '@/hooks/use-timestamp',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.browser.spec.tsx',
specifier: '../../version-restore',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/logs/__tests__/page.browser.spec.tsx',
specifier: 'react-i18next',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/logs/__tests__/page.browser.spec.tsx',
specifier: '@/context/i18n',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/agent-v2/agent-detail/logs/__tests__/page.browser.spec.tsx',
specifier: '@/hooks/use-timestamp',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/contacts/im-platform/__tests__/nested-dialog-backdrops.browser.spec.tsx',
specifier: '@/context/i18n',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/network-access/app-binding/__tests__/focus.browser.spec.tsx',
specifier: '@/app/components/base/amplitude',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/network-access/policy-editor/__tests__/focus.browser.spec.tsx',
specifier: 'react-i18next',
occurrence: 1,
reason: legacyMockDebt,
},
{
path: 'features/new-rag/documents/__tests__/delete-document.browser.spec.tsx',
specifier: '../../components/knowledge-model-readiness-banner',