diff --git a/shepherd.js/src/components/shepherd-button.svelte b/shepherd.js/src/components/shepherd-button.svelte
index 4c2d52ee7..287aaebbf 100644
--- a/shepherd.js/src/components/shepherd-button.svelte
+++ b/shepherd.js/src/components/shepherd-button.svelte
@@ -1,5 +1,6 @@
diff --git a/shepherd.js/src/components/shepherd-cancel-icon.svelte b/shepherd.js/src/components/shepherd-cancel-icon.svelte
index e2d387f97..111c5f6ae 100644
--- a/shepherd.js/src/components/shepherd-cancel-icon.svelte
+++ b/shepherd.js/src/components/shepherd-cancel-icon.svelte
@@ -1,4 +1,6 @@
diff --git a/shepherd.js/src/utils/data-attributes.ts b/shepherd.js/src/utils/data-attributes.ts
new file mode 100644
index 000000000..e83ca7feb
--- /dev/null
+++ b/shepherd.js/src/utils/data-attributes.ts
@@ -0,0 +1,41 @@
+/**
+ * Represents a single data attribute with an id and value
+ */
+export interface DataAttribute {
+ id: string;
+ value: string | number | boolean;
+}
+
+/**
+ * Converts an array of data attributes to an object of data-* attributes
+ * suitable for spreading onto HTML elements.
+ *
+ * @param dataAttributes - Array of data attribute objects with id and value
+ * @returns Object with data-* attributes as keys
+ *
+ * @example
+ * ```typescript
+ * const attrs = convertDataAttributes([
+ * { id: 'foo', value: 'bar' },
+ * { id: 'count', value: 42 }
+ * ]);
+ * // Returns: { 'data-foo': 'bar', 'data-count': '42' }
+ * ```
+ */
+export function convertDataAttributes(
+ dataAttributes?: DataAttribute[] | null
+): Record {
+ if (!dataAttributes || !Array.isArray(dataAttributes)) {
+ return {};
+ }
+
+ return dataAttributes.reduce(
+ (acc, attr) => {
+ if (attr.id) {
+ acc[`data-${attr.id}`] = String(attr.value);
+ }
+ return acc;
+ },
+ {} as Record
+ );
+}
diff --git a/test/unit/components/shepherd-button.spec.js b/test/unit/components/shepherd-button.spec.js
index b16746fc1..07c422cae 100644
--- a/test/unit/components/shepherd-button.spec.js
+++ b/test/unit/components/shepherd-button.spec.js
@@ -193,4 +193,150 @@ describe('component/ShepherdButton', () => {
expect(buttonUpdated).toHaveTextContent('Test 2');
});
});
+
+ describe('dataAttributes', () => {
+ it('applies single data attribute correctly', () => {
+ const config = {
+ text: 'Click me',
+ dataAttributes: [{ id: 'test', value: 'testValue' }]
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toHaveAttribute('data-test', 'testValue');
+ });
+
+ it('applies multiple data attributes correctly', () => {
+ const config = {
+ text: 'Click me',
+ dataAttributes: [
+ { id: 'foo', value: 'someData' },
+ { id: 'bar', value: '1234' },
+ { id: 'baz', value: 'anotherValue' }
+ ]
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toHaveAttribute('data-foo', 'someData');
+ expect(button).toHaveAttribute('data-bar', '1234');
+ expect(button).toHaveAttribute('data-baz', 'anotherValue');
+ });
+
+ it('handles data attributes with numeric values', () => {
+ const config = {
+ text: 'Click me',
+ dataAttributes: [
+ { id: 'count', value: 42 },
+ { id: 'price', value: 99.99 }
+ ]
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toHaveAttribute('data-count', '42');
+ expect(button).toHaveAttribute('data-price', '99.99');
+ });
+
+ it('handles empty dataAttributes array', () => {
+ const config = {
+ text: 'Click me',
+ dataAttributes: []
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ const dataAttrs = Array.from(button.attributes).filter((attr) =>
+ attr.name.startsWith('data-')
+ );
+ expect(dataAttrs).toHaveLength(0);
+ });
+
+ it('handles undefined dataAttributes', () => {
+ const config = {
+ text: 'Click me'
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toBeInTheDocument();
+ });
+
+ it('ignores data attributes without id', () => {
+ const config = {
+ text: 'Click me',
+ dataAttributes: [
+ { id: 'valid', value: 'validValue' },
+ { value: 'noId' },
+ { id: '', value: 'emptyId' }
+ ]
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toHaveAttribute('data-valid', 'validValue');
+
+ const dataAttrs = Array.from(button.attributes).filter((attr) =>
+ attr.name.startsWith('data-')
+ );
+ expect(dataAttrs).toHaveLength(1);
+ });
+
+ it('works with other button properties', () => {
+ const config = {
+ text: 'Next',
+ label: 'Go to next step',
+ classes: 'custom-class',
+ secondary: true,
+ dataAttributes: [
+ { id: 'step', value: '2' },
+ { id: 'action', value: 'next' }
+ ]
+ };
+
+ const { container } = render(ShepherdButton, {
+ props: {
+ config
+ }
+ });
+
+ const button = container.querySelector('.shepherd-button');
+ expect(button).toHaveAttribute('aria-label', 'Go to next step');
+ expect(button).toHaveClass('custom-class');
+ expect(button).toHaveClass('shepherd-button-secondary');
+ expect(button).toHaveAttribute('data-step', '2');
+ expect(button).toHaveAttribute('data-action', 'next');
+ expect(button).toHaveTextContent('Next');
+ });
+ });
});
diff --git a/test/unit/components/shepherd-cancel-icon.spec.js b/test/unit/components/shepherd-cancel-icon.spec.js
new file mode 100644
index 000000000..adee0c10c
--- /dev/null
+++ b/test/unit/components/shepherd-cancel-icon.spec.js
@@ -0,0 +1,437 @@
+import { vi } from 'vitest';
+import { cleanup, fireEvent, render } from '@testing-library/svelte';
+import ShepherdCancelIcon from '../../../shepherd.js/src/components/shepherd-cancel-icon.svelte';
+import { Tour } from '../../../shepherd.js/src/tour';
+import { Step } from '../../../shepherd.js/src/step';
+
+describe('components/ShepherdCancelIcon', () => {
+ beforeEach(cleanup);
+
+ describe('basic functionality', () => {
+ it('renders cancel icon with default aria-label', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toBeInTheDocument();
+ expect(cancelIcon).toHaveAttribute('aria-label', 'Close Tour');
+ expect(cancelIcon).toHaveAttribute('type', 'button');
+ });
+
+ it('renders cancel icon with custom aria-label', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true,
+ label: 'Custom Close Label'
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ label: 'Custom Close Label'
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('aria-label', 'Custom Close Label');
+ });
+
+ it('renders close symbol (×)', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const closeSymbol = container.querySelector('span[aria-hidden="true"]');
+ expect(closeSymbol).toBeInTheDocument();
+ expect(closeSymbol.textContent).toBe('×');
+ });
+ });
+
+ describe('click behavior', () => {
+ it('calls step.cancel() when clicked', async () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+ const stepCancelSpy = vi.spyOn(step, 'cancel');
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ await fireEvent.click(cancelIcon);
+
+ expect(stepCancelSpy).toHaveBeenCalledOnce();
+ });
+
+ it('prevents default event behavior when clicked', async () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ const clickEvent = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true
+ });
+ const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault');
+
+ cancelIcon.dispatchEvent(clickEvent);
+
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('dataAttributes functionality', () => {
+ it('applies single data attribute correctly', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [{ id: 'test', value: 'testValue' }]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-test', 'testValue');
+ });
+
+ it('applies multiple data attributes correctly', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'foo', value: 'someData' },
+ { id: 'bar', value: '1234' },
+ { id: 'baz', value: 'anotherValue' }
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-foo', 'someData');
+ expect(cancelIcon).toHaveAttribute('data-bar', '1234');
+ expect(cancelIcon).toHaveAttribute('data-baz', 'anotherValue');
+ });
+
+ it('handles data attributes with numeric values', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'count', value: 42 },
+ { id: 'price', value: 99.99 }
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-count', '42');
+ expect(cancelIcon).toHaveAttribute('data-price', '99.99');
+ });
+
+ it('handles data attributes with boolean values', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'active', value: true },
+ { id: 'disabled', value: false }
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-active', 'true');
+ expect(cancelIcon).toHaveAttribute('data-disabled', 'false');
+ });
+
+ it('handles empty dataAttributes array', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: []
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toBeInTheDocument();
+ // Should not have any data-* attributes
+ const dataAttrs = Array.from(cancelIcon.attributes).filter((attr) =>
+ attr.name.startsWith('data-')
+ );
+ expect(dataAttrs).toHaveLength(0);
+ });
+
+ it('handles undefined dataAttributes', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toBeInTheDocument();
+ });
+
+ it('handles null dataAttributes', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: null
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toBeInTheDocument();
+ });
+
+ it('ignores data attributes without id', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'valid', value: 'validValue' },
+ { value: 'noId' }, // Should be ignored
+ { id: '', value: 'emptyId' } // Should be ignored
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-valid', 'validValue');
+
+ // Check that only one data attribute exists
+ const dataAttrs = Array.from(cancelIcon.attributes).filter((attr) =>
+ attr.name.startsWith('data-')
+ );
+ expect(dataAttrs).toHaveLength(1);
+ });
+
+ it('handles data attributes with special characters in values', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'url', value: 'https://example.com/test?param=value' },
+ { id: 'json', value: '{"key":"value"}' },
+ { id: 'spaces', value: 'value with spaces' }
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute(
+ 'data-url',
+ 'https://example.com/test?param=value'
+ );
+ expect(cancelIcon).toHaveAttribute('data-json', '{"key":"value"}');
+ expect(cancelIcon).toHaveAttribute('data-spaces', 'value with spaces');
+ });
+ });
+
+ describe('integration with label and dataAttributes', () => {
+ it('works with both custom label and data attributes', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: {
+ enabled: true,
+ label: 'close the tour',
+ dataAttributes: [
+ { id: 'foo', value: 'someData' },
+ { id: 'bar', value: '1234' }
+ ]
+ },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('aria-label', 'close the tour');
+ expect(cancelIcon).toHaveAttribute('data-foo', 'someData');
+ expect(cancelIcon).toHaveAttribute('data-bar', '1234');
+ expect(cancelIcon).toHaveAttribute('type', 'button');
+ expect(cancelIcon).toHaveClass('shepherd-cancel-icon');
+ });
+ });
+
+ describe('CSS classes and styling', () => {
+ it('has correct CSS class', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveClass('shepherd-cancel-icon');
+ });
+
+ it('button has transparent background', () => {
+ const tour = new Tour();
+ const step = new Step(tour, {
+ cancelIcon: {
+ enabled: true
+ }
+ });
+
+ const { container } = render(ShepherdCancelIcon, {
+ props: {
+ cancelIcon: { enabled: true },
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ const styles = window.getComputedStyle(cancelIcon);
+ // Basic style checks - specific values might vary based on CSS
+ expect(cancelIcon.style.background || styles.background).toBeTruthy();
+ });
+ });
+});
diff --git a/test/unit/components/shepherd-header.spec.js b/test/unit/components/shepherd-header.spec.js
index 1cbd56c32..33c9ffd8e 100644
--- a/test/unit/components/shepherd-header.spec.js
+++ b/test/unit/components/shepherd-header.spec.js
@@ -88,4 +88,131 @@ describe('components/ShepherdHeader', () => {
fireEvent.click(container.querySelector('.shepherd-cancel-icon'));
expect(stepCancelSpy).toHaveBeenCalled();
});
+
+ it('cancel icon renders with data attributes', () => {
+ const step = {
+ options: {
+ cancelIcon: {
+ enabled: true,
+ dataAttributes: [
+ { id: 'test', value: 'testValue' },
+ { id: 'other', value: '1234' }
+ ]
+ }
+ }
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('data-test', 'testValue');
+ expect(cancelIcon).toHaveAttribute('data-other', '1234');
+ });
+
+ it('cancel icon renders with both label and data attributes', () => {
+ const step = {
+ options: {
+ cancelIcon: {
+ enabled: true,
+ label: 'close the tour',
+ dataAttributes: [
+ { id: 'foo', value: 'someData' },
+ { id: 'bar', value: '1234' }
+ ]
+ }
+ }
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step
+ }
+ });
+
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+ expect(cancelIcon).toHaveAttribute('aria-label', 'close the tour');
+ expect(cancelIcon).toHaveAttribute('data-foo', 'someData');
+ expect(cancelIcon).toHaveAttribute('data-bar', '1234');
+ });
+
+ it('renders title when provided', () => {
+ const step = {
+ options: {
+ title: 'Test Title'
+ }
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step,
+ labelId: 'test-label'
+ }
+ });
+
+ const title = container.querySelector('.shepherd-title');
+ expect(title).toBeInTheDocument();
+ expect(title).toHaveTextContent('Test Title');
+ });
+
+ it('does not render title when not provided', () => {
+ const step = {
+ options: {}
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step
+ }
+ });
+
+ const title = container.querySelector('.shepherd-title');
+ expect(title).not.toBeInTheDocument();
+ });
+
+ it('renders both title and cancel icon when both provided', () => {
+ const step = {
+ options: {
+ title: 'Test Title',
+ cancelIcon: {
+ enabled: true
+ }
+ }
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step,
+ labelId: 'test-label'
+ }
+ });
+
+ const title = container.querySelector('.shepherd-title');
+ const cancelIcon = container.querySelector('.shepherd-cancel-icon');
+
+ expect(title).toBeInTheDocument();
+ expect(cancelIcon).toBeInTheDocument();
+ });
+
+ it('header has correct CSS class', () => {
+ const step = {
+ options: {
+ title: 'Test Title'
+ }
+ };
+
+ const { container } = render(ShepherdHeader, {
+ props: {
+ step,
+ labelId: 'test-label'
+ }
+ });
+
+ const header = container.querySelector('.shepherd-header');
+ expect(header).toBeInTheDocument();
+ expect(header.tagName.toLowerCase()).toBe('header');
+ });
});
diff --git a/test/unit/utils/data-attributes.spec.js b/test/unit/utils/data-attributes.spec.js
new file mode 100644
index 000000000..c95ee5b92
--- /dev/null
+++ b/test/unit/utils/data-attributes.spec.js
@@ -0,0 +1,123 @@
+import { describe, it, expect } from 'vitest';
+import { convertDataAttributes } from '../../../shepherd.js/src/utils/data-attributes.ts';
+
+describe('utils/data-attributes', () => {
+ describe('convertDataAttributes', () => {
+ it('returns empty object when dataAttributes is undefined', () => {
+ const result = convertDataAttributes(undefined);
+ expect(result).toEqual({});
+ });
+
+ it('returns empty object when dataAttributes is null', () => {
+ const result = convertDataAttributes(null);
+ expect(result).toEqual({});
+ });
+
+ it('returns empty object when dataAttributes is empty array', () => {
+ const result = convertDataAttributes([]);
+ expect(result).toEqual({});
+ });
+
+ it('converts single data attribute correctly', () => {
+ const result = convertDataAttributes([{ id: 'test', value: 'testValue' }]);
+ expect(result).toEqual({ 'data-test': 'testValue' });
+ });
+
+ it('converts multiple data attributes correctly', () => {
+ const result = convertDataAttributes([
+ { id: 'foo', value: 'someData' },
+ { id: 'bar', value: '1234' },
+ { id: 'baz', value: 'anotherValue' }
+ ]);
+ expect(result).toEqual({
+ 'data-foo': 'someData',
+ 'data-bar': '1234',
+ 'data-baz': 'anotherValue'
+ });
+ });
+
+ it('handles numeric values', () => {
+ const result = convertDataAttributes([
+ { id: 'count', value: 42 },
+ { id: 'price', value: 99.99 }
+ ]);
+ expect(result).toEqual({
+ 'data-count': '42',
+ 'data-price': '99.99'
+ });
+ });
+
+ it('handles boolean values', () => {
+ const result = convertDataAttributes([
+ { id: 'active', value: true },
+ { id: 'disabled', value: false }
+ ]);
+ expect(result).toEqual({
+ 'data-active': 'true',
+ 'data-disabled': 'false'
+ });
+ });
+
+ it('ignores attributes without id', () => {
+ const result = convertDataAttributes([
+ { id: 'valid', value: 'validValue' },
+ { value: 'noId' },
+ { id: '', value: 'emptyId' }
+ ]);
+ expect(result).toEqual({ 'data-valid': 'validValue' });
+ });
+
+ it('handles special characters in values', () => {
+ const result = convertDataAttributes([
+ { id: 'url', value: 'https://example.com/test?param=value' },
+ { id: 'json', value: '{"key":"value"}' },
+ { id: 'spaces', value: 'value with spaces' }
+ ]);
+ expect(result).toEqual({
+ 'data-url': 'https://example.com/test?param=value',
+ 'data-json': '{"key":"value"}',
+ 'data-spaces': 'value with spaces'
+ });
+ });
+
+ it('handles mixed types of values', () => {
+ const result = convertDataAttributes([
+ { id: 'string', value: 'text' },
+ { id: 'number', value: 123 },
+ { id: 'boolean', value: true },
+ { id: 'float', value: 3.14 }
+ ]);
+ expect(result).toEqual({
+ 'data-string': 'text',
+ 'data-number': '123',
+ 'data-boolean': 'true',
+ 'data-float': '3.14'
+ });
+ });
+
+ it('handles zero as a value', () => {
+ const result = convertDataAttributes([{ id: 'count', value: 0 }]);
+ expect(result).toEqual({ 'data-count': '0' });
+ });
+
+ it('handles empty string as a value', () => {
+ const result = convertDataAttributes([{ id: 'empty', value: '' }]);
+ expect(result).toEqual({ 'data-empty': '' });
+ });
+
+ it('preserves kebab-case in attribute ids', () => {
+ const result = convertDataAttributes([
+ { id: 'my-custom-attr', value: 'value' }
+ ]);
+ expect(result).toEqual({ 'data-my-custom-attr': 'value' });
+ });
+
+ it('handles duplicate ids by keeping last value', () => {
+ const result = convertDataAttributes([
+ { id: 'duplicate', value: 'first' },
+ { id: 'duplicate', value: 'second' }
+ ]);
+ expect(result).toEqual({ 'data-duplicate': 'second' });
+ });
+ });
+});