UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

355 lines 17.3 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 { Form } from '../form.js'; import { MarkdownInput } from './markdown-input.js'; describe('MarkdownInput', () => { 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(MarkdownInput, { value: "" }), }); await flushUpdates(); const el = document.querySelector('shade-markdown-input'); expect(el).not.toBeNull(); }); }); it('should render a textarea with the given value', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "# Hello" }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea).not.toBeNull(); expect(textarea.value).toBe('# Hello'); }); }); it('should render the label title', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", labelTitle: "Markdown Content" }), }); await flushUpdates(); const label = document.querySelector('shade-markdown-input label'); expect(label?.textContent).toContain('Markdown Content'); }); }); it('should set placeholder on textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", placeholder: "Type markdown..." }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea.placeholder).toBe('Type markdown...'); }); }); it('should set data-disabled when disabled', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", disabled: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.hasAttribute('data-disabled')).toBe(true); const textarea = wrapper.querySelector('textarea'); expect(textarea.disabled).toBe(true); }); }); it('should set readOnly on textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", readOnly: true }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea.readOnly).toBe(true); }); }); it('should call onValueChange on input event', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", onValueChange: onValueChange }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); textarea.value = '# New content'; textarea.dispatchEvent(new Event('input', { bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith('# New content'); }); }); it('should use custom rows prop', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", rows: 20 }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea.rows).toBe(20); }); }); it('should set name attribute on textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", name: "description" }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea.name).toBe('description'); }); }); it('should set required attribute on textarea', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", required: true }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); expect(textarea.required).toBe(true); }); }); 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(MarkdownInput, { value: "", required: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.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(MarkdownInput, { value: "some content", required: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.hasAttribute('data-invalid')).toBe(false); }); }); it('should show validation error message from getValidationResult', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(MarkdownInput, { value: "short", getValidationResult: ({ value }) => value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true } })), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.hasAttribute('data-invalid')).toBe(true); expect(wrapper.textContent).toContain('Too short'); }); }); it('should show helper text from getHelperText', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", getHelperText: () => 'Write some markdown here' }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.textContent).toContain('Write some markdown here'); }); }); describe('hideChrome', () => { it('should suppress the label when hideChrome is true', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", labelTitle: "My Label", hideChrome: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); const spans = wrapper.querySelectorAll('label > span'); const labelSpan = Array.from(spans).find((s) => s.textContent === 'My Label'); expect(labelSpan).toBeUndefined(); }); }); it('should suppress the helper text when hideChrome is true', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(MarkdownInput, { value: "short", hideChrome: true, getValidationResult: ({ value }) => value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true } })), }); await flushUpdates(); const helperText = document.querySelector('shade-markdown-input .helperText'); expect(helperText).toBeNull(); }); }); it('should still set data-invalid when hideChrome is true', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", required: true, hideChrome: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.hasAttribute('data-invalid')).toBe(true); }); }); }); it('should render with validation inside a Form', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Form, { onSubmit: () => { }, validate: (_data) => true }, createComponent(MarkdownInput, { value: "short", name: "content", getValidationResult: ({ value }) => value.length < 10 ? { isValid: false, message: 'Too short' } : { isValid: true } }))), }); await flushUpdates(); const wrapper = document.querySelector('shade-markdown-input'); expect(wrapper.hasAttribute('data-invalid')).toBe(true); expect(wrapper.textContent).toContain('Too short'); const textarea = wrapper.querySelector('textarea'); expect(textarea.name).toBe('content'); }); }); describe('image paste', () => { const createPasteEvent = (items) => { const pasteEvent = new Event('paste', { bubbles: true, cancelable: true }); Object.defineProperty(pasteEvent, 'clipboardData', { value: { items: Object.assign(items.map((item) => ({ type: item.type, getAsFile: () => item.file, })), { length: items.length }), }, }); return pasteEvent; }; it('should inline a pasted image as base64 Markdown', async () => { const originalFileReader = globalThis.FileReader; try { const fakeBase64 = 'data:image/png;base64,dGVzdA=='; globalThis.FileReader = class { result = fakeBase64; onload = null; onerror = null; readAsDataURL() { queueMicrotask(() => this.onload?.()); } }; await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "Hello ", onValueChange: onValueChange }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); textarea.selectionStart = 6; textarea.selectionEnd = 6; const file = new File(['png-data'], 'test.png', { type: 'image/png' }); const pasteEvent = createPasteEvent([{ type: 'image/png', file }]); textarea.dispatchEvent(pasteEvent); await flushUpdates(); expect(onValueChange).toHaveBeenCalledOnce(); const result = onValueChange.mock.calls[0][0]; expect(result).toContain('![pasted image](data:'); expect(result.startsWith('Hello ')).toBe(true); }); } finally { globalThis.FileReader = originalFileReader; } }); it('should ignore pasted images exceeding maxImageSizeBytes', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", onValueChange: onValueChange, maxImageSizeBytes: 5 }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); const file = new File(['this-is-larger-than-5-bytes'], 'big.png', { type: 'image/png' }); const pasteEvent = createPasteEvent([{ type: 'image/png', file }]); textarea.dispatchEvent(pasteEvent); await flushUpdates(); expect(onValueChange).not.toHaveBeenCalled(); }); }); it('should not interfere with non-image paste', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(MarkdownInput, { value: "", onValueChange: onValueChange }), }); await flushUpdates(); const textarea = document.querySelector('shade-markdown-input textarea'); const file = new File(['text content'], 'note.txt', { type: 'text/plain' }); const pasteEvent = createPasteEvent([{ type: 'text/plain', file }]); const wasDefaultPrevented = !textarea.dispatchEvent(pasteEvent); await flushUpdates(); expect(wasDefaultPrevented).toBe(false); expect(onValueChange).not.toHaveBeenCalled(); }); }); }); }); //# sourceMappingURL=markdown-input.spec.js.map