7886840362
Code-review follow-ups: - Validate the raw submitted value before apply_values coerces it, in both the single-edit and bulk endpoints — a scalar set onto a tags field now rejects (422 / rejected) instead of silently char-splitting into a list. - Catalog prunes bulk selections to the currently-visible (filtered) entries after each list fetch, so the bulk bar can't act on rows the user has filtered out of view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
6.0 KiB
React
139 lines
6.0 KiB
React
import React from 'react'
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import { MemoryRouter } from 'react-router-dom'
|
|
|
|
// §22.4a SLICE-3 — the faceted catalog pane (PUC-3). Mock the api so
|
|
// getCollection drives the field schema and listRFCs returns { items, facets }.
|
|
const listRFCs = vi.fn()
|
|
const getCollection = vi.fn()
|
|
const bulkEntryMeta = vi.fn()
|
|
vi.mock('../api', () => ({
|
|
listRFCs: (...a) => listRFCs(...a),
|
|
listProposals: () => Promise.resolve({ items: [] }),
|
|
getCollection: (...a) => getCollection(...a),
|
|
bulkEntryMeta: (...a) => bulkEntryMeta(...a),
|
|
}))
|
|
vi.mock('./ToastHost.jsx', () => ({ showToast: vi.fn() }))
|
|
vi.mock('../lib/entryPaths', () => ({
|
|
entryPath: () => '/x', proposalPath: () => '/p',
|
|
useProjectId: () => 'ohm', useCollectionId: () => 'default',
|
|
}))
|
|
import Catalog from './Catalog.jsx'
|
|
|
|
const renderCatalog = () =>
|
|
render(<MemoryRouter><Catalog viewer={null} onProposeRFC={() => {}} version={0} /></MemoryRouter>)
|
|
|
|
describe('Catalog faceted pane', () => {
|
|
beforeEach(() => { listRFCs.mockReset(); getCollection.mockReset(); bulkEntryMeta.mockReset() })
|
|
|
|
it('renders facet groups with counts when the collection declares fields', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0', 'P1'] } },
|
|
viewer: { can_contribute: false }, entry_noun: 'RFC',
|
|
})
|
|
listRFCs.mockResolvedValue({
|
|
items: [{ slug: 'a', title: 'A', state: 'active', tags: [], starred_by_me: false }],
|
|
facets: { priority: { P0: 3, P1: 1 }, state: { active: 4 } },
|
|
})
|
|
renderCatalog()
|
|
expect(await screen.findByText('P0')).toBeInTheDocument()
|
|
expect(screen.getByText('3')).toBeInTheDocument()
|
|
})
|
|
|
|
it('re-fetches with the selection when a facet checkbox is toggled', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0'] } },
|
|
viewer: {}, entry_noun: 'RFC',
|
|
})
|
|
listRFCs.mockResolvedValue({ items: [], facets: { priority: { P0: 2 }, state: {} } })
|
|
renderCatalog()
|
|
const box = await screen.findByText('P0')
|
|
fireEvent.click(box.closest('label').querySelector('input'))
|
|
await waitFor(() => {
|
|
const last = listRFCs.mock.calls.at(-1)
|
|
expect(last[2].selections.priority).toContain('P0')
|
|
})
|
|
})
|
|
|
|
it('reveals the bulk action bar when a row is selected (faceted + contributor)', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0', 'P1'] } },
|
|
viewer: { can_contribute: true }, entry_noun: 'RFC',
|
|
})
|
|
listRFCs.mockResolvedValue({
|
|
items: [{ slug: 'a', title: 'A', state: 'active', tags: [], starred_by_me: false }],
|
|
facets: { priority: { P0: 1 }, state: { active: 1 } },
|
|
})
|
|
renderCatalog()
|
|
const checkbox = await screen.findByLabelText(/select A/i)
|
|
fireEvent.click(checkbox)
|
|
expect(await screen.findByText(/1 selected/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('sends a bulk request and re-fetches when a set op is applied', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0', 'P1'] } },
|
|
viewer: { can_contribute: true }, entry_noun: 'RFC',
|
|
})
|
|
listRFCs.mockResolvedValue({
|
|
items: [{ slug: 'a', title: 'A', state: 'active', tags: [], starred_by_me: false }],
|
|
facets: { priority: { P0: 1 }, state: { active: 1 } },
|
|
})
|
|
bulkEntryMeta.mockResolvedValue({ applied: ['a'], rejected: [], committed: true })
|
|
renderCatalog()
|
|
fireEvent.click(await screen.findByLabelText(/select A/i))
|
|
fireEvent.change(await screen.findByLabelText(/set priority/i), { target: { value: 'P1' } })
|
|
await waitFor(() => {
|
|
expect(bulkEntryMeta).toHaveBeenCalledWith('ohm', 'default',
|
|
{ slugs: ['a'], op: 'set', field: 'priority', value: 'P1' })
|
|
})
|
|
})
|
|
|
|
it('drops a selection when a filter change hides the row', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0', 'P1'] } },
|
|
viewer: { can_contribute: true }, entry_noun: 'RFC',
|
|
})
|
|
// Content-driven: unfiltered → row "a"; once the P0 facet is selected the
|
|
// server response omits "a" (drives the prune). Robust to extra mount-time
|
|
// fetches the collection effect triggers.
|
|
listRFCs.mockImplementation((p, c, opts) => Promise.resolve({
|
|
items: opts?.selections?.priority?.includes('P0')
|
|
? [{ slug: 'b', title: 'B', state: 'active', tags: [], starred_by_me: false }]
|
|
: [{ slug: 'a', title: 'A', state: 'active', tags: [], starred_by_me: false }],
|
|
facets: { priority: { P0: 1 }, state: { active: 1 } },
|
|
}))
|
|
const { container } = renderCatalog()
|
|
fireEvent.click(await screen.findByLabelText(/select A/i))
|
|
expect(await screen.findByText(/1 selected/i)).toBeInTheDocument()
|
|
// Toggle the P0 facet checkbox → re-fetch returns a list without "a".
|
|
fireEvent.click(container.querySelector('.facet-group input[type="checkbox"]'))
|
|
await waitFor(() => {
|
|
expect(screen.queryByText(/1 selected/i)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('does not offer row selection for a non-contributor', async () => {
|
|
getCollection.mockResolvedValue({
|
|
fields: { priority: { type: 'enum', values: ['P0'] } },
|
|
viewer: { can_contribute: false }, entry_noun: 'RFC',
|
|
})
|
|
listRFCs.mockResolvedValue({
|
|
items: [{ slug: 'a', title: 'A', state: 'active', tags: [], starred_by_me: false }],
|
|
facets: { priority: { P0: 1 }, state: { active: 1 } },
|
|
})
|
|
renderCatalog()
|
|
await screen.findByText('A')
|
|
expect(screen.queryByLabelText(/select A/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('renders legacy state chips when the collection declares no fields', async () => {
|
|
getCollection.mockResolvedValue({ fields: null, viewer: {}, entry_noun: 'RFC' })
|
|
listRFCs.mockResolvedValue({ items: [], facets: {} })
|
|
renderCatalog()
|
|
expect(await screen.findByText('Super-draft')).toBeInTheDocument()
|
|
expect(screen.getByText('Active')).toBeInTheDocument()
|
|
})
|
|
})
|