UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

328 lines 16.9 kB
import { createInjector } from '@furystack/inject'; import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'; import { usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CollectionService } from '../../services/collection-service.js'; import { DataGridRow } from './data-grid-row.js'; describe('DataGridRow', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; // Mock scrollTo for jsdom Element.prototype.scrollTo = vi.fn(); }); afterEach(() => { document.body.innerHTML = ''; }); const renderRow = async (props) => { const injector = createInjector(); const root = document.getElementById('root'); // Create shade-data-grid element manually to simulate production structure // shade-data-grid is the scrollable container in production const shadeDataGrid = document.createElement('shade-data-grid'); shadeDataGrid.style.overflow = 'auto'; shadeDataGrid.style.height = '200px'; shadeDataGrid.style.display = 'block'; root.appendChild(shadeDataGrid); initializeShadeRoot({ injector, rootElement: shadeDataGrid, jsxElement: (createComponent("div", { className: "shade-grid-wrapper" }, createComponent("table", null, createComponent("thead", null, createComponent("tr", null, createComponent("th", null, "ID"), createComponent("th", null, "Name"))), createComponent("tbody", null, createComponent(DataGridRow, { entry: props.entry, service: props.service, columns: props.columns ?? ['id', 'name'], onRowClick: props.onRowClick, onRowDoubleClick: props.onRowDoubleClick, focusedRowStyle: props.focusedRowStyle, selectedRowStyle: props.selectedRowStyle, unfocusedRowStyle: props.unfocusedRowStyle, unselectedRowStyle: props.unselectedRowStyle, rowComponents: props.rowComponents }))))), }); await flushUpdates(); return { injector, getRow: () => root.querySelector('shades-data-grid-row'), getCells: () => root.querySelectorAll('td'), getScrollContainer: () => shadeDataGrid, [Symbol.asyncDispose]: () => injector[Symbol.asyncDispose](), }; }; describe('rendering', () => { it('should render as a table row element', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row).toBeTruthy(); expect(row?.tagName.toLowerCase()).toBe('shades-data-grid-row'); }); }); }); it('should render a cell for each column', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service, columns: ['id', 'name'] }), async ({ getCells }) => { expect(getCells().length).toBe(2); }); }); }); it('should render entry property values in cells', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 42, name: 'Test Entry' }; await usingAsync(await renderRow({ entry, service }), async ({ getCells }) => { const cells = getCells(); expect(cells[0]?.textContent).toBe('42'); expect(cells[1]?.textContent).toBe('Test Entry'); }); }); }); it('should use custom row components when provided', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Custom' }; await usingAsync(await renderRow({ entry, service, rowComponents: { id: (e) => createComponent("span", { "data-testid": "custom-id" }, "ID: ", e.id), name: (e) => createComponent("strong", { "data-testid": "custom-name" }, e.name), }, }), async ({ getCells }) => { const cells = getCells(); expect(cells[0]?.querySelector('[data-testid="custom-id"]')?.textContent).toContain('ID: 1'); expect(cells[1]?.querySelector('[data-testid="custom-name"]')?.textContent).toBe('Custom'); }); }); }); }); describe('selection state', () => { it('should not have selected class when entry is not selected', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(false); }); }); }); it('should have selected class when entry is in selection', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.selection.setValue([entry]); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(true); }); }); }); it('should update selected class when selection changes', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(false); service.selection.setValue([entry]); await flushUpdates(); expect(row?.hasAttribute('data-selected')).toBe(true); service.selection.setValue([]); await flushUpdates(); expect(row?.hasAttribute('data-selected')).toBe(false); }); }); }); it('should set aria-selected attribute based on selection', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.getAttribute('aria-selected')).toBe('false'); service.selection.setValue([entry]); await flushUpdates(); expect(row?.getAttribute('aria-selected')).toBe('true'); }); }); }); it('should apply selectedRowStyle when entry is selected', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.selection.setValue([entry]); await usingAsync(await renderRow({ entry, service, selectedRowStyle: { backgroundColor: 'rgb(255, 0, 0)' }, }), async ({ getRow }) => { const row = getRow(); expect(row?.style.backgroundColor).toBe('rgb(255, 0, 0)'); }); }); }); it('should apply unselectedRowStyle when entry is not selected', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service, unselectedRowStyle: { backgroundColor: 'rgb(0, 255, 0)' }, }), async ({ getRow }) => { const row = getRow(); expect(row?.style.backgroundColor).toBe('rgb(0, 255, 0)'); }); }); }); }); describe('focus state', () => { it('should not have focused class when entry is not focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-focused')).toBe(false); }); }); }); it('should have focused class when entry is focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.focusedEntry.setValue(entry); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-focused')).toBe(true); }); }); }); it('should update focused class when focus changes', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-focused')).toBe(false); service.focusedEntry.setValue(entry); await flushUpdates(); expect(row?.hasAttribute('data-focused')).toBe(true); service.focusedEntry.setValue(undefined); await flushUpdates(); expect(row?.hasAttribute('data-focused')).toBe(false); }); }); }); it('should apply focusedRowStyle when entry is focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.focusedEntry.setValue(entry); await usingAsync(await renderRow({ entry, service, focusedRowStyle: { fontWeight: 'bold' }, }), async ({ getRow }) => { const row = getRow(); expect(row?.style.fontWeight).toBe('bold'); }); }); }); it('should apply unfocusedRowStyle when entry is not focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service, unfocusedRowStyle: { opacity: '0.8' }, }), async ({ getRow }) => { const row = getRow(); expect(row?.style.opacity).toBe('0.8'); }); }); }); it('should not have focused class when different entry is focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; const otherEntry = { id: 2, name: 'Other' }; service.focusedEntry.setValue(otherEntry); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-focused')).toBe(false); }); }); }); }); describe('click handlers', () => { it('should call onRowClick when cell is clicked', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; const onRowClick = vi.fn(); await usingAsync(await renderRow({ entry, service, onRowClick }), async ({ getCells }) => { const cell = getCells()[0]; cell.click(); expect(onRowClick).toHaveBeenCalledWith(entry, expect.any(MouseEvent)); }); }); }); it('should call onRowDoubleClick when cell is double-clicked', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; const onRowDoubleClick = vi.fn(); await usingAsync(await renderRow({ entry, service, onRowDoubleClick }), async ({ getCells }) => { const cell = getCells()[0]; const dblClickEvent = new MouseEvent('dblclick', { bubbles: true }); cell.dispatchEvent(dblClickEvent); expect(onRowDoubleClick).toHaveBeenCalledWith(entry, expect.any(MouseEvent)); }); }); }); it('should not throw when onRowClick is not provided', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getCells }) => { const cell = getCells()[0]; expect(() => cell.click()).not.toThrow(); }); }); }); it('should not throw when onRowDoubleClick is not provided', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; await usingAsync(await renderRow({ entry, service }), async ({ getCells }) => { const cell = getCells()[0]; const dblClickEvent = new MouseEvent('dblclick', { bubbles: true }); expect(() => cell.dispatchEvent(dblClickEvent)).not.toThrow(); }); }); }); }); describe('combined selection and focus states', () => { it('should have both selected and focused classes when applicable', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.selection.setValue([entry]); service.focusedEntry.setValue(entry); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(true); expect(row?.hasAttribute('data-focused')).toBe(true); }); }); }); it('should be selected but not focused', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; const otherEntry = { id: 2, name: 'Other' }; service.selection.setValue([entry]); service.focusedEntry.setValue(otherEntry); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(true); expect(row?.hasAttribute('data-focused')).toBe(false); }); }); }); it('should be focused but not selected', async () => { await usingAsync(new CollectionService(), async (service) => { const entry = { id: 1, name: 'Test' }; service.focusedEntry.setValue(entry); await usingAsync(await renderRow({ entry, service }), async ({ getRow }) => { const row = getRow(); expect(row?.hasAttribute('data-selected')).toBe(false); expect(row?.hasAttribute('data-focused')).toBe(true); }); }); }); }); }); //# sourceMappingURL=data-grid-row.spec.js.map