Merge remote-tracking branch 'origin/master' into mergebot/pr1389
# Conflicts: # packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
// @vitest-environment jsdom
|
||||
// WebBlock: both kinds of the web card. The search card's answer, its citation
|
||||
// list with the title-or-hostname label fallback and optional snippet/date, the
|
||||
// source-list height cap and its expand control, and the truncated indicator;
|
||||
// the fetch card's linked URL, status, and truncation. Safe-link attributes on
|
||||
// both kinds: an http(s) URL becomes an external anchor (target/rel), any other
|
||||
// URL renders as plain text with no href.
|
||||
// full source list under one <ol>, and the truncated indicator; the fetch
|
||||
// card's linked URL, status, and truncation. Safe-link
|
||||
// attributes on both kinds: an http(s) URL becomes an external anchor
|
||||
// (target/rel), any other URL renders as plain text with no href.
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { DEFAULT_WEB_MAX_SOURCES, WebBlock } from '../src/index.ts'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { WebBlock } from '../src/index.ts'
|
||||
import type { WebSourceView } from '../src/index.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
/** `count` sources with sequential hostnames, so the cap slices read distinctly. */
|
||||
/** `count` sources with sequential hostnames, so each row reads distinctly. */
|
||||
function sources(count: number): WebSourceView[] {
|
||||
return Array.from({ length: count }, (_value, index) => ({
|
||||
url: `https://site-${index}.example.com/page`,
|
||||
@@ -123,58 +123,25 @@ describe('WebBlock search card', () => {
|
||||
expect(off.queryByText('来源列表已截断')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders every source and no expand control under the cap', () => {
|
||||
const view = render(<WebBlock kind="search" sources={sources(4)} truncated={false} maxSources={4} />)
|
||||
expect(view.container.querySelectorAll('[class^="_source_"]')).toHaveLength(4)
|
||||
it('renders every source in one <ol> with no expand control', () => {
|
||||
// The card shows the whole list the tool returned, with no head/tail
|
||||
// collapse and no expand button. jsdom does not resolve the CSS Modules
|
||||
// layout, so the scroll geometry the `.sources` max-height produces is
|
||||
// pinned by the assembled browser case in apps/web/tests/web-search-round.e2e.ts,
|
||||
// not here.
|
||||
const view = render(<WebBlock kind="search" sources={sources(30)} truncated={false} />)
|
||||
expect(view.container.querySelectorAll('li[class^="_source_"]')).toHaveLength(30)
|
||||
expect(view.container.querySelector('[aria-expanded]')).toBeNull()
|
||||
})
|
||||
|
||||
it('slices head and tail over the cap and expands on click', () => {
|
||||
const view = render(<WebBlock kind="search" sources={sources(10)} truncated={false} maxSources={4} />)
|
||||
// maxSources 4: head = ceil(4/2) = 2, tail = 4 - 2 = 2, 6 hidden.
|
||||
expect([...view.container.querySelectorAll('[class^="_sourceLink_"]')].map(n => n.textContent))
|
||||
.toEqual(['Source 0', 'Source 1', 'Source 8', 'Source 9'])
|
||||
const toggle = view.getByRole('button', { name: '展开其余 6 条来源' })
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(toggle.textContent).toBe('… 其余 6 条来源')
|
||||
|
||||
fireEvent.click(toggle)
|
||||
expect(view.container.querySelectorAll('[class^="_source_"]')).toHaveLength(10)
|
||||
const collapse = view.getByRole('button', { name: '收起来源' })
|
||||
expect(collapse.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(collapse.textContent).toBe('收起')
|
||||
|
||||
fireEvent.click(collapse)
|
||||
expect(view.container.querySelectorAll('[class^="_source_"]')).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('numbers a collapsed tail by each source original position, not its visible slot', () => {
|
||||
// maxSources 4 over 10 sources: the tail is sources 8 and 9, which must read
|
||||
// as citations 9 and 10 (via <li value>), not renumbered 3 and 4.
|
||||
const view = render(<WebBlock kind="search" sources={sources(10)} truncated={false} maxSources={4} />)
|
||||
const items = [...view.container.querySelectorAll('li[class^="_source_"]')]
|
||||
expect(items.map(li => li.getAttribute('value'))).toEqual(['1', '2', '9', '10'])
|
||||
})
|
||||
|
||||
it('keeps the expander out of the ordered-list numbering', () => {
|
||||
// The expander is a marker-less <li>, so it is valid inside <ol> and does not
|
||||
// consume a citation number between the head and tail sources.
|
||||
const view = render(<WebBlock kind="search" sources={sources(10)} truncated={false} maxSources={4} />)
|
||||
expect(view.container.querySelector('button')).toBeNull()
|
||||
// Every direct child of the <ol> is a source <li> (no marker-less expander).
|
||||
const ol = view.container.querySelector('ol')!
|
||||
// Every direct child is an <li> (no bare <button> child — invalid HTML).
|
||||
expect([...ol.children].every(child => child.tagName === 'LI')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the head slice alone when the cap leaves no tail', () => {
|
||||
const view = render(<WebBlock kind="search" sources={sources(5)} truncated={false} maxSources={1} />)
|
||||
expect([...view.container.querySelectorAll('[class^="_sourceLink_"]')].map(n => n.textContent)).toEqual(['Source 0'])
|
||||
expect(view.getByRole('button', { name: '展开其余 4 条来源' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('caps at the documented default when maxSources is absent', () => {
|
||||
const view = render(<WebBlock kind="search" sources={sources(DEFAULT_WEB_MAX_SOURCES + 1)} truncated={false} />)
|
||||
expect(view.container.querySelectorAll('[class^="_source_"]')).toHaveLength(DEFAULT_WEB_MAX_SOURCES)
|
||||
expect(view.getByRole('button', { name: '展开其余 1 条来源' })).toBeTruthy()
|
||||
it('numbers every source by its 1-based citation index via <li value>', () => {
|
||||
const view = render(<WebBlock kind="search" sources={sources(4)} truncated={false} />)
|
||||
const items = [...view.container.querySelectorAll('li[class^="_source_"]')]
|
||||
expect(items.map(li => li.getAttribute('value'))).toEqual(['1', '2', '3', '4'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user