mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
test(web): migrate New RAG setup specs to Browser Mode (#45722)
This commit is contained in:
1 parent
f8c99ece94
commit
3abf97176e
8 files changed
+495
-583
No files matched your search
+142
@@ -0,0 +1,142 @@
|
||||
import type { KnowledgeFsSettingsResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { KnowledgeModelReadinessBanner } from '../knowledge-model-readiness-banner'
|
||||
|
||||
const getSettings = vi.hoisted(() => vi.fn<() => Promise<KnowledgeFsSettingsResponse>>())
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => '/datasets/new/space-1/documents',
|
||||
useSearchParams: () => new URLSearchParams('status=failed'),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'knowledgeFs.spaces.byControlSpaceId.settings.get': getSettings,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const allCapabilities = {
|
||||
deep: true,
|
||||
index: true,
|
||||
ingest: true,
|
||||
query: true,
|
||||
research: true,
|
||||
source_sync: true,
|
||||
}
|
||||
|
||||
const readiness = (
|
||||
overrides: Partial<KnowledgeFsSettingsResponse> = {},
|
||||
): KnowledgeFsSettingsResponse => ({
|
||||
active_profile_available: true,
|
||||
active_profile_revisions: { embedding: 1, retrieval: 1 },
|
||||
capabilities: allCapabilities,
|
||||
configuration_state: 'active',
|
||||
embedding: null,
|
||||
issues: [],
|
||||
retrieval: null,
|
||||
revision: 1,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
async function renderBanner(capability?: 'index') {
|
||||
const { wrapper: QueryProvider } = createConsoleQueryWrapper()
|
||||
return render(
|
||||
<QueryProvider>
|
||||
<KnowledgeModelReadinessBanner capability={capability} knowledgeSpaceId="space-1" />
|
||||
<button type="button">After readiness</button>
|
||||
</QueryProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('KnowledgeModelReadinessBanner', () => {
|
||||
beforeEach(async () => {
|
||||
await page.viewport(800, 600)
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
it.each([
|
||||
['the active profile is healthy', readiness()],
|
||||
[
|
||||
'validation is waiting for the first document',
|
||||
readiness({
|
||||
configuration_state: 'pending-validation',
|
||||
issues: [{ code: 'missing', field: 'embedding', retryable: true }],
|
||||
}),
|
||||
],
|
||||
])('stays hidden after settings settle when %s', async (_, response) => {
|
||||
getSettings.mockResolvedValue(response)
|
||||
const screen = await renderBanner('index')
|
||||
|
||||
await vi.waitFor(() => expect(getSettings).toHaveBeenCalledOnce())
|
||||
await expect.element(screen.getByRole('status')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('alert')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('button', { name: 'After readiness' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('shows configuration guidance when no model profile can serve the capability', async () => {
|
||||
getSettings.mockResolvedValue(
|
||||
readiness({
|
||||
active_profile_available: false,
|
||||
active_profile_revisions: { embedding: null, retrieval: null },
|
||||
capabilities: Object.fromEntries(
|
||||
Object.keys(allCapabilities).map((key) => [key, false]),
|
||||
) as KnowledgeFsSettingsResponse['capabilities'],
|
||||
configuration_state: 'setup-required',
|
||||
issues: [{ code: 'missing', field: 'embedding', retryable: false }],
|
||||
}),
|
||||
)
|
||||
const screen = await renderBanner('index')
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('status'))
|
||||
.toMatchTextContent('knowledgeSpace.overview.attention.modelReadiness.profilesMissing')
|
||||
})
|
||||
|
||||
it('offers the current page as the return target and exposes the action to the keyboard', async () => {
|
||||
getSettings.mockResolvedValue(
|
||||
readiness({
|
||||
capabilities: { ...allCapabilities, index: false },
|
||||
configuration_state: 'validation-failed',
|
||||
issues: [{ code: 'validation_failed', field: 'embedding', retryable: true }],
|
||||
}),
|
||||
)
|
||||
const user = userEvent
|
||||
const screen = await renderBanner('index')
|
||||
|
||||
const alert = screen.getByRole('alert')
|
||||
await expect.element(alert).toBeVisible()
|
||||
await expect.element(alert).toMatchTextContent('common.api.actionFailed')
|
||||
const configure = alert.getByRole('link', {
|
||||
name: 'knowledgeSpace.overview.attention.action.configureModels',
|
||||
})
|
||||
await expect
|
||||
.element(configure)
|
||||
.toHaveAttribute(
|
||||
'href',
|
||||
'/datasets/new/space-1/settings?returnTo=%2Fdatasets%2Fnew%2Fspace-1%2Fdocuments%3Fstatus%3Dfailed&capability=index',
|
||||
)
|
||||
await user.tab()
|
||||
await expect.element(configure).toHaveFocus()
|
||||
})
|
||||
|
||||
it('removes a transient error after the user retries successfully', async () => {
|
||||
getSettings.mockRejectedValueOnce(new Error('unavailable')).mockResolvedValue(readiness())
|
||||
const screen = await renderBanner()
|
||||
|
||||
const retry = screen.getByRole('button', { name: 'common.operation.retry' })
|
||||
await expect.element(retry).toBeVisible()
|
||||
await retry.click()
|
||||
|
||||
await expect.element(retry).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('alert')).not.toBeInTheDocument()
|
||||
expect(getSettings).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,153 +0,0 @@
|
||||
import type { KnowledgeFsSettingsResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { KnowledgeModelReadinessBanner } from '../knowledge-model-readiness-banner'
|
||||
|
||||
const getSettings = vi.hoisted(() => vi.fn<() => Promise<KnowledgeFsSettingsResponse>>())
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => '/datasets/new/space-1/documents',
|
||||
useSearchParams: () => new URLSearchParams('status=failed'),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'knowledgeFs.spaces.byControlSpaceId.settings.get': getSettings,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const allCapabilities = {
|
||||
deep: true,
|
||||
index: true,
|
||||
ingest: true,
|
||||
query: true,
|
||||
research: true,
|
||||
source_sync: true,
|
||||
}
|
||||
|
||||
const readiness = (
|
||||
overrides: Partial<KnowledgeFsSettingsResponse> = {},
|
||||
): KnowledgeFsSettingsResponse => ({
|
||||
active_profile_available: true,
|
||||
active_profile_revisions: { embedding: 1, retrieval: 1 },
|
||||
capabilities: allCapabilities,
|
||||
configuration_state: 'active',
|
||||
embedding: null,
|
||||
issues: [],
|
||||
retrieval: null,
|
||||
revision: 1,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const configureModelsLink = () =>
|
||||
screen.queryByRole('link', { name: 'knowledgeSpace.overview.attention.action.configureModels' })
|
||||
|
||||
describe('KnowledgeModelReadinessBanner', () => {
|
||||
it.each([
|
||||
['the active profile is healthy', readiness()],
|
||||
[
|
||||
'configuration is complete but the first profile is not active yet',
|
||||
readiness({
|
||||
active_profile_available: false,
|
||||
active_profile_revisions: { embedding: null, retrieval: null },
|
||||
capabilities: {
|
||||
...allCapabilities,
|
||||
deep: false,
|
||||
index: false,
|
||||
query: false,
|
||||
research: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
'configuration is complete despite an unrelated runtime issue',
|
||||
readiness({
|
||||
capabilities: { ...allCapabilities, research: false },
|
||||
issues: [{ code: 'unavailable', field: 'publication', retryable: true }],
|
||||
}),
|
||||
],
|
||||
[
|
||||
'validation is waiting for the first document',
|
||||
readiness({
|
||||
configuration_state: 'pending-validation',
|
||||
issues: [{ code: 'missing', field: 'embedding', retryable: true }],
|
||||
}),
|
||||
],
|
||||
])('stays hidden when %s', async (_, response) => {
|
||||
getSettings.mockResolvedValue(response)
|
||||
|
||||
renderWithConsoleQuery(
|
||||
<KnowledgeModelReadinessBanner capability="index" knowledgeSpaceId="space-1" />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(getSettings).toHaveBeenCalled())
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows configuration guidance when model setup is required', async () => {
|
||||
getSettings.mockResolvedValue(
|
||||
readiness({
|
||||
active_profile_available: false,
|
||||
active_profile_revisions: { embedding: null, retrieval: null },
|
||||
capabilities: Object.fromEntries(
|
||||
Object.keys(allCapabilities).map((key) => [key, false]),
|
||||
) as KnowledgeFsSettingsResponse['capabilities'],
|
||||
configuration_state: 'setup-required',
|
||||
issues: [{ code: 'missing', field: 'embedding', retryable: false }],
|
||||
}),
|
||||
)
|
||||
|
||||
renderWithConsoleQuery(
|
||||
<KnowledgeModelReadinessBanner capability="index" knowledgeSpaceId="space-1" />,
|
||||
)
|
||||
|
||||
expect(await screen.findByRole('status')).toHaveTextContent(
|
||||
'knowledgeSpace.overview.attention.modelReadiness.profilesMissing',
|
||||
)
|
||||
})
|
||||
|
||||
it('links validation failures to settings and preserves the current page', async () => {
|
||||
getSettings.mockResolvedValue(
|
||||
readiness({
|
||||
capabilities: { ...allCapabilities, index: false },
|
||||
configuration_state: 'validation-failed',
|
||||
issues: [{ code: 'validation_failed', field: 'embedding', retryable: true }],
|
||||
}),
|
||||
)
|
||||
|
||||
renderWithConsoleQuery(
|
||||
<KnowledgeModelReadinessBanner capability="index" knowledgeSpaceId="space-1" />,
|
||||
)
|
||||
|
||||
const alert = await screen.findByRole('alert')
|
||||
expect(alert).toHaveTextContent('common.api.actionFailed')
|
||||
expect(alert).toHaveTextContent('knowledgeSpace.overview.attention.modelReadiness.description')
|
||||
expect(configureModelsLink()).toHaveAttribute(
|
||||
'href',
|
||||
'/datasets/new/space-1/settings?returnTo=%2Fdatasets%2Fnew%2Fspace-1%2Fdocuments%3Fstatus%3Dfailed&capability=index',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a readiness fetch failure separate and retryable', async () => {
|
||||
const user = userEvent.setup()
|
||||
getSettings.mockRejectedValueOnce(new Error('unavailable')).mockResolvedValue(readiness())
|
||||
|
||||
renderWithConsoleQuery(<KnowledgeModelReadinessBanner knowledgeSpaceId="space-1" />)
|
||||
await user.click(await screen.findByRole('button', { name: 'common.operation.retry' }))
|
||||
|
||||
expect(configureModelsLink()).not.toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'common.operation.retry' }),
|
||||
).not.toBeInTheDocument(),
|
||||
)
|
||||
expect(getSettings).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
+167
-22
@@ -1,10 +1,77 @@
|
||||
import type { DatasourceParameters, DatasourceParameterSchema } from '../datasource-parameter-model'
|
||||
import { useState } from 'react'
|
||||
import { page } from 'vite-plus/test/browser'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { WebsiteDatasourceParameterForm } from '../datasource-parameter-form'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import {
|
||||
DatasourceParameterForm,
|
||||
WebsiteDatasourceParameterForm,
|
||||
} from '../datasource-parameter-form'
|
||||
import {
|
||||
datasourceParameterDefaults,
|
||||
websiteDatasourceParameterSchemas,
|
||||
} from '../datasource-parameter-model'
|
||||
|
||||
const schemas: DatasourceParameterSchema[] = [
|
||||
const limitSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Limit' },
|
||||
max: 200,
|
||||
min: 100,
|
||||
name: 'limit',
|
||||
options: [],
|
||||
required: true,
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
const urlSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Root URL' },
|
||||
name: 'url',
|
||||
options: [],
|
||||
required: true,
|
||||
type: 'string',
|
||||
}
|
||||
|
||||
const decimalSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Threshold' },
|
||||
name: 'threshold',
|
||||
options: [],
|
||||
precision: 2,
|
||||
required: false,
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
function ParameterForm({ schema }: { schema: DatasourceParameterSchema }) {
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>({})
|
||||
return (
|
||||
<DatasourceParameterForm parameters={parameters} schemas={[schema]} onChange={setParameters} />
|
||||
)
|
||||
}
|
||||
|
||||
function WebsiteParameterForm({ customized = false }: { customized?: boolean }) {
|
||||
const schemas = websiteDatasourceParameterSchemas({ parameters: [] } as never)
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>(() => ({
|
||||
...datasourceParameterDefaults(schemas),
|
||||
...(customized ? { limit: 99 } : {}),
|
||||
}))
|
||||
return (
|
||||
<WebsiteDatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={schemas}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyWebsiteParameterForm() {
|
||||
const schemas = websiteDatasourceParameterSchemas({ parameters: [] } as never)
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>(() =>
|
||||
datasourceParameterDefaults(schemas),
|
||||
)
|
||||
return (
|
||||
<DatasourceParameterForm parameters={parameters} schemas={schemas} onChange={setParameters} />
|
||||
)
|
||||
}
|
||||
|
||||
const scrollSchema: DatasourceParameterSchema[] = [
|
||||
{
|
||||
label: { en_US: 'Include subpages' },
|
||||
name: 'include_subpages',
|
||||
@@ -16,7 +83,6 @@ const schemas: DatasourceParameterSchema[] = [
|
||||
|
||||
function ScrollableCrawlOptions() {
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>({ include_subpages: true })
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Source setup"
|
||||
@@ -27,7 +93,7 @@ function ScrollableCrawlOptions() {
|
||||
<div style={{ height: 500 }} />
|
||||
<WebsiteDatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={schemas}
|
||||
schemas={scrollSchema}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
<div style={{ height: 500 }} />
|
||||
@@ -37,23 +103,102 @@ function ScrollableCrawlOptions() {
|
||||
)
|
||||
}
|
||||
|
||||
it('keeps the surrounding panel in place when clicking a scrolled crawl option label', async () => {
|
||||
await page.viewport(900, 600)
|
||||
const screen = await render(<ScrollableCrawlOptions />)
|
||||
await screen.getByRole('button', { name: 'knowledgeSources.crawlOptions' }).click()
|
||||
const label = screen.getByText('Include subpages', { exact: true })
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Include subpages' })
|
||||
const panel = screen.getByRole('region', { name: 'Source setup' }).element()
|
||||
const heading = screen.getByRole('heading', { name: 'Connect a source' }).element()
|
||||
const headingTop = heading.getBoundingClientRect().top
|
||||
describe('DatasourceParameterForm', () => {
|
||||
beforeEach(async () => {
|
||||
await page.viewport(900, 600)
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
await label.click()
|
||||
await expect.element(checkbox).not.toBeChecked()
|
||||
expect(panel.scrollTop).toBe(0)
|
||||
expect(heading.getBoundingClientRect().top).toBe(headingTop)
|
||||
it('keeps an out-of-range number visible and exposes native and accessible invalid state', async () => {
|
||||
const screen = await render(<ParameterForm schema={limitSchema} />)
|
||||
const input = screen.getByRole('spinbutton', { name: 'Limit' })
|
||||
|
||||
await label.click()
|
||||
await expect.element(checkbox).toBeChecked()
|
||||
expect(panel.scrollTop).toBe(0)
|
||||
expect(heading.getBoundingClientRect().top).toBe(headingTop)
|
||||
await input.fill('15')
|
||||
await userEvent.tab()
|
||||
|
||||
await expect.element(input).toHaveValue(15)
|
||||
await expect.element(input).toHaveAttribute('aria-invalid', 'true')
|
||||
expect((input.element() as HTMLInputElement).validity.rangeUnderflow).toBe(true)
|
||||
})
|
||||
|
||||
it('associates the actionable URL error after native blur validation', async () => {
|
||||
const screen = await render(<ParameterForm schema={urlSchema} />)
|
||||
const input = screen.getByRole('textbox', { name: 'Root URL' })
|
||||
|
||||
await input.fill('ftp://example.com')
|
||||
await userEvent.tab()
|
||||
|
||||
await expect.element(input).toHaveAttribute('aria-invalid', 'true')
|
||||
await expect.element(input).toHaveAccessibleDescription('dataset.validation.invalidHttpUrl')
|
||||
await expect.element(screen.getByText('dataset.validation.invalidHttpUrl')).toBeVisible()
|
||||
await expect.element(input).toHaveValue('ftp://example.com')
|
||||
})
|
||||
|
||||
it('reveals website options, applies edits, and resets them to provider defaults', async () => {
|
||||
const screen = await render(<WebsiteParameterForm />)
|
||||
const crawlOptions = screen.getByRole('button', { name: 'knowledgeSources.crawlOptions' })
|
||||
|
||||
await expect.element(crawlOptions).toHaveAttribute('aria-expanded', 'false')
|
||||
await expect.element(crawlOptions).toMatchTextContent('knowledgeSources.usingDefaults')
|
||||
await crawlOptions.click()
|
||||
const includeSubpages = screen.getByRole('checkbox', {
|
||||
name: 'knowledgeSources.includeSubpages',
|
||||
})
|
||||
const reset = screen.getByRole('button', { name: 'knowledgeSources.resetToDefaults' })
|
||||
await expect.element(includeSubpages).toBeChecked()
|
||||
await includeSubpages.click()
|
||||
await expect.element(includeSubpages).not.toBeChecked()
|
||||
await reset.click()
|
||||
await expect.element(includeSubpages).toBeChecked()
|
||||
})
|
||||
|
||||
it('does not describe customized collapsed website options as defaults', async () => {
|
||||
const customized = await render(<WebsiteParameterForm customized />)
|
||||
const trigger = customized.getByRole('button', { name: 'knowledgeSources.crawlOptions' })
|
||||
await expect.element(trigger).toHaveTextContent('knowledgeSources.crawlOptions')
|
||||
})
|
||||
|
||||
it('preserves declaration precision through the native decimal input', async () => {
|
||||
const decimal = await render(<ParameterForm schema={decimalSchema} />)
|
||||
const input = decimal.getByRole('spinbutton', { name: 'Threshold' })
|
||||
await expect.element(input).toHaveAttribute('inputmode', 'decimal')
|
||||
await expect.element(input).toHaveAttribute('step', '0.01')
|
||||
await input.fill('0.25')
|
||||
await expect.element(input).toHaveValue(0.25)
|
||||
expect((input.element() as HTMLInputElement).validity.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves the legacy website controls to their public labels and native values', async () => {
|
||||
const screen = await render(<LegacyWebsiteParameterForm />)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('textbox', { name: 'knowledgeSources.rootUrl' }))
|
||||
.toHaveAttribute('placeholder', 'knowledgeSources.rootUrlPlaceholder')
|
||||
await expect
|
||||
.element(screen.getByRole('switch', { name: 'knowledgeSources.includeSubpages' }))
|
||||
.toBeChecked()
|
||||
await expect
|
||||
.element(screen.getByRole('spinbutton', { name: 'knowledgeSources.maxPages' }))
|
||||
.toHaveValue(100)
|
||||
})
|
||||
|
||||
it('keeps the surrounding panel in place when clicking a scrolled crawl option label', async () => {
|
||||
const screen = await render(<ScrollableCrawlOptions />)
|
||||
await screen.getByRole('button', { name: 'knowledgeSources.crawlOptions' }).click()
|
||||
const label = screen.getByText('Include subpages', { exact: true })
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Include subpages' })
|
||||
const panel = screen.getByRole('region', { name: 'Source setup' }).element()
|
||||
const heading = screen.getByRole('heading', { name: 'Connect a source' }).element()
|
||||
const headingTop = heading.getBoundingClientRect().top
|
||||
|
||||
await label.click()
|
||||
await expect.element(checkbox).not.toBeChecked()
|
||||
expect(panel.scrollTop).toBe(0)
|
||||
expect(heading.getBoundingClientRect().top).toBe(headingTop)
|
||||
|
||||
await label.click()
|
||||
await expect.element(checkbox).toBeChecked()
|
||||
expect(panel.scrollTop).toBe(0)
|
||||
expect(heading.getBoundingClientRect().top).toBe(headingTop)
|
||||
})
|
||||
})
|
||||
@@ -1,211 +0,0 @@
|
||||
import type { DatasourceParameters, DatasourceParameterSchema } from '../datasource-parameter-model'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { render } from '@/test/console/render'
|
||||
import {
|
||||
DatasourceParameterForm,
|
||||
WebsiteDatasourceParameterForm,
|
||||
} from '../datasource-parameter-form'
|
||||
import {
|
||||
datasourceParameterDefaults,
|
||||
websiteDatasourceParameterSchemas,
|
||||
} from '../datasource-parameter-model'
|
||||
|
||||
const limitSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Limit' },
|
||||
max: 200,
|
||||
min: 100,
|
||||
name: 'limit',
|
||||
options: [],
|
||||
required: true,
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
const urlSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Root URL' },
|
||||
name: 'url',
|
||||
options: [],
|
||||
required: true,
|
||||
type: 'string',
|
||||
}
|
||||
|
||||
const decimalSchema: DatasourceParameterSchema = {
|
||||
label: { en_US: 'Threshold' },
|
||||
name: 'threshold',
|
||||
options: [],
|
||||
precision: 2,
|
||||
required: false,
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
function NumberParameterForm() {
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>({})
|
||||
return (
|
||||
<DatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={[limitSchema]}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function UrlParameterForm() {
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>({})
|
||||
return (
|
||||
<DatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={[urlSchema]}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DecimalParameterForm() {
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>({})
|
||||
return (
|
||||
<DatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={[decimalSchema]}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyWebsiteParameterForm() {
|
||||
const schemas = websiteDatasourceParameterSchemas({ parameters: [] } as never)
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>(() =>
|
||||
datasourceParameterDefaults(schemas),
|
||||
)
|
||||
return (
|
||||
<DatasourceParameterForm parameters={parameters} schemas={schemas} onChange={setParameters} />
|
||||
)
|
||||
}
|
||||
|
||||
function GroupedWebsiteParameterForm() {
|
||||
const schemas = websiteDatasourceParameterSchemas({ parameters: [] } as never)
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>(() =>
|
||||
datasourceParameterDefaults(schemas),
|
||||
)
|
||||
return (
|
||||
<WebsiteDatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={schemas}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomizedGroupedWebsiteParameterForm() {
|
||||
const schemas = websiteDatasourceParameterSchemas({ parameters: [] } as never)
|
||||
const [parameters, setParameters] = useState<DatasourceParameters>(() => ({
|
||||
...datasourceParameterDefaults(schemas),
|
||||
limit: 99,
|
||||
}))
|
||||
return (
|
||||
<WebsiteDatasourceParameterForm
|
||||
parameters={parameters}
|
||||
schemas={schemas}
|
||||
onChange={setParameters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('DatasourceParameterForm', () => {
|
||||
it('keeps an out-of-range numeric value visible and reports it as invalid', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NumberParameterForm />)
|
||||
|
||||
const input = screen.getByRole('spinbutton', { name: 'Limit' })
|
||||
await user.type(input, '15')
|
||||
expect(input).toHaveValue(15)
|
||||
|
||||
await user.tab()
|
||||
expect(input).toHaveValue(15)
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('associates an actionable error with an invalid URL field', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<UrlParameterForm />)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Root URL' })
|
||||
expect(input).not.toHaveAttribute('aria-invalid', 'true')
|
||||
expect(screen.queryByText('dataset.validation.invalidHttpUrl')).not.toBeInTheDocument()
|
||||
|
||||
await user.type(input, 'ftp://example.com')
|
||||
await user.tab()
|
||||
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(input).toHaveAccessibleDescription('dataset.validation.invalidHttpUrl')
|
||||
expect(screen.getByText('dataset.validation.invalidHttpUrl')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('groups website options and resets edits to provider defaults', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<GroupedWebsiteParameterForm />)
|
||||
|
||||
expect(screen.getByRole('textbox', { name: 'knowledgeSources.rootUrl' })).toBeVisible()
|
||||
const crawlOptions = screen.getByRole('button', {
|
||||
name: 'knowledgeSources.crawlOptions',
|
||||
})
|
||||
expect(crawlOptions).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(crawlOptions).toHaveTextContent('knowledgeSources.usingDefaults')
|
||||
expect(
|
||||
screen.queryByRole('checkbox', { name: 'knowledgeSources.includeSubpages' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'knowledgeSources.resetToDefaults' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
await user.click(crawlOptions)
|
||||
const includeSubpages = screen.getByRole('checkbox', {
|
||||
name: 'knowledgeSources.includeSubpages',
|
||||
})
|
||||
const resetButton = screen.getByRole('button', {
|
||||
name: 'knowledgeSources.resetToDefaults',
|
||||
})
|
||||
expect(includeSubpages).toBeChecked()
|
||||
expect(resetButton).toBeEnabled()
|
||||
expect(screen.getByRole('spinbutton', { name: 'knowledgeSources.maxPages' })).toBeVisible()
|
||||
|
||||
await user.click(includeSubpages)
|
||||
expect(includeSubpages).not.toBeChecked()
|
||||
expect(resetButton).toBeEnabled()
|
||||
|
||||
await user.click(resetButton)
|
||||
expect(includeSubpages).toBeChecked()
|
||||
})
|
||||
|
||||
it('does not describe customized collapsed website options as defaults', () => {
|
||||
render(<CustomizedGroupedWebsiteParameterForm />)
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'knowledgeSources.crawlOptions' }),
|
||||
).not.toHaveTextContent('knowledgeSources.usingDefaults')
|
||||
})
|
||||
|
||||
it('allows decimal input using the declaration precision', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<DecimalParameterForm />)
|
||||
|
||||
const input = screen.getByRole('spinbutton', { name: 'Threshold' })
|
||||
expect(input).toHaveAttribute('inputmode', 'decimal')
|
||||
expect(input).toHaveAttribute('step', '0.01')
|
||||
|
||||
await user.type(input, '0.25')
|
||||
expect(input).toHaveValue(0.25)
|
||||
expect(input).not.toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('resolves translation keys for legacy website parameter labels', () => {
|
||||
render(<LegacyWebsiteParameterForm />)
|
||||
|
||||
expect(screen.getByRole('textbox', { name: 'knowledgeSources.rootUrl' })).toHaveAttribute(
|
||||
'placeholder',
|
||||
'knowledgeSources.rootUrlPlaceholder',
|
||||
)
|
||||
expect(screen.getByRole('switch', { name: 'knowledgeSources.includeSubpages' })).toBeChecked()
|
||||
expect(screen.getByRole('spinbutton', { name: 'knowledgeSources.maxPages' })).toHaveValue(100)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState } from 'react'
|
||||
import { page } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { SourceProviderIcon } from '../fields'
|
||||
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => ({
|
||||
currentWorkspace: { id: 'workspace-123' },
|
||||
}))
|
||||
})
|
||||
|
||||
const validIcon =
|
||||
'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22%3E%3Crect width=%2216%22 height=%2216%22 fill=%22%234B7BEC%22/%3E%3C/svg%3E'
|
||||
const brokenIcon = 'data:image/png;base64,broken-image'
|
||||
|
||||
function IconSession() {
|
||||
const [icon, setIcon] = useState(brokenIcon)
|
||||
return (
|
||||
<>
|
||||
<SourceProviderIcon icon={icon} fallbackIcon="i-ri-file-text-line" />
|
||||
<button type="button" onClick={() => setIcon(validIcon)}>
|
||||
Use valid icon
|
||||
</button>
|
||||
<button type="button" onClick={() => setIcon(brokenIcon)}>
|
||||
Use broken icon
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('SourceProviderIcon', () => {
|
||||
beforeEach(async () => {
|
||||
await page.viewport(480, 320)
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
it('recovers from a native decode failure for each new image session', async () => {
|
||||
const screen = await render(<IconSession />)
|
||||
const image = () => screen.getByAltText('')
|
||||
|
||||
await expect.element(image()).not.toBeInTheDocument()
|
||||
|
||||
await screen.getByRole('button', { name: 'Use valid icon' }).click()
|
||||
await expect.element(image()).toHaveAttribute('src', validIcon)
|
||||
await expect
|
||||
.poll(() => {
|
||||
const element = image().element() as HTMLImageElement
|
||||
return element.complete && element.naturalWidth
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await screen.getByRole('button', { name: 'Use broken icon' }).click()
|
||||
await expect.element(image()).not.toBeInTheDocument()
|
||||
|
||||
await screen.getByRole('button', { name: 'Use valid icon' }).click()
|
||||
await expect
|
||||
.poll(() => {
|
||||
const element = image().element() as HTMLImageElement
|
||||
return element.complete && element.naturalWidth
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { SourceProviderOption } from '../provider-options'
|
||||
import { fireEvent, screen, within } from '@testing-library/react'
|
||||
import { render } from '@/test/console/render'
|
||||
import { SourceProviderIcon, SourceProviderSelector } from '../fields'
|
||||
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => ({
|
||||
currentWorkspace: { id: 'workspace-123' },
|
||||
}))
|
||||
})
|
||||
|
||||
describe('source provider icons', () => {
|
||||
it('replaces a broken image and retries after switching images, including switching back', () => {
|
||||
const { rerender, container } = render(
|
||||
<SourceProviderIcon icon="/first.svg" fallbackIcon="i-ri-file-text-line" />,
|
||||
)
|
||||
fireEvent.error(screen.getByRole('presentation', { hidden: true }))
|
||||
expect(screen.queryByRole('presentation', { hidden: true })).not.toBeInTheDocument()
|
||||
expect(container.firstElementChild).toBeVisible()
|
||||
|
||||
rerender(<SourceProviderIcon icon="/second.svg" fallbackIcon="i-ri-file-text-line" />)
|
||||
expect(screen.getByRole('presentation', { hidden: true })).toHaveAttribute('src', '/second.svg')
|
||||
rerender(<SourceProviderIcon icon="/first.svg" fallbackIcon="i-ri-file-text-line" />)
|
||||
expect(screen.getByRole('presentation', { hidden: true })).toHaveAttribute('src', '/first.svg')
|
||||
})
|
||||
|
||||
it('uses the plugin image when the datasource image is empty', () => {
|
||||
const label = { en_US: 'GitHub Repository', zh_Hans: 'GitHub Repository' }
|
||||
const datasource = {
|
||||
description: label,
|
||||
identity: { author: 'langgenius', icon: '', label, name: 'github', provider: 'github' },
|
||||
parameters: [],
|
||||
}
|
||||
const option: SourceProviderOption = {
|
||||
datasource,
|
||||
fallbackIcon: 'i-ri-file-text-line',
|
||||
key: 'github',
|
||||
label: 'GitHub Repository',
|
||||
packageId: 'langgenius/github',
|
||||
providerType: 'online_document',
|
||||
sourceType: 'onlineDocuments',
|
||||
plugin: {
|
||||
plugin_id: 'langgenius/github',
|
||||
plugin_unique_identifier: 'langgenius/github:1.0.0@local',
|
||||
provider: 'github',
|
||||
is_authorized: false,
|
||||
declaration: {
|
||||
credentials_schema: [],
|
||||
provider_type: 'online_document',
|
||||
datasources: [datasource],
|
||||
identity: {
|
||||
author: 'langgenius',
|
||||
description: label,
|
||||
icon: 'github.svg',
|
||||
label,
|
||||
name: 'github',
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
render(<SourceProviderSelector options={[option]} providerKey="github" onChange={vi.fn()} />)
|
||||
const image = within(screen.getByRole('radio', { name: 'GitHub Repository' })).getByRole(
|
||||
'presentation',
|
||||
{ hidden: true },
|
||||
)
|
||||
expect(image.getAttribute('src')).toContain(
|
||||
'/workspaces/current/plugin/icon?tenant_id=workspace-123&filename=github.svg',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
|
||||
import { KnowledgeFsApiAccessDialog } from '../api-access-dialog'
|
||||
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'datasets.apiKeys.get': async () => ({
|
||||
data: [
|
||||
{
|
||||
id: 'workspace-key',
|
||||
token: 'dataset-existing-secret',
|
||||
created_at: 1710000000,
|
||||
last_used_at: null,
|
||||
type: 'dataset',
|
||||
dataset_ids: [],
|
||||
knowledge_space_ids: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
'datasets.apiBaseInfo.get': async () => ({ api_base_url: 'https://api.example.com/v1/' }),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
async function renderDialog(
|
||||
props: Omit<ComponentProps<typeof KnowledgeFsApiAccessDialog>, 'onOpenChange' | 'open'>,
|
||||
workspacePermissionKeys: string[] = ['dataset.api_key.manage'],
|
||||
) {
|
||||
function Viewer() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setOpen(true)}>
|
||||
Open API access
|
||||
</button>
|
||||
<KnowledgeFsApiAccessDialog {...props} open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
const { wrapper: QueryProvider } = createConsoleQueryWrapper({ workspacePermissionKeys })
|
||||
return render(
|
||||
<QueryProvider>
|
||||
<Viewer />
|
||||
</QueryProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('KnowledgeFsApiAccessDialog', () => {
|
||||
beforeEach(async () => {
|
||||
await page.viewport(800, 700)
|
||||
onTestFinished(guardUnexpectedFetch())
|
||||
})
|
||||
|
||||
it('moves from Service API details to workspace keys and reopens a fresh dialog', async () => {
|
||||
const user = userEvent
|
||||
const screen = await renderDialog({ status: 'active', knowledgeSpaceId: 'space-1' })
|
||||
const opener = screen.getByRole('button', { name: 'Open API access' })
|
||||
|
||||
await opener.click()
|
||||
let serviceDialog = screen.getByRole('dialog', { name: 'knowledgeSpace.apiAgentAccess' })
|
||||
await expect.element(serviceDialog).toBeVisible()
|
||||
expect(serviceDialog.element().contains(document.activeElement)).toBe(true)
|
||||
await expect
|
||||
.element(serviceDialog.getByText('https://api.example.com/v1/knowledge-fs/spaces/space-1'))
|
||||
.toBeVisible()
|
||||
|
||||
await user.click(serviceDialog.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
await expect.element(serviceDialog).not.toBeInTheDocument()
|
||||
const keyDialog = screen.getByRole('dialog', { name: 'appApi.apiKeyModal.apiSecretKey' })
|
||||
await expect.element(keyDialog).toBeVisible()
|
||||
expect(keyDialog.element().contains(document.activeElement)).toBe(true)
|
||||
await expect.element(keyDialog.getByText('datas...cret')).toBeVisible()
|
||||
await expect
|
||||
.element(keyDialog.getByRole('button', { name: 'appApi.apiKeyModal.createNewSecretKey' }))
|
||||
.toBeEnabled()
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
await expect.element(keyDialog).not.toBeInTheDocument()
|
||||
await expect.element(opener).toHaveFocus()
|
||||
|
||||
await user.keyboard('{Enter}')
|
||||
serviceDialog = screen.getByRole('dialog', { name: 'knowledgeSpace.apiAgentAccess' })
|
||||
await expect.element(serviceDialog).toBeVisible()
|
||||
await expect
|
||||
.element(serviceDialog.getByText('https://api.example.com/v1/knowledge-fs/spaces/space-1'))
|
||||
.toBeVisible()
|
||||
})
|
||||
|
||||
it('shows inactive access without exposing workspace key management', async () => {
|
||||
const screen = await renderDialog({ status: 'inactive', knowledgeSpaceId: 'space-1' })
|
||||
await screen.getByRole('button', { name: 'Open API access' }).click()
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'knowledgeSpace.apiAgentAccess' })
|
||||
await expect.element(dialog.getByText('knowledgeSpace.apiAccessInactive')).toBeVisible()
|
||||
await expect
|
||||
.element(dialog.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
.not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the endpoint visible but makes key management read-only without permission', async () => {
|
||||
const screen = await renderDialog({ status: 'active', knowledgeSpaceId: 'space-1' }, [])
|
||||
await screen.getByRole('button', { name: 'Open API access' }).click()
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'knowledgeSpace.apiAgentAccess' })
|
||||
await expect.element(dialog.getByText('knowledgeSpace.settings.viewOnly')).toBeVisible()
|
||||
await expect
|
||||
.element(dialog.getByText('https://api.example.com/v1/knowledge-fs/spaces/space-1'))
|
||||
.toBeVisible()
|
||||
await expect
|
||||
.element(dialog.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
.not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,125 +0,0 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { KnowledgeFsApiAccessDialog } from '../api-access-dialog'
|
||||
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'datasets.apiKeys.get': async () => ({
|
||||
data: [
|
||||
{
|
||||
id: 'workspace-key',
|
||||
token: 'dataset-existing-secret',
|
||||
created_at: 1710000000,
|
||||
last_used_at: null,
|
||||
type: 'dataset',
|
||||
dataset_ids: [],
|
||||
knowledge_space_ids: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
'datasets.apiBaseInfo.get': async () => ({ api_base_url: 'https://api.example.com/v1/' }),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
function renderDialog(
|
||||
props: ComponentProps<typeof KnowledgeFsApiAccessDialog>,
|
||||
workspacePermissionKeys: string[] = ['dataset.api_key.manage'],
|
||||
) {
|
||||
return renderWithConsoleQuery(<KnowledgeFsApiAccessDialog {...props} />, {
|
||||
workspacePermissionKeys,
|
||||
})
|
||||
}
|
||||
|
||||
describe('KnowledgeFsApiAccessDialog', () => {
|
||||
it('shows the generated KnowledgeFS Service API endpoint', async () => {
|
||||
renderDialog({
|
||||
status: 'active',
|
||||
knowledgeSpaceId: 'space-1',
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
})
|
||||
|
||||
expect(
|
||||
await screen.findByText('https://api.example.com/v1/knowledge-fs/spaces/space-1'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('common.appMenus.apiAccessTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens the shared workspace Dataset API key manager', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenChange = vi.fn()
|
||||
function Viewer() {
|
||||
const [open, setOpen] = useState(true)
|
||||
return (
|
||||
<KnowledgeFsApiAccessDialog
|
||||
status="active"
|
||||
knowledgeSpaceId="space-1"
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
onOpenChange(next)
|
||||
setOpen(next)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
renderWithConsoleQuery(<Viewer />, { workspacePermissionKeys: ['dataset.api_key.manage'] })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: 'appApi.apiKeyModal.apiSecretKey' }),
|
||||
).toBeInTheDocument()
|
||||
expect(await screen.findByText('datas...cret')).toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByLabelText('knowledgeSpace.apiAgentAccess')).not.toBeInTheDocument(),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'appApi.apiKeyModal.createNewSecretKey' }),
|
||||
).toBeEnabled()
|
||||
})
|
||||
|
||||
it('does not expose API key management while Service API access is disabled', async () => {
|
||||
renderDialog({
|
||||
status: 'inactive',
|
||||
knowledgeSpaceId: 'space-1',
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
})
|
||||
|
||||
expect(screen.getByText('knowledgeSpace.apiAccessInactive')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'dataset.serviceApi.card.apiKey' }),
|
||||
).not.toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: 'appApi.apiKeyModal.apiSecretKey' }),
|
||||
).not.toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the workspace API key permission for the read-only state', () => {
|
||||
renderDialog(
|
||||
{
|
||||
status: 'active',
|
||||
knowledgeSpaceId: 'space-1',
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
expect(screen.getByText('knowledgeSpace.settings.viewOnly')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'dataset.serviceApi.card.apiKey' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user