mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 23:06:40 +08:00
test(web): restore feature owners in browser specs (#46025)
This commit is contained in:
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()
|
||||
|
||||
+28
-16
@@ -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 = [
|
||||
|
||||
+48
-33
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user