/** * Component tests for `api.setFacetFilter()` - the imperative entry * point that powers the three set-filter UX patterns (Excel-style * column-menu, async value loaders, and tree-list cascades). The grid * is filter-source-agnostic; this test exercises the API directly so * coverage is independent of any specific UI implementation. * * Covers: * - Single column facet filter narrows the displayed rows * - Multi-value filter (OR within column) accepts every listed value * - Passing null / empty clears the filter * - Two columns AND across (different columns) * - Combines with operator filter from setFilter() * - Survives unknown values (no crash, just no matches) * - Unicode / non-ASCII values match exactly (no normalization) */ import { describe, expect, it } from 'vitest' import { mount, unmount } from 'svelte' import SvGrid from './SvGrid.svelte' import { columnFilteringFeature, createCoreRowModel, createFilteredRowModel, createSortedRowModel, rowSortingFeature, sortFns, tableFeatures, } from './index' import type { ColumnDef, SvGridApi } from './index' type Order = { id: string region: 'Americas' | 'EMEA' | 'APAC' country: string status: 'open' | 'paid' | 'shipped' | 'delivered' | 'returned' amount: number } const features = tableFeatures({ columnFilteringFeature, rowSortingFeature }) const rows: Order[] = [ { id: 'O-1', region: 'Americas', country: 'United States', status: 'open', amount: 120 }, { id: 'O-2', region: 'Americas', country: 'United States', status: 'paid', amount: 240 }, { id: 'O-3', region: 'Americas', country: 'Canada', status: 'delivered', amount: 410 }, { id: 'O-4', region: 'Americas', country: 'Brazil', status: 'shipped', amount: 180 }, { id: 'O-5', region: 'EMEA', country: 'Germany', status: 'paid', amount: 330 }, { id: 'O-6', region: 'EMEA', country: 'Germany', status: 'returned', amount: 205 }, { id: 'O-7', region: 'EMEA', country: 'France', status: 'open', amount: 155 }, { id: 'O-8', region: 'EMEA', country: 'France', status: 'shipped', amount: 420 }, { id: 'O-9', region: 'APAC', country: 'Japan', status: 'paid', amount: 890 }, { id: 'O-10', region: 'APAC', country: 'Japan', status: 'returned', amount: 240 }, { id: 'O-11', region: 'APAC', country: 'Australia', status: 'open', amount: 175 }, { id: 'O-12', region: 'APAC', country: 'Singapore', status: 'delivered', amount: 1200 }, ] const cols: ColumnDef[] = [ { field: 'id', header: 'ID', width: 90 }, { field: 'region', header: 'Region', width: 110 }, { field: 'country', header: 'Country', width: 160 }, { field: 'status', header: 'Status', width: 110 }, { field: 'amount', header: 'Amount', width: 100, align: 'right' }, ] function mountGrid() { return new Promise<{ api: SvGridApi destroy: () => void }>((res, rej) => { const target = document.createElement('div') document.body.appendChild(target) let api: SvGridApi | null = null const app = mount(SvGrid, { target, props: { data: rows, columns: cols, features, _rowModels: { coreRowModel: createCoreRowModel(), filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(sortFns), }, containerHeight: 360, virtualization: false, columnVirtualization: false, showPagination: false, showColumnFilters: false, showGlobalFilter: false, showRowSelection: false, onApiReady(received: SvGridApi) { api = received res({ api, destroy: () => { unmount(app); target.remove() } }) }, } as any, }) queueMicrotask(() => { if (!api) rej(new Error('onApiReady never fired')) }) }) } const tick = () => new Promise((r) => queueMicrotask(r)) describe('SvGridApi.setFacetFilter - single column', () => { it('narrows to one allowed value', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['EMEA']) await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-5', 'O-6', 'O-7', 'O-8']) } finally { destroy() } }) it('accepts multiple values (OR within column)', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['EMEA', 'APAC']) await tick() const regions = new Set(api.getDisplayedRows().map((r) => r.region)) expect(regions).toEqual(new Set(['EMEA', 'APAC'])) expect(api.getDisplayedRows().length).toBe(8) } finally { destroy() } }) it('empty allowed array filters everything out', async () => { // An empty SET is treated as a clear (matches the implementation - // see `setFacetFilter`'s null branch). Verify the documented // behavior: empty array clears. const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', []) await tick() expect(api.getDisplayedRows().length).toBe(rows.length) } finally { destroy() } }) it('null clears the filter', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['EMEA']) await tick() expect(api.getDisplayedRows().length).toBe(4) api.setFacetFilter('region', null) await tick() expect(api.getDisplayedRows().length).toBe(rows.length) } finally { destroy() } }) }) describe('SvGridApi.setFacetFilter - multi-column AND', () => { it('region + status both narrow', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['EMEA']) api.setFacetFilter('status', ['paid', 'returned']) await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-5', 'O-6']) // EMEA rows whose status is paid or returned } finally { destroy() } }) it('region + country + status all compose', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['APAC']) api.setFacetFilter('country', ['Japan', 'Singapore']) api.setFacetFilter('status', ['delivered']) await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-12']) // APAC + Singapore + delivered } finally { destroy() } }) }) describe('SvGridApi.setFacetFilter - composition with operator filters', () => { it('facet narrows; setFilter further narrows on the same column id', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['Americas', 'EMEA']) api.setFilter('country', { operator: 'startsWith', value: 'g' }) // → Germany await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-5', 'O-6']) } finally { destroy() } }) it('clearing the facet but keeping setFilter only honors setFilter', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('region', ['EMEA']) api.setFilter('country', { operator: 'contains', value: 'fran' }) await tick() expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8']) api.setFacetFilter('region', null) await tick() expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8']) } finally { destroy() } }) }) describe('SvGridApi.setFacetFilter - edge cases', () => { it('unknown allowed values silently match nothing', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('status', ['SHIPPED_IN_TRANSIT', 'PENDING_REVIEW']) await tick() expect(api.getDisplayedRows().length).toBe(0) } finally { destroy() } }) it('mixed known + unknown values: known values still match', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('status', ['shipped', 'UNKNOWN_VALUE']) await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-4', 'O-8']) } finally { destroy() } }) }) describe('SvGridApi.setFacetFilter - simulates the tree-list pattern', () => { // Demo 111's tree picker resolves a hierarchical selection to a flat // list of leaf values, then calls setFacetFilter. These tests // exercise that contract. it('simulates a tree drop: select Americas → all American countries', async () => { const { api, destroy } = await mountGrid() try { // Leaf descendants of the "Americas" tree node: const americansCountries = ['United States', 'Canada', 'Brazil'] api.setFacetFilter('country', americansCountries) await tick() const regions = new Set(api.getDisplayedRows().map((r) => r.region)) expect(regions).toEqual(new Set(['Americas'])) } finally { destroy() } }) it('simulates a partial-tree selection: only Japan + Singapore', async () => { const { api, destroy } = await mountGrid() try { api.setFacetFilter('country', ['Japan', 'Singapore']) await tick() const ids = api.getDisplayedRows().map((r) => r.id) expect(ids).toEqual(['O-9', 'O-10', 'O-12']) } finally { destroy() } }) }) describe('SvGridApi.setFacetFilter - simulates the async loader pattern', () => { // Demo 111's async card emulates a server returning the distinct // emails. The grid is told the final selection via setFacetFilter - // there's no special "loading" code path inside the grid, the // consumer's UI handles that. Verify the contract works regardless of // whether the values were enumerated client-side or server-side. it('async-loaded values: select user-resolved subset', async () => { const { api, destroy } = await mountGrid() try { // Pretend the user clicked these from a server-loaded value list. const userSelected = ['Germany', 'France'] api.setFacetFilter('country', userSelected) await tick() const regions = new Set(api.getDisplayedRows().map((r) => r.region)) expect(regions).toEqual(new Set(['EMEA'])) expect(api.getDisplayedRows().length).toBe(4) } finally { destroy() } }) })