From fa75e028e0bef104e0799860702424b608a417a4 Mon Sep 17 00:00:00 2001 From: Jerry Thomas Date: Fri, 10 Jan 2025 21:52:31 -0600 Subject: [PATCH] feat: added helpers, components item, pill, switch and toggle --- bun.lockb | Bin 228192 -> 229472 bytes packages/actions/README.md | 57 + packages/actions/package.json | 38 + packages/actions/spec/index.spec.js | 9 + packages/actions/spec/keyboard.spec.svelte.js | 156 ++ packages/actions/spec/utils.spec.js | 53 + packages/actions/src/index.js | 3 + packages/actions/src/keyboard.svelte.js | 58 + packages/actions/src/types.js | 10 + packages/actions/src/utils.js | 28 + packages/actions/tsconfig.json | 8 + packages/actions/vitest.config.js | 2 + packages/atoms/package.json | 29 + packages/atoms/spec/Icon.spec.svelte.js | 32 + packages/atoms/spec/Item.spec.svelte.js | 41 + packages/atoms/spec/Pill.spec.svelte.js | 49 + packages/atoms/spec/Switch.spec.svelte.js | 132 ++ packages/atoms/spec/Toggle.spec.svelte.js | 135 ++ .../__snapshots__/Icon.spec.svelte.js.snap | 81 + .../spec/__snapshots__/Input.spec.js.snap | 30 + .../__snapshots__/Input.spec.svelte.js.snap | 153 ++ .../InputCheckbox.spec.svelte.js.snap | 19 + .../InputRadio.spec.svelte.js.snap | 200 +++ .../__snapshots__/Item.spec.svelte.js.snap | 99 ++ .../__snapshots__/Pill.spec.svelte.js.snap | 134 ++ .../__snapshots__/Switch.spec.svelte.js.snap | 1445 +++++++++++++++++ .../__snapshots__/Toggle.spec.svelte.js.snap | 488 ++++++ packages/atoms/spec/index.spec.js | 9 + packages/atoms/src/Icon.svelte | 15 + packages/atoms/src/Item.svelte | 18 + packages/atoms/src/Pill.svelte | 34 + packages/atoms/src/Switch.svelte | 91 ++ packages/atoms/src/Toggle.svelte | 48 + packages/atoms/src/index.js | 1 + packages/atoms/tsconfig.build.json | 11 + packages/core/package.json | 5 +- packages/core/src/field-mapper.js | 12 +- packages/helpers/package.json | 33 + packages/helpers/spec/index.spec.js | 14 + packages/helpers/spec/matchers/action.spec.js | 102 ++ packages/helpers/spec/matchers/array.spec.js | 33 + .../helpers/spec/matchers/dataset.spec.js | 32 + packages/helpers/spec/matchers/event.spec.js | 35 + packages/helpers/spec/matchers/index.spec.js | 15 + packages/helpers/spec/mocks/animate.spec.js | 24 + packages/helpers/spec/mocks/element.spec.js | 136 ++ packages/helpers/spec/mocks/index.spec.js | 16 + .../helpers/spec/mocks/match-media.spec.js | 75 + .../spec/mocks/resize-observer.spec.js | 52 + packages/helpers/spec/simulator.spec.js | 96 ++ .../helpers/src/components/MockItem.svelte | 5 + packages/helpers/src/index.js | 1 + packages/helpers/src/matchers/action.js | 88 + packages/helpers/src/matchers/array.js | 16 + packages/helpers/src/matchers/dataset.js | 18 + packages/helpers/src/matchers/event.js | 18 + packages/helpers/src/matchers/index.js | 4 + packages/helpers/src/matchers/internal.js | 9 + packages/helpers/src/mocks/animate.js | 26 + packages/helpers/src/mocks/element.js | 83 + packages/helpers/src/mocks/index.js | 7 + packages/helpers/src/mocks/match-media.js | 90 + packages/helpers/src/mocks/resize-observer.js | 25 + packages/helpers/src/simulators/touch.js | 67 + packages/helpers/vitest.config.js | 2 + packages/input/package.json | 5 +- 66 files changed, 4854 insertions(+), 6 deletions(-) create mode 100644 packages/actions/README.md create mode 100644 packages/actions/package.json create mode 100644 packages/actions/spec/index.spec.js create mode 100644 packages/actions/spec/keyboard.spec.svelte.js create mode 100644 packages/actions/spec/utils.spec.js create mode 100644 packages/actions/src/index.js create mode 100644 packages/actions/src/keyboard.svelte.js create mode 100644 packages/actions/src/types.js create mode 100644 packages/actions/src/utils.js create mode 100644 packages/actions/tsconfig.json create mode 100644 packages/actions/vitest.config.js create mode 100644 packages/atoms/package.json create mode 100644 packages/atoms/spec/Icon.spec.svelte.js create mode 100644 packages/atoms/spec/Item.spec.svelte.js create mode 100644 packages/atoms/spec/Pill.spec.svelte.js create mode 100644 packages/atoms/spec/Switch.spec.svelte.js create mode 100644 packages/atoms/spec/Toggle.spec.svelte.js create mode 100644 packages/atoms/spec/__snapshots__/Icon.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Input.spec.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Input.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/InputCheckbox.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/InputRadio.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Item.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Pill.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Switch.spec.svelte.js.snap create mode 100644 packages/atoms/spec/__snapshots__/Toggle.spec.svelte.js.snap create mode 100644 packages/atoms/spec/index.spec.js create mode 100644 packages/atoms/src/Icon.svelte create mode 100644 packages/atoms/src/Item.svelte create mode 100644 packages/atoms/src/Pill.svelte create mode 100644 packages/atoms/src/Switch.svelte create mode 100644 packages/atoms/src/Toggle.svelte create mode 100644 packages/atoms/src/index.js create mode 100644 packages/atoms/tsconfig.build.json create mode 100644 packages/helpers/package.json create mode 100644 packages/helpers/spec/index.spec.js create mode 100644 packages/helpers/spec/matchers/action.spec.js create mode 100644 packages/helpers/spec/matchers/array.spec.js create mode 100644 packages/helpers/spec/matchers/dataset.spec.js create mode 100644 packages/helpers/spec/matchers/event.spec.js create mode 100644 packages/helpers/spec/matchers/index.spec.js create mode 100644 packages/helpers/spec/mocks/animate.spec.js create mode 100644 packages/helpers/spec/mocks/element.spec.js create mode 100644 packages/helpers/spec/mocks/index.spec.js create mode 100644 packages/helpers/spec/mocks/match-media.spec.js create mode 100644 packages/helpers/spec/mocks/resize-observer.spec.js create mode 100644 packages/helpers/spec/simulator.spec.js create mode 100644 packages/helpers/src/components/MockItem.svelte create mode 100644 packages/helpers/src/index.js create mode 100644 packages/helpers/src/matchers/action.js create mode 100644 packages/helpers/src/matchers/array.js create mode 100644 packages/helpers/src/matchers/dataset.js create mode 100644 packages/helpers/src/matchers/event.js create mode 100644 packages/helpers/src/matchers/index.js create mode 100644 packages/helpers/src/matchers/internal.js create mode 100644 packages/helpers/src/mocks/animate.js create mode 100644 packages/helpers/src/mocks/element.js create mode 100644 packages/helpers/src/mocks/index.js create mode 100644 packages/helpers/src/mocks/match-media.js create mode 100644 packages/helpers/src/mocks/resize-observer.js create mode 100644 packages/helpers/src/simulators/touch.js create mode 100644 packages/helpers/vitest.config.js diff --git a/bun.lockb b/bun.lockb index 07b6f91342cd46c5ddbcdfee8ec0897c3f7f7636..3584cde52dd160b285f87d95b56914c63cb5476a 100755 GIT binary patch delta 34031 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

kMj-;Q3Da&2vyXTq-U`p1- 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ë<~c(Cx#VLK@X0#} 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 diff --git a/packages/actions/README.md b/packages/actions/README.md new file mode 100644 index 00000000..eb44ed8c --- /dev/null +++ b/packages/actions/README.md @@ -0,0 +1,57 @@ +# Actions + +This package provides a set of actions that can be used to perform various tasks. + +## Keyboard + +The keyboard action can be used to map keyboard events to actions. The following example shows how to map the `K` key combination to an action: + +The default behavior is to listen to keyup events. + +- [x] Configuration driven +- [x] Custom events can be defined +- [x] Supports mapping an array of keys to an event +- [x] Supports mapping a regex to an event +- [ ] Support key modifiers +- [ ] Support a combination of regex patterns and array of keys + +Default configuration + +- _add_: alphabet keys cause an `add` event +- _submit_: enter causes a `submit` event +- _cancel_: escape causes a `cancel` event +- _delete_: backspace or delete causes a `delete` event + +### Basic Usage + +```svelte + + +

+``` + +### Custom Events + +```svelte + + +
+``` diff --git a/packages/actions/package.json b/packages/actions/package.json new file mode 100644 index 00000000..8ea8592f --- /dev/null +++ b/packages/actions/package.json @@ -0,0 +1,38 @@ +{ + "name": "@rokkit/actions", + "version": "1.0.0-next.0", + "description": "Contains generic actions that can be used in various components.", + "author": "Jerry Thomas ", + "license": "MIT", + "main": "index.js", + "module": "src/index.js", + "types": "dist/index.d.ts", + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepublishOnly": "tsc --project tsconfig.build.json", + "clean": "rm -rf dist", + "build": "pnpm clean && pnpm prepublishOnly" + }, + "files": [ + "src/**/*.js", + "src/**/*.svelte" + ], + "exports": { + "./src": "./src", + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.js", + "svelte": "./src/index.js" + } + }, + "dependencies": { + "ramda": "^0.30.1" + }, + "devDependencies": { + "@rokkit/helpers": "workspace:*" + } +} diff --git a/packages/actions/spec/index.spec.js b/packages/actions/spec/index.spec.js new file mode 100644 index 00000000..976fadfe --- /dev/null +++ b/packages/actions/spec/index.spec.js @@ -0,0 +1,9 @@ +import { describe, it, expect } from 'vitest' +// skipcq: JS-C1003 - Importing all components for verification +import * as actions from '../src/index.js' + +describe('actions', () => { + it('should contain all exported actions', () => { + expect(Object.keys(actions)).toEqual(['keyboard']) + }) +}) diff --git a/packages/actions/spec/keyboard.spec.svelte.js b/packages/actions/spec/keyboard.spec.svelte.js new file mode 100644 index 00000000..55a7eb9d --- /dev/null +++ b/packages/actions/spec/keyboard.spec.svelte.js @@ -0,0 +1,156 @@ +import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' +import { flushSync } from 'svelte' +import { keyboard } from '../src/keyboard.svelte.js' +import { toHaveBeenDispatchedWith } from '@rokkit/helpers/matchers' + +expect.extend({ toHaveBeenDispatchedWith }) + +describe('keyboard', () => { + const node = document.createElement('div') + const spies = { + add: vi.fn(), + remove: vi.fn(), + submit: vi.fn() + } + + const keys = ['a', 'b', 'Enter', 'Backspace'] + keys.forEach((key) => { + const button = document.createElement('button') + button.setAttribute('data-key', key) + node.appendChild(button) + }) + + beforeEach(() => { + Object.entries(spies).forEach(([event, callback]) => { + node.addEventListener(event, callback) + }) + }) + + afterEach(() => { + Object.entries(spies).forEach(([event, callback]) => { + node.removeEventListener(event, callback) + }) + vi.resetAllMocks() + }) + + it('should add and remove keyup handlers to document', () => { + const addEventListenerSpy = vi.spyOn(document, 'addEventListener') + const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') + const removeEventOnNodeSpy = vi.spyOn(node, 'removeEventListener') + const addEventOnNodeSpy = vi.spyOn(node, 'addEventListener') + + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + expect(addEventListenerSpy).toHaveBeenCalledTimes(1) + expect(addEventListenerSpy).toHaveBeenNthCalledWith(1, 'keyup', expect.any(Function), {}) + expect(addEventOnNodeSpy).toHaveBeenCalledTimes(1) + expect(addEventOnNodeSpy).toHaveBeenNthCalledWith(1, 'click', expect.any(Function), {}) + + cleanup() + expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) + expect(removeEventListenerSpy).toHaveBeenNthCalledWith(1, 'keyup', expect.any(Function), {}) + expect(removeEventOnNodeSpy).toHaveBeenCalledTimes(1) + expect(removeEventOnNodeSpy).toHaveBeenNthCalledWith(1, 'click', expect.any(Function), {}) + + addEventListenerSpy.mockRestore() + removeEventListenerSpy.mockRestore() + removeEventOnNodeSpy.mockRestore() + addEventOnNodeSpy.mockRestore() + }) + + it('should not dispatch any event when an unmapped key is pressed', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + document.dispatchEvent(new KeyboardEvent('keyup', { key: '-' })) + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).not.toHaveBeenCalled() + expect(spies.submit).not.toHaveBeenCalled() + cleanup() + }) + + it('should dispatch "add" event when an alphabet is pressed', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' })) + expect(spies.add).toHaveBeenDispatchedWith('a') + expect(spies.remove).not.toHaveBeenCalled() + expect(spies.submit).not.toHaveBeenCalled() + + cleanup() + }) + + it('should dispatch "remove" event when delete is pressed', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Delete' })) + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).toHaveBeenDispatchedWith('Delete') + expect(spies.submit).not.toHaveBeenCalled() + + cleanup() + }) + + it('should dispatch "remove" event when backspace is pressed', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Backspace' })) + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).toHaveBeenDispatchedWith('Backspace') + expect(spies.submit).not.toHaveBeenCalled() + + cleanup() + }) + + it('should dispatch "submit" event when backspace is pressed', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })) + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).not.toHaveBeenCalled() + expect(spies.submit).toHaveBeenDispatchedWith('Enter') + + cleanup() + }) + + it('should dispatch "add" event when an alphabet is clicked', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + node.querySelector('[data-key="a"]').click() + expect(spies.add).toHaveBeenDispatchedWith('a') + expect(spies.remove).not.toHaveBeenCalled() + expect(spies.submit).not.toHaveBeenCalled() + + cleanup() + }) + + it('should dispatch "remove" event when delete is clicked', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + node.querySelector('[data-key="Backspace"]').click() + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).toHaveBeenDispatchedWith('Backspace') + expect(spies.submit).not.toHaveBeenCalled() + + cleanup() + }) + + it('should dispatch "submit" event when enter is clicked', () => { + const cleanup = $effect.root(() => keyboard(node)) + flushSync() + + node.querySelector('[data-key="Enter"]').click() + expect(spies.add).not.toHaveBeenCalled() + expect(spies.remove).not.toHaveBeenCalled() + expect(spies.submit).toHaveBeenDispatchedWith('Enter') + + cleanup() + }) +}) diff --git a/packages/actions/spec/utils.spec.js b/packages/actions/spec/utils.spec.js new file mode 100644 index 00000000..49d6c3db --- /dev/null +++ b/packages/actions/spec/utils.spec.js @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import { getClosestAncestorWithAttribute, getEventForKey } from '../src/utils.js' + +describe('utils', () => { + describe('getClosestAncestorWithAttribute', () => { + it('should return null if element does not have the given attribute and is orphan', () => { + const element = document.createElement('div') + expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(null) + }) + it('should return the element if it has the given attribute', () => { + const element = document.createElement('div') + element.setAttribute('data-test', 'test') + expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(element) + }) + it('should return the element if it has the given attribute and is nested', () => { + const parent = document.createElement('div') + const element = document.createElement('div') + element.setAttribute('data-test', 'test') + parent.appendChild(element) + expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(element) + }) + it('should return the closest ancestor if it has the given attribute', () => { + const element = document.createElement('div') + const parent = document.createElement('div') + parent.setAttribute('data-test', 'test') + parent.appendChild(element) + expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(parent) + }) + }) + + describe('getEventForKey', () => { + const keyMapping = { + event1: ['a', 'b', 'c'], + event2: /d/, + event3: ['e', 'f'], + event4: /g/ + } + + it('should return the correct event name for a key in an array', () => { + expect(getEventForKey(keyMapping, 'a')).toBe('event1') + expect(getEventForKey(keyMapping, 'f')).toBe('event3') + }) + + it('should return the correct event name for a key matching a regex', () => { + expect(getEventForKey(keyMapping, 'd')).toBe('event2') + expect(getEventForKey(keyMapping, 'g')).toBe('event4') + }) + + it('should return null for a key that does not match any event', () => { + expect(getEventForKey(keyMapping, 'z')).toBeNull() + }) + }) +}) diff --git a/packages/actions/src/index.js b/packages/actions/src/index.js new file mode 100644 index 00000000..0eeecd24 --- /dev/null +++ b/packages/actions/src/index.js @@ -0,0 +1,3 @@ +// skipcq: JS-E1004 - Needed for exposing all types +export * from './types.js' +export { keyboard } from './keyboard.svelte.js' diff --git a/packages/actions/src/keyboard.svelte.js b/packages/actions/src/keyboard.svelte.js new file mode 100644 index 00000000..5e9905eb --- /dev/null +++ b/packages/actions/src/keyboard.svelte.js @@ -0,0 +1,58 @@ +import { on } from 'svelte/events' +import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js' + +/** + * Default key mappings + * @type {import('./types.js').KeyboardConfig} + */ +const defaultKeyMappings = { + remove: ['Backspace', 'Delete'], + submit: ['Enter'], + add: /^[a-zA-Z]$/ +} + +/** + * Handle keyboard events + * + * @param {HTMLElement} root + * @param {import('./types.js').KeyboardConfig} options - Custom key mappings + */ +export function keyboard(root, options = defaultKeyMappings) { + const keyMappings = options || defaultKeyMappings + + /** + * Handle keyboard events + * + * @param {KeyboardEvent} event + */ + const keyup = (event) => { + const { key } = event + const eventName = getEventForKey(keyMappings, key) + + if (eventName) { + root.dispatchEvent(new CustomEvent(eventName, { detail: key })) + } + } + + const click = (event) => { + const node = getClosestAncestorWithAttribute(event.target, 'data-key') + + if (node) { + const key = node.getAttribute('data-key') + const eventName = getEventForKey(keyMappings, key) + + if (eventName) { + root.dispatchEvent(new CustomEvent(eventName, { detail: key })) + } + } + } + + $effect(() => { + const cleanupKeyupEvent = on(document, 'keyup', keyup) + const cleanupClickEvent = on(root, 'click', click) + return () => { + cleanupKeyupEvent() + cleanupClickEvent() + } + }) +} diff --git a/packages/actions/src/types.js b/packages/actions/src/types.js new file mode 100644 index 00000000..43a5e73e --- /dev/null +++ b/packages/actions/src/types.js @@ -0,0 +1,10 @@ +/** + * @typedef {Object} EventMapping + * @property {string} event - The event name + * @property {string[]} [keys] - The keys that trigger the event + * @property {RegExp} [pattern] - The pattern that triggers the event + */ + +/** + * @typedef {Object} KeyboardConfig + */ diff --git a/packages/actions/src/utils.js b/packages/actions/src/utils.js new file mode 100644 index 00000000..80c62d24 --- /dev/null +++ b/packages/actions/src/utils.js @@ -0,0 +1,28 @@ +/** + * Finds the closest ancestor of the given element that has the given attribute. + * + * @param {HTMLElement} element + * @param {string} attribute + * @returns {HTMLElement|null} + */ +export function getClosestAncestorWithAttribute(element, attribute) { + if (!element) return null + if (element.getAttribute(attribute)) return element + return getClosestAncestorWithAttribute(element.parentElement, attribute) +} + +import * as R from 'ramda' + +/** + * Get the event name for a given key. + * @param {import('./types.js').KeyboardConfig} keyMapping + * @param {string} key - The key to match. + * @returns {string|null} - The event name or null if no match is found. + */ +export const getEventForKey = (keyMapping, key) => { + const matchEvent = ([eventName, keys]) => + (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key)) + + const event = R.find(matchEvent, R.toPairs(keyMapping)) + return event ? event[0] : null +} diff --git a/packages/actions/tsconfig.json b/packages/actions/tsconfig.json new file mode 100644 index 00000000..06f05e21 --- /dev/null +++ b/packages/actions/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "shared-config/svelte-lib.json", + "include": ["src/**/*.js", "src/**/*.svelte"], + "exclude": ["spec"], + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/packages/actions/vitest.config.js b/packages/actions/vitest.config.js new file mode 100644 index 00000000..f35581ea --- /dev/null +++ b/packages/actions/vitest.config.js @@ -0,0 +1,2 @@ +import defineConfig from 'shared-config/vitest.config.js' +export default defineConfig diff --git a/packages/atoms/package.json b/packages/atoms/package.json new file mode 100644 index 00000000..c709f924 --- /dev/null +++ b/packages/atoms/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rokkit/atoms", + "version": "1.0.0-next.100", + "description": "Atomic components are small, reusable building blocks that can be combined to create complex UI elements", + "author": "Jerry Thomas ", + "license": "MIT", + "module": "src/index.js", + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepublishOnly": "tsc --project tsconfig.build.json", + "clean": "rm -rf dist", + "build": "pnpm clean && pnpm prepublishOnly" + }, + "files": [ + "src/**/*.js", + "dist/**/*.d.ts" + ], + "exports": { + "./src": "./src", + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./src/index.js" + } + } +} diff --git a/packages/atoms/spec/Icon.spec.svelte.js b/packages/atoms/spec/Icon.spec.svelte.js new file mode 100644 index 00000000..33517234 --- /dev/null +++ b/packages/atoms/spec/Icon.spec.svelte.js @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest' +import { render } from '@testing-library/svelte' +import { flushSync } from 'svelte' +import Icon from '../src/Icon.svelte' + +describe('Icon', () => { + it('should render', () => { + const { container } = render(Icon) + expect(container).toBeTruthy() + expect(container).toMatchSnapshot() + }) + + it('should render with props', () => { + const props = $state({ + name: 'home' + }) + const { container } = render(Icon, { props }) + expect(container).toMatchSnapshot() + + props.name = 'settings' + flushSync() + expect(container).toMatchSnapshot() + + props.class = 'small' + flushSync() + expect(container).toMatchSnapshot() + + props.label = 'Settings' + flushSync() + expect(container).toMatchSnapshot() + }) +}) diff --git a/packages/atoms/spec/Item.spec.svelte.js b/packages/atoms/spec/Item.spec.svelte.js new file mode 100644 index 00000000..6e434c1e --- /dev/null +++ b/packages/atoms/spec/Item.spec.svelte.js @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { render } from '@testing-library/svelte' +import { flushSync } from 'svelte' +import Item from '../src/Item.svelte' +import { FieldMapper } from '@rokkit/core' + +describe('Item', () => { + it('should render', () => { + const props = $state({ value: 'Hello' }) + const { container } = render(Item, { props }) + expect(container).toMatchSnapshot() + props.value = { text: 'World' } + flushSync() + expect(container).toMatchSnapshot() + }) + + it('should render with custom mapping', () => { + const mapper = new FieldMapper({ icon: 'text' }) + const props = $state({ value: null, fields: mapper }) + const { container } = render(Item, { props }) + expect(container).toMatchSnapshot() + props.value = { text: 'World' } + flushSync() + expect(container).toMatchSnapshot() + + mapper.fields = { icon: 'icon' } + props.value.image = 'world.png' + flushSync() + expect(container).toMatchSnapshot() + }) + + it('should render with icon', () => { + const { container } = render(Item, { value: { text: 'Hello', icon: 'world' } }) + expect(container).toMatchSnapshot() + }) + + it('should render with image', () => { + const { container } = render(Item, { value: { text: 'Hello', image: 'world.png' } }) + expect(container).toMatchSnapshot() + }) +}) diff --git a/packages/atoms/spec/Pill.spec.svelte.js b/packages/atoms/spec/Pill.spec.svelte.js new file mode 100644 index 00000000..e7549826 --- /dev/null +++ b/packages/atoms/spec/Pill.spec.svelte.js @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest' +import { render, fireEvent } from '@testing-library/svelte' +import { tick } from 'svelte' +import Pill from '../src/Pill.svelte' + +describe('Pill', () => { + it('should render', () => { + const { container } = render(Pill, { value: 'Hello' }) + expect(container).toMatchSnapshot() + }) + + it('should render with icon', () => { + const { container } = render(Pill, { value: { text: 'Hello', icon: 'world' } }) + expect(container).toMatchSnapshot() + }) + + it('should render with image', () => { + const { container } = render(Pill, { value: { text: 'Hello', image: 'world.png' } }) + expect(container).toMatchSnapshot() + }) + + it('should render with close button', () => { + const { container } = render(Pill, { value: 'Hello', removable: true }) + expect(container).toMatchSnapshot() + }) + + it('should render with disabled close button', () => { + const { container } = render(Pill, { value: 'Hello', removable: true, disabled: true }) + expect(container).toMatchSnapshot() + }) + + it('should fire remove event on click', async () => { + const props = $state({ value: 'Hello', removable: true, onremove: vi.fn() }) + const { container } = render(Pill, { props }) + const closeButton = container.querySelector('button') + fireEvent.click(closeButton) + await tick() + expect(props.onremove).toHaveBeenCalledWith('Hello') + }) + + it.each(['Delete', 'Backspace'])('should fire remove event on %s', async (key) => { + const props = $state({ value: 'Hello', removable: true, onremove: vi.fn() }) + const { container } = render(Pill, { props }) + const pill = container.querySelector('rkt-pill') + fireEvent.keyUp(pill, { key }) + await tick() + expect(props.onremove).toHaveBeenCalledWith('Hello') + }) +}) diff --git a/packages/atoms/spec/Switch.spec.svelte.js b/packages/atoms/spec/Switch.spec.svelte.js new file mode 100644 index 00000000..5c504cea --- /dev/null +++ b/packages/atoms/spec/Switch.spec.svelte.js @@ -0,0 +1,132 @@ +import { describe, it, expect } from 'vitest' +import { render, fireEvent } from '@testing-library/svelte' +import Switch from '../src/Switch.svelte' +import { FieldMapper } from '@rokkit/core' +import Item from '../src/Item.svelte' +import { flushSync, tick } from 'svelte' + +describe('Switch', () => { + const fields = new FieldMapper({ icon: 'text' }, { default: Item }) + const nextKeys = ['ArrowDown', 'ArrowRight', 'Enter', ' '] + const prevKeys = ['ArrowUp', 'ArrowLeft'] + + it('should render', () => { + const props = $state({ value: null }) + const { container } = render(Switch, { props }) + const switchElement = container.querySelector('rkt-switch') + expect(switchElement).toBeTruthy() + // expect(switchElement).toHaveAttribute('role', 'listbox') + expect(switchElement).toMatchSnapshot() + + props.class = 'small' + flushSync() + expect(switchElement.classList).toContain('small') + }) + + it('should render with error for invalid options', () => { + const { container } = render(Switch, { options: ['a'] }) + const switchElement = container.querySelector('rkt-switch') + expect(switchElement).toBeFalsy() + expect(switchElement).toMatchSnapshot() + }) + it('should render with options', () => { + const { container } = render(Switch, { options: ['a', 'b'] }) + const switchElement = container.querySelector('rkt-switch') + expect(switchElement).toBeTruthy() + expect(switchElement).toMatchSnapshot() + }) + + it('should render with object options', () => { + const mapper = new FieldMapper({ icon: 'text' }) + const { container } = render(Switch, { + value: { text: 'sun' }, + options: [{ text: 'sun' }, { text: 'moon' }], + fields: mapper + }) + + const switchElement = container.querySelector('rkt-switch') + expect(switchElement).toBeTruthy() + expect(switchElement).toMatchSnapshot() + }) + + it('should switch options on click', async () => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Switch, { props }) + const items = container.querySelectorAll('rkt-item') + expect(items.length).toBe(2) + fireEvent.click(items[0]) + await tick() + + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + + fireEvent.click(items[1]) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + }) + + it.each(nextKeys)('should switch options on keydown %s', async (key) => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Switch, { props }) + const switchElement = container.querySelector('rkt-switch') + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + }) + + it.each(prevKeys)('should switch options on keydown %s', async (key) => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }, { text: 'earth' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Switch, { props }) + const switchElement = container.querySelector('rkt-switch') + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'earth' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'earth' }) + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + + fireEvent.keyUp(switchElement, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + }) +}) diff --git a/packages/atoms/spec/Toggle.spec.svelte.js b/packages/atoms/spec/Toggle.spec.svelte.js new file mode 100644 index 00000000..7c087017 --- /dev/null +++ b/packages/atoms/spec/Toggle.spec.svelte.js @@ -0,0 +1,135 @@ +import { describe, it, expect } from 'vitest' +import { render, fireEvent } from '@testing-library/svelte' +import Toggle from '../src/Toggle.svelte' +import { FieldMapper } from '@rokkit/core' +import Item from '../src/Item.svelte' +import { flushSync, tick } from 'svelte' + +describe('Toggle', () => { + const fields = new FieldMapper({ icon: 'text' }, { default: Item }) + const nextKeys = ['ArrowDown', 'ArrowRight', 'Enter', ' '] + const prevKeys = ['ArrowUp', 'ArrowLeft'] + + it('should render', () => { + const props = $state({ value: null }) + const { container } = render(Toggle, { props }) + const toggleButton = container.querySelector('rkt-toggle') + expect(toggleButton).toBeTruthy() + expect(toggleButton).toMatchSnapshot() + + props.class = 'small' + flushSync() + expect(toggleButton.classList).toContain('small') + + props.class = 'medium' + flushSync() + expect(toggleButton.classList).toContain('medium') + }) + + it('should render with options', () => { + const { container } = render(Toggle, { options: ['a', 'b'] }) + const toggleButton = container.querySelector('rkt-toggle > button') + expect(toggleButton).toBeTruthy() + expect(toggleButton).toMatchSnapshot() + }) + + it('should render with object options', () => { + const mapper = new FieldMapper({ icon: 'text' }) + const { container } = render(Toggle, { + value: { text: 'sun' }, + options: [{ text: 'sun' }, { text: 'moon' }], + fields: mapper + }) + + const toggleButton = container.querySelector('rkt-toggle > button') + expect(toggleButton).toBeTruthy() + expect(toggleButton).toMatchSnapshot() + }) + + it('should switch options on click', async () => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Toggle, { props }) + const button = container.querySelector('rkt-toggle > button') + + fireEvent.click(button) + await tick() + + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + + fireEvent.click(button) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + + fireEvent.click(button) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + }) + + it.each(nextKeys)('should switch options on keydown %s', async (key) => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Toggle, { props }) + const toggleButton = container.querySelector('rkt-toggle > button') + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + }) + + it.each(prevKeys)('should switch options on keydown %s', async (key) => { + const props = $state({ + value: null, + options: [{ text: 'sun' }, { text: 'moon' }, { text: 'earth' }], + fields, + onchange: vi.fn() + }) + const { container } = render(Toggle, { props }) + const toggleButton = container.querySelector('rkt-toggle > button') + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'earth' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'earth' }) + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'moon' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'moon' }) + + fireEvent.keyUp(toggleButton, { key }) + await tick() + expect(container).toMatchSnapshot() + expect(props.value).toEqual({ text: 'sun' }) + expect(props.onchange).toHaveBeenCalledWith({ text: 'sun' }) + }) +}) diff --git a/packages/atoms/spec/__snapshots__/Icon.spec.svelte.js.snap b/packages/atoms/spec/__snapshots__/Icon.spec.svelte.js.snap new file mode 100644 index 00000000..64ab6c85 --- /dev/null +++ b/packages/atoms/spec/__snapshots__/Icon.spec.svelte.js.snap @@ -0,0 +1,81 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Icon > should render 1`] = ` +
+ +