Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/web-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
merge-multiple: true

- name: Merge reports
run: pnpm vitest --merge-reports --coverage --silent=passed-only
run: pnpm vitest --merge-reports --reporter=json --reporter=agent --coverage

- name: Coverage Summary
if: always()
Expand Down
5 changes: 5 additions & 0 deletions web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# console or api domain.
# example: http://udify.app/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
# Dev-only Hono proxy targets. The frontend keeps requesting http://localhost:5001 directly.
HONO_PROXY_HOST=127.0.0.1
HONO_PROXY_PORT=5001
HONO_CONSOLE_API_PROXY_TARGET=
HONO_PUBLIC_API_PROXY_TARGET=
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
NEXT_PUBLIC_COOKIE_DOMAIN=

Expand Down
13 changes: 5 additions & 8 deletions web/app/components/base/avatar/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { Avatar } from '../index'

describe('Avatar', () => {
describe('Rendering', () => {
it('should render img element when avatar URL is provided', () => {
it('should keep the fallback visible when avatar URL is provided before image load', () => {
render(<Avatar name="John Doe" avatar="https://example.com/avatar.jpg" />)

const img = screen.getByRole('img', { name: 'John Doe' })
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
expect(screen.getByText('J')).toBeInTheDocument()
})

it('should render fallback with uppercase initial when avatar is null', () => {
Expand All @@ -18,10 +16,9 @@ describe('Avatar', () => {
expect(screen.getByText('A')).toBeInTheDocument()
})

it('should render both image and fallback when avatar is provided', () => {
it('should render the fallback when avatar is provided', () => {
render(<Avatar name="John" avatar="https://example.com/avatar.jpg" />)

expect(screen.getByRole('img')).toBeInTheDocument()
expect(screen.getByText('J')).toBeInTheDocument()
})
})
Expand Down Expand Up @@ -90,7 +87,7 @@ describe('Avatar', () => {
})

describe('onLoadingStatusChange', () => {
it('should render image when avatar and onLoadingStatusChange are provided', () => {
it('should render the fallback when avatar and onLoadingStatusChange are provided', () => {
render(
<Avatar
name="John"
Expand All @@ -99,7 +96,7 @@ describe('Avatar', () => {
/>,
)

expect(screen.getByRole('img')).toBeInTheDocument()
expect(screen.getByText('J')).toBeInTheDocument()
})

it('should not render image when avatar is null even with onLoadingStatusChange', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ describe('ChatWrapper', () => {
expect(screen.getByAltText('answer icon')).toBeInTheDocument()
})

it('should render question icon when user avatar is available', () => {
it('should render question icon fallback when user avatar is available', () => {
vi.mocked(useChatWithHistoryContext).mockReturnValue({
...defaultContextValue,
initUserVariables: {
Expand All @@ -992,12 +992,11 @@ describe('ChatWrapper', () => {
chatList: [{ id: 'q1', content: 'Question' }],
} as unknown as ChatHookReturn)

const { container } = render(<ChatWrapper />)
const avatar = container.querySelector('img[alt="John Doe"]')
expect(avatar).toBeInTheDocument()
render(<ChatWrapper />)
expect(screen.getByText('J')).toBeInTheDocument()
})

it('should use fallback values for nullable appData, appMeta and user name', () => {
it('should use fallback values for nullable appData, appMeta and avatar name', () => {
vi.mocked(useChatWithHistoryContext).mockReturnValue({
...defaultContextValue,
appData: null as unknown as AppData,
Expand All @@ -1014,7 +1013,7 @@ describe('ChatWrapper', () => {

render(<ChatWrapper />)
expect(screen.getByText('Question with fallback avatar name')).toBeInTheDocument()
expect(screen.getByAltText('user')).toBeInTheDocument()
expect(screen.getByText('U')).toBeInTheDocument()
})

it('should set handleStop on currentChatInstanceRef', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe('EmbeddedChatbot chat-wrapper', () => {
expect(screen.getByRole('button', { name: 'send message' })).toBeDisabled()
})

it('should show the user name when avatar data is provided', () => {
it('should show the user avatar fallback when avatar data is provided', () => {
vi.mocked(useEmbeddedChatbotContext).mockReturnValue(createContextValue({
initUserVariables: {
avatar_url: 'https://example.com/avatar.png',
Expand All @@ -337,7 +337,7 @@ describe('EmbeddedChatbot chat-wrapper', () => {

render(<ChatWrapper />)

expect(screen.getByRole('img', { name: 'Alice' })).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
})
})

Expand Down
126 changes: 24 additions & 102 deletions web/app/components/base/mermaid/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -639,128 +639,50 @@ describe('Mermaid Flowchart Component Module Isolation', () => {
}
})

it('should tolerate missing hidden container during classic render and cleanup', async () => {
vi.resetModules()
let pendingContainerRef: unknown | null = null
let patchedContainerRef = false
let patchedTimeoutRef = false
let containerReadCount = 0
const virtualContainer = { innerHTML: 'seed' } as HTMLDivElement

vi.doMock('react', async () => {
const reactActual = await vi.importActual<typeof import('react')>('react')
const mockedUseRef = ((initialValue: unknown) => {
const ref = reactActual.useRef(initialValue as never)
if (!patchedContainerRef && initialValue === null)
pendingContainerRef = ref

if (!patchedContainerRef
&& pendingContainerRef
&& typeof initialValue === 'string'
&& initialValue.startsWith('mermaid-chart-')) {
Object.defineProperty(pendingContainerRef as { current: unknown }, 'current', {
configurable: true,
get() {
containerReadCount += 1
if (containerReadCount === 1)
return virtualContainer
return null
},
set(_value: HTMLDivElement | null) { },
})
patchedContainerRef = true
pendingContainerRef = null
}

if (patchedContainerRef && !patchedTimeoutRef && initialValue === undefined) {
patchedTimeoutRef = true
Object.defineProperty(ref, 'current', {
configurable: true,
get() {
return undefined
},
set(_value: NodeJS.Timeout | undefined) { },
})
return ref
}

return ref
}) as typeof reactActual.useRef

return {
...reactActual,
useRef: mockedUseRef,
}
})
it('should cancel a pending classic render on unmount', async () => {
const { default: FlowchartFresh } = await import('../index')

vi.useFakeTimers()
try {
const { default: FlowchartFresh } = await import('../index')
const { unmount } = render(<FlowchartFresh PrimitiveCode={mockCode} />)
await waitFor(() => {
expect(screen.getByText('test-svg')).toBeInTheDocument()
}, { timeout: 3000 })
unmount()

await act(async () => {
unmount()
await vi.advanceTimersByTimeAsync(350)
})

expect(vi.mocked(mermaidFresh.render)).not.toHaveBeenCalled()
}
finally {
vi.doUnmock('react')
vi.useRealTimers()
}
})

it('should tolerate missing hidden container during handDrawn render', async () => {
vi.resetModules()
let pendingContainerRef: unknown | null = null
let patchedContainerRef = false
let containerReadCount = 0
const virtualContainer = { innerHTML: 'seed' } as HTMLDivElement

vi.doMock('react', async () => {
const reactActual = await vi.importActual<typeof import('react')>('react')
const mockedUseRef = ((initialValue: unknown) => {
const ref = reactActual.useRef(initialValue as never)
if (!patchedContainerRef && initialValue === null)
pendingContainerRef = ref
it('should cancel a pending handDrawn render on unmount', async () => {
const { default: FlowchartFresh } = await import('../index')
const { unmount } = render(<FlowchartFresh PrimitiveCode={mockCode} />)

if (!patchedContainerRef
&& pendingContainerRef
&& typeof initialValue === 'string'
&& initialValue.startsWith('mermaid-chart-')) {
Object.defineProperty(pendingContainerRef as { current: unknown }, 'current', {
configurable: true,
get() {
containerReadCount += 1
if (containerReadCount === 1)
return virtualContainer
return null
},
set(_value: HTMLDivElement | null) { },
})
patchedContainerRef = true
pendingContainerRef = null
}
return ref
}) as typeof reactActual.useRef
await waitFor(() => {
expect(screen.getByText('test-svg')).toBeInTheDocument()
}, { timeout: 3000 })

return {
...reactActual,
useRef: mockedUseRef,
}
})
const initialHandDrawnCalls = vi.mocked(mermaidFresh.mermaidAPI.render).mock.calls.length

vi.useFakeTimers()
try {
const { default: FlowchartFresh } = await import('../index')
const { rerender } = render(<FlowchartFresh PrimitiveCode="graph" />)
await act(async () => {
fireEvent.click(screen.getByText(HAND_DRAWN_RE))
rerender(<FlowchartFresh PrimitiveCode={mockCode} />)
})

await act(async () => {
unmount()
await vi.advanceTimersByTimeAsync(350)
})
await Promise.resolve()
expect(screen.getByText('test-svg-api')).toBeInTheDocument()

expect(vi.mocked(mermaidFresh.mermaidAPI.render).mock.calls.length).toBe(initialHandDrawnCalls)
}
finally {
vi.useRealTimers()
vi.doUnmock('react')
}
})
})
Expand Down
3 changes: 1 addition & 2 deletions web/app/components/base/segmented-control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/utils/classnames'
import Divider from '../divider'
import './index.css'

type SegmentedControlOption<T> = {
value: T
Expand Down Expand Up @@ -131,7 +130,7 @@ export const SegmentedControl = <T extends string | number | symbol>({
<div className={cn('inline-flex items-center gap-x-1', ItemTextWrapperVariants({ size }))}>
<span>{text}</span>
{!!(count && size === 'large') && (
<div className="system-2xs-medium-uppercase inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] text-text-tertiary">
<div className="inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] text-text-tertiary system-2xs-medium-uppercase">
{count}
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions web/app/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@import "../components/base/button/index.css";
@import "../components/base/modal/index.css";
@import "../components/base/premium-badge/index.css";
@import "../components/base/segmented-control/index.css";

@tailwind base;
@tailwind components;
Expand Down
5 changes: 0 additions & 5 deletions web/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2657,11 +2657,6 @@
"count": 1
}
},
"app/components/base/segmented-control/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/base/select/custom.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
Expand Down
6 changes: 2 additions & 4 deletions web/knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ const config: KnipConfig = {
entry: [
'scripts/**/*.{js,ts,mjs}',
'bin/**/*.{js,ts,mjs}',
'taze.config.js',
'tsslint.config.ts',
],
ignore: [
'i18n/**',
'public/**',
],
ignoreBinaries: [
Expand All @@ -19,9 +20,6 @@ const config: KnipConfig = {
'@iconify-json/*',

'@storybook/addon-onboarding',

'@tsslint/compat-eslint',
'@tsslint/config',
],
rules: {
files: 'warn',
Expand Down
Loading
Loading