UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

388 lines 20 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 { MarkdownEditor } from './markdown-editor.js'; describe('MarkdownEditor', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; vi.restoreAllMocks(); }); it('should render as custom element', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello" }), }); await flushUpdates(); const el = document.querySelector('shade-markdown-editor'); expect(el).not.toBeNull(); }); }); describe('side-by-side layout (default)', () => { it('should render input and preview panes side by side', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello" }), }); await flushUpdates(); const split = document.querySelector('.md-editor-split'); expect(split).not.toBeNull(); expect(split.dataset.layout).toBe('side-by-side'); const input = document.querySelector('shade-markdown-editor shade-markdown-input'); expect(input).not.toBeNull(); const display = document.querySelector('shade-markdown-editor shade-markdown-display'); expect(display).not.toBeNull(); }); }); }); describe('above-below layout', () => { it('should render with above-below layout', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "above-below" }), }); await flushUpdates(); const split = document.querySelector('.md-editor-split'); expect(split).not.toBeNull(); expect(split.dataset.layout).toBe('above-below'); }); }); }); describe('tabs layout', () => { it('should render with tabs layout', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "tabs" }), }); await flushUpdates(); const tabs = document.querySelector('shade-markdown-editor shade-tabs'); expect(tabs).not.toBeNull(); const tabButtons = document.querySelectorAll('shade-markdown-editor .shade-tab-btn'); expect(tabButtons.length).toBe(2); }); }); it('should show the edit tab by default', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "tabs" }), }); await flushUpdates(); const input = document.querySelector('shade-markdown-editor shade-markdown-input'); expect(input).not.toBeNull(); }); }); }); it('should pass value to both input and display', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const mdContent = '# Test Content'; initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: mdContent }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.value).toBe(mdContent); const heading = document.querySelector('shade-markdown-editor shade-markdown-display [is^="shade-typography"][data-variant="h1"]'); expect(heading).not.toBeNull(); expect(heading?.textContent).toContain('Test Content'); }); }); describe('form integration', () => { it('should render a label when labelTitle is provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", labelTitle: "Description" }), }); await flushUpdates(); const label = document.querySelector('shade-markdown-editor .md-editor-label'); expect(label).not.toBeNull(); expect(label?.textContent).toBe('Description'); }); }); it('should not render a label when labelTitle is not provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "" }), }); await flushUpdates(); const label = document.querySelector('shade-markdown-editor .md-editor-label'); expect(label).toBeNull(); }); }); it('should set data-invalid when required and value is empty', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", required: true }), }); await flushUpdates(); const editor = document.querySelector('shade-markdown-editor'); expect(editor.hasAttribute('data-invalid')).toBe(true); }); }); it('should not set data-invalid when required and value is provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "some content", required: true }), }); await flushUpdates(); const editor = document.querySelector('shade-markdown-editor'); expect(editor.hasAttribute('data-invalid')).toBe(false); }); }); it('should show "Value is required" helper text when required and empty', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", required: true }), }); await flushUpdates(); const helperText = document.querySelector('shade-markdown-editor .md-editor-helperText'); expect(helperText).not.toBeNull(); expect(helperText?.textContent).toBe('Value is required'); }); }); it('should set data-invalid when getValidationResult returns invalid', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(MarkdownEditor, { value: "short", getValidationResult: ({ value }) => value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true } })), }); await flushUpdates(); const editor = document.querySelector('shade-markdown-editor'); expect(editor.hasAttribute('data-invalid')).toBe(true); expect(editor.textContent).toContain('Too short'); }); }); it('should not set data-invalid when getValidationResult returns valid', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(MarkdownEditor, { value: "long enough content", getValidationResult: ({ value }) => value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true } })), }); await flushUpdates(); const editor = document.querySelector('shade-markdown-editor'); expect(editor.hasAttribute('data-invalid')).toBe(false); }); }); it('should display helper text from getHelperText', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", getHelperText: () => 'Enter your description' }), }); await flushUpdates(); const helperText = document.querySelector('shade-markdown-editor .md-editor-helperText'); expect(helperText).not.toBeNull(); expect(helperText?.textContent).toBe('Enter your description'); }); }); it('should forward name prop to the inner textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", name: "description" }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.name).toBe('description'); }); }); it('should forward required prop to the inner textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "content", required: true }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.required).toBe(true); }); }); it('should forward disabled prop to the inner textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", disabled: true }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.disabled).toBe(true); }); }); it('should forward placeholder prop to the inner textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", placeholder: "Type here..." }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.placeholder).toBe('Type here...'); }); }); it('should forward rows prop to the inner textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", rows: 5 }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea.rows).toBe(5); }); }); it('should set hideChrome on the inner MarkdownInput', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "", labelTitle: "My Label" }), }); await flushUpdates(); const editorLabel = document.querySelector('shade-markdown-editor .md-editor-label'); expect(editorLabel?.textContent).toBe('My Label'); const inputLabel = document.querySelector('shade-markdown-editor shade-markdown-input label > span'); expect(inputLabel).toBeNull(); }); }); }); describe('keyboard navigation', () => { it('should have a focusable textarea in side-by-side layout', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello" }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-editor textarea'); expect(textarea).not.toBeNull(); textarea.focus(); expect(document.activeElement).toBe(textarea); }); }); it('should have focusable tab buttons in tabs layout', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "tabs" }), }); await flushUpdates(); const tabButtons = document.querySelectorAll('shade-markdown-editor .shade-tab-btn'); expect(tabButtons.length).toBe(2); tabButtons[0].focus(); expect(document.activeElement).toBe(tabButtons[0]); tabButtons[1].focus(); expect(document.activeElement).toBe(tabButtons[1]); }); }); it('should use tabIndex to indicate active tab in controlled tabs layout', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "tabs" }), }); await flushUpdates(); const tabButtons = document.querySelectorAll('shade-markdown-editor .shade-tab-btn'); const activeTab = Array.from(tabButtons).find((btn) => btn.classList.contains('active')); const inactiveTab = Array.from(tabButtons).find((btn) => !btn.classList.contains('active')); expect(activeTab).not.toBeUndefined(); expect(inactiveTab).not.toBeUndefined(); expect(activeTab?.tabIndex).toBe(0); expect(inactiveTab?.tabIndex).toBe(-1); }); }); it('should switch tabs when tab button is clicked via keyboard activation', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownEditor, { value: "# Hello", layout: "tabs" }), }); await flushUpdates(); const tabButtons = document.querySelectorAll('shade-markdown-editor .shade-tab-btn'); const previewButton = Array.from(tabButtons).find((btn) => btn.textContent?.includes('Preview')); expect(previewButton).not.toBeUndefined(); previewButton.click(); await flushUpdates(); const display = document.querySelector('shade-markdown-editor shade-markdown-display'); expect(display).not.toBeNull(); }); }); it('should have focusable interactive elements in preview pane', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(MarkdownEditor, { value: '- [ ] Task A\n- [x] Task B\n\n[A link](https://example.com)', onValueChange: () => { } })), }); await flushUpdates(); const checkboxes = document.querySelectorAll('shade-markdown-editor shade-markdown-display shade-checkbox input[type="checkbox"]'); expect(checkboxes.length).toBe(2); const link = document.querySelector('shade-markdown-editor shade-markdown-display .md-link'); expect(link).not.toBeNull(); checkboxes[0].focus(); expect(document.activeElement).toBe(checkboxes[0]); }); }); }); }); //# sourceMappingURL=markdown-editor.spec.js.map