From 94cf62b8ef22259bd13d141189902e5f57eaba89 Mon Sep 17 00:00:00 2001 From: Alan Cole Date: Wed, 4 Dec 2024 12:09:23 +1100 Subject: [PATCH] [CIVIC-1947] Added jest tests. Removed some storybook utils. --- build.js | 4 +- components/00-base/grid/grid.test.js | 3 +- .../storybook/storybook.docs.utils.test.js | 1 + .../storybook/storybook.generators.utils.js | 79 ------ .../storybook.generators.utils.test.js | 132 ---------- .../storybook/storybook.knobs.utils.js | 230 ------------------ .../storybook/storybook.knobs.utils.test.js | 222 ----------------- .../storybook/storybook.layout.utils.test.js | 1 + .../storybook/storybook.random.utils.js | 98 -------- .../storybook/storybook.random.utils.test.js | 83 ------- components/04-templates/page/page.test.js | 3 +- jest.config.js | 12 +- package.json | 7 +- tests/jest.helpers.js | 3 +- 14 files changed, 18 insertions(+), 860 deletions(-) delete mode 100644 components/00-base/storybook/storybook.generators.utils.js delete mode 100644 components/00-base/storybook/storybook.generators.utils.test.js delete mode 100644 components/00-base/storybook/storybook.knobs.utils.js delete mode 100644 components/00-base/storybook/storybook.knobs.utils.test.js delete mode 100644 components/00-base/storybook/storybook.random.utils.js delete mode 100644 components/00-base/storybook/storybook.random.utils.test.js diff --git a/build.js b/build.js index 4ba949d0..3d412de4 100644 --- a/build.js +++ b/build.js @@ -311,7 +311,9 @@ if (config.cli) { scripts.forEach((script, idx) => { const child = spawn('npm', ['run', script]) child.stdout.on('data', (data) => { - process.stdout.write(`\x1b[3${Math.min(idx, 3) + 3}m${data}`) + const color = `\x1b[3${Math.min(idx, 3) + 3}m` + const sData = data.toString().replaceAll(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') // strip ANSI colours + process.stdout.write(`${color}${sData}`) }); }); } diff --git a/components/00-base/grid/grid.test.js b/components/00-base/grid/grid.test.js index a236fd93..d063bb89 100644 --- a/components/00-base/grid/grid.test.js +++ b/components/00-base/grid/grid.test.js @@ -1,4 +1,5 @@ -import each from 'jest-each'; +import jestEach from 'jest-each'; +const each = jestEach.default; const template = 'components/00-base/grid/grid.twig'; diff --git a/components/00-base/storybook/storybook.docs.utils.test.js b/components/00-base/storybook/storybook.docs.utils.test.js index 1bca6d0c..30e2d955 100644 --- a/components/00-base/storybook/storybook.docs.utils.test.js +++ b/components/00-base/storybook/storybook.docs.utils.test.js @@ -1,4 +1,5 @@ import { decoratorDocs } from './storybook.docs.utils'; +import { jest } from '@jest/globals'; describe('decoratorDocs', () => { const mockContent = jest.fn(() => '
Content
'); diff --git a/components/00-base/storybook/storybook.generators.utils.js b/components/00-base/storybook/storybook.generators.utils.js deleted file mode 100644 index 51dd1833..00000000 --- a/components/00-base/storybook/storybook.generators.utils.js +++ /dev/null @@ -1,79 +0,0 @@ -// -// Domain-specific generators for all Storybook stories. -// -/* eslint max-classes-per-file: 0 */ - -import { randomInt, randomBool, randomSentence, randomString, randomArrayItem } from './storybook.random.utils'; - -export const themes = () => ({ - light: 'Light', - dark: 'Dark', -}); - -export const placeholder = (content = 'Content placeholder', words = 0, cssClass = 'story-placeholder') => `
${content}${words > 0 ? ` ${randomSentence(words)}` : ''}
`; - -export const code = (content) => `${content}`; - -export const demoImage = (idx) => { - const images = [ - 'demo/images/demo1.jpg', - 'demo/images/demo2.jpg', - 'demo/images/demo3.jpg', - 'demo/images/demo4.jpg', - 'demo/images/demo5.jpg', - 'demo/images/demo6.jpg', - ]; - - const maxIndex = images.length - 1; - idx = typeof idx !== 'undefined' ? Math.max(0, Math.min(idx, maxIndex)) : null; - - return idx !== null ? images[idx] : randomArrayItem(images); -}; - -export const demoIcon = () => './assets/icons/megaphone.svg'; - -export const demoVideoPoster = () => 'demo/videos/demo_poster.png'; - -export const demoVideos = () => [ - { - url: 'demo/videos/demo.webm', - type: 'video/webm', - }, - { - url: 'demo/videos/demo.mp4', - type: 'video/mp4', - }, - { - url: 'demo/videos/demo.avi', - type: 'video/avi', - }, -]; - -export const generateItems = (count, content) => { - const items = []; - for (let i = 1; i <= count; i++) { - if (typeof content === 'function') { - items.push(content(i, count)); - } else { - items.push(content); - } - } - return items; -}; - -export const generateSelectOptions = (count, type = 'option') => { - const options = []; - for (let i = 1; i <= count; i++) { - const disabled = randomBool(0.8); - const option = { - type, - is_selected: randomBool(0.8), - is_disabled: disabled, - label: (type === 'optgroup' ? `Group ${i}` : randomString(randomInt(3, 8))) + (disabled ? ' (disabled)' : ''), - value: randomString(randomInt(1, 8)), - options: type === 'optgroup' ? generateSelectOptions(count) : null, - }; - options.push(option); - } - return options; -}; diff --git a/components/00-base/storybook/storybook.generators.utils.test.js b/components/00-base/storybook/storybook.generators.utils.test.js deleted file mode 100644 index 286b6d49..00000000 --- a/components/00-base/storybook/storybook.generators.utils.test.js +++ /dev/null @@ -1,132 +0,0 @@ -// storybook.domain.utils.test.js - -import { code, demoIcon, demoImage, demoVideoPoster, demoVideos, generateItems, generateSelectOptions, placeholder, themes } from './storybook.generators.utils'; - -describe('Domain-Specific Generators', () => { - describe('themes', () => { - it('returns the correct themes', () => { - expect(themes()).toEqual({ - light: 'Light', - dark: 'Dark', - }); - }); - }); - - describe('placeholder', () => { - it('returns placeholder with default parameters', () => { - expect(placeholder()) - .toBe('
Content placeholder
'); - }); - - it('returns placeholder with custom content and class', () => { - expect(placeholder('Custom content', 0, 'custom-class')) - .toBe('
Custom content
'); - }); - - it('returns placeholder with random sentence', () => { - const result = placeholder('Custom content', 5); - expect(result.startsWith('
Custom content ')) - .toBe(true); - }); - }); - - describe('code', () => { - it('wraps content in code tags', () => { - expect(code('const x = 1;')).toBe('const x = 1;'); - }); - }); - - describe('demoImage', () => { - const images = [ - 'demo/images/demo1.jpg', - 'demo/images/demo2.jpg', - 'demo/images/demo3.jpg', - 'demo/images/demo4.jpg', - 'demo/images/demo5.jpg', - 'demo/images/demo6.jpg', - ]; - - it('returns a random demo image when index is not provided', () => { - const result = demoImage(); - expect(images).toContain(result); - }); - - it.each([0, 1, 2, 3, 4, 5])('returns the correct image for index %i', (idx) => { - expect(demoImage(idx)).toBe(images[idx]); - }); - - it('returns the last image for index out of bounds', () => { - expect(demoImage(6)).toBe(images[5]); - }); - }); - - describe('demoIcon', () => { - it('returns the correct demo icon path', () => { - expect(demoIcon()).toBe('./assets/icons/megaphone.svg'); - }); - }); - - describe('demoVideoPoster', () => { - it('returns the correct demo video poster path', () => { - expect(demoVideoPoster()).toBe('demo/videos/demo_poster.png'); - }); - }); - - describe('demoVideos', () => { - it('returns the correct demo videos array', () => { - expect(demoVideos()).toEqual([ - { - url: 'demo/videos/demo.webm', - type: 'video/webm', - }, - { - url: 'demo/videos/demo.mp4', - type: 'video/mp4', - }, - { - url: 'demo/videos/demo.avi', - type: 'video/avi', - }, - ]); - }); - }); - - describe('generateItems', () => { - it('generates items with static content', () => { - expect(generateItems(3, 'item')).toEqual(['item', 'item', 'item']); - }); - - it('generates items with function content', () => { - const content = jest.fn((i) => `item ${i}`); - expect(generateItems(3, content)).toEqual(['item 1', 'item 2', 'item 3']); - }); - }); - - describe('generateSelectOptions', () => { - it('generates select options with type "option"', () => { - const options = generateSelectOptions(2, 'option'); - expect(options.length).toBe(2); - options.forEach((option) => { - expect(option.type).toBe('option'); - expect(typeof option.is_selected).toBe('boolean'); - expect(typeof option.is_disabled).toBe('boolean'); - expect(typeof option.label).toBe('string'); - expect(typeof option.value).toBe('string'); - expect(option.options).toBe(null); - }); - }); - - it('generates select options with type "optgroup"', () => { - const options = generateSelectOptions(2, 'optgroup'); - expect(options.length).toBe(2); - options.forEach((option) => { - expect(option.type).toBe('optgroup'); - expect(typeof option.is_selected).toBe('boolean'); - expect(typeof option.is_disabled).toBe('boolean'); - expect(typeof option.label).toBe('string'); - expect(typeof option.value).toBe('string'); - expect(Array.isArray(option.options)).toBe(true); - }); - }); - }); -}); diff --git a/components/00-base/storybook/storybook.knobs.utils.js b/components/00-base/storybook/storybook.knobs.utils.js deleted file mode 100644 index 1d230d83..00000000 --- a/components/00-base/storybook/storybook.knobs.utils.js +++ /dev/null @@ -1,230 +0,0 @@ -// -// Centralised utilities for all Storybook stories. -// -/* eslint max-classes-per-file: 0 */ - -import { boolean, color, date as dateKnob, number, optionsKnob, radios, select, text } from '@storybook/addon-knobs'; - -/** - * Knob wrappers are poor man's story Args with additional functionality. - * - * The use case is to allow re-using the same pre-defined story knobs within - * the child component in the parent component's story. But also allow the - * parent component story to override the value of the knob or completely - * suppress the knob from being shown. - * - * The wrapper provides a capability for a parent story to call a child - * component's story: - * 1. Without any values passed - would render a child component with its - * default knob values and no knobs shown. - * 2. With some values passed - would render a child component with these - * values (unspecified values would use the knobs' default values) and - * no knobs shown. - * 3. Without **knob values** passed and wanting to see **all** the knobs - - * would render a child component with its default knob values and show - * **all** the knobs. - * 4. Without **knob values** passed and wanting to see **some** knobs - would - * render a child component with their default knob values and - * show **only the knobs for passed values**. - * 5. With some **knob values** passed and wanting to see the knobs - would - * render a child component with **these knob values** and show the knobs - * only for these passed values. - * - * @code - * // Render a child component with its default knob values and no knobs - * // shown. All the component's properties will use the values set on - * // the knobs in the child component's story. - * const component1 = MyComponent(new KnobValues()); - * - * // Render a child component with `title` set to `My title` and no knobs - * // shown. Unspecified component's properties will use the values set on - * // the knobs in the child component's story. - * const component2 = MyComponent(new KnobValues({ - * title: 'My title', - * })); - * - * // Render a child component with its default knob values and show - * // **all** the knobs. The values of the knobs will use the values set on - * // the knobs in the child component's story. - * const component3 = MyComponent(); - * - * // Render a child component with a value from the `Theme` knob set in the - * // child component's story and show the `Theme` knob with **that** value. - * // Unspecified component's properties will use the values set on - * // the knobs in the child component's story. - * const component3 = MyComponent(new KnobValues({ - * theme: new KnobValue(), - * })); - * - * // Render a child component with a value `dark` and show the `Theme` knob - * // with the value `dark`. - * // Unspecified component's properties will use the values set on - * // the knobs in the child component's story. - * const component3 = MyComponent(new KnobValues({ - * theme: new KnobValue('dark'), - * })); - * @endcode - */ - -/** - * Knob value container. - * - * If the value is set to null, then the default value of the knob is used and - * the knob is shown. - * - * If the value is set to anything else, then this value is used and the knob is - * shown. - * - * If the value is set to null, and useDefault is set to true, then the default - * value of the knob is used and the knob is not shown. - */ -export class KnobValue { - constructor(value = null, useDefault = false) { - this.value = value; - this.useDefault = useDefault; - } - - getValue() { - return this.value; - } - - isUsingDefault() { - return this.useDefault; - } -} - -/** - * Container for the knob values passed to the stories. - */ -export class KnobValues { - constructor(knobs = {}, shouldRender = true, parentKnobs = {}) { - this.knobs = knobs; - this.parentKnobs = parentKnobs; - this.shouldRender = shouldRender; - - /* eslint no-constructor-return: 0 */ - return new Proxy(this, { - get: (target, prop) => { - if (prop === 'shouldRender') { - return target.shouldRender; - } - - if (prop in target.parentKnobs) { - return target.parentKnobs[prop]; - } - - if (prop in target.knobs) { - return target.knobs[prop]; - } - - if (prop === 'knobTab') { - return 'General'; - } - - return new KnobValue(null, true); - }, - }); - } -} - -/** - * Process values passed to the knob and return a value or render a knob. - */ -export const processKnob = (name, defaultValue, parent, group, knobCallback) => { - // If parent is undefined, use the default value and render the knob. - if (parent === undefined) { - return knobCallback(name, defaultValue, group); - } - - // If parent is null, a scalar value or an object, use it's value. - if (parent === null || !(parent instanceof KnobValue)) { - return parent; - } - - // If parent is a KnobValue instance set to use the default value, return the - // default value. - if (parent && parent.isUsingDefault()) { - return defaultValue; - } - - // If parent is a KnobValue instance with a null value, use the default value - // and render the knob. - if (parent.getValue() === null) { - return knobCallback(name, defaultValue, group); - } - - // Use the value from the KnobValue instance. - return knobCallback(name, parent.getValue(), group); -}; - -export const knobText = (name, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => text(knobName, knobValue, knobGroup)); - -export const knobRadios = (name, options, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => radios(knobName, options, knobValue, knobGroup)); - -export const knobBoolean = (name, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => boolean(knobName, knobValue, knobGroup)); - -export const knobNumber = (name, value, options, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => number(knobName, knobValue, options, knobGroup)); - -export const knobSelect = (name, options, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => select(knobName, options, knobValue, knobGroup)); - -export const knobColor = (name, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => color(knobName, knobValue, knobGroup)); - -export const knobOptions = (name, options, value, optionsObj, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => optionsKnob(knobName, options, knobValue, optionsObj, knobGroup)); - -export const knobDate = (name, value, parent, group = 'General') => processKnob(name, value, parent, group, (knobName, knobValue, knobGroup) => dateKnob(knobName, knobValue, knobGroup)); - -/** - * Render a component if none of the parentKnobs are of KnobValue class. - * - * Allows to re-use stories to collect the values without rendering the - * component. - * - * Do not optimize this function - it is laid out in a way that is easy to - * understand and follow the logic. - */ -export const shouldRender = (parentKnobs) => { - if (parentKnobs === null || typeof parentKnobs !== 'object') return true; - - if (Object.keys(parentKnobs).length === 0 && parentKnobs.constructor === Object) { - return true; - } - - // If the parentKnobs are of KnobValues class, then check the shouldRender - // flag. - if (parentKnobs instanceof KnobValues) { - return parentKnobs.shouldRender; - } - - let showKnobs = false; - const knobsValues = Object.values(parentKnobs); - - for (let i = 0; i < knobsValues.length; i++) { - const value = knobsValues[i]; - if (value instanceof KnobValue) { - if (value.getValue() !== null) { - showKnobs = true; - break; - } - - if (value.getValue() == null && !value.isUsingDefault()) { - showKnobs = true; - break; - } - } - } - - return !showKnobs; -}; - -export const slotKnobs = (names) => { - const showSlots = boolean('Show story slots', false, 'Slots'); - const obj = {}; - - if (showSlots) { - for (const i in names) { - obj[names[i]] = `
{{ ${names[i]} }}
`; - } - } - - return obj; -}; diff --git a/components/00-base/storybook/storybook.knobs.utils.test.js b/components/00-base/storybook/storybook.knobs.utils.test.js deleted file mode 100644 index e6787b76..00000000 --- a/components/00-base/storybook/storybook.knobs.utils.test.js +++ /dev/null @@ -1,222 +0,0 @@ -import { boolean, color, date as dateKnob, number, optionsKnob, radios, select, text } from '@storybook/addon-knobs'; -import { knobBoolean, knobColor, knobDate, knobNumber, knobOptions, knobRadios, knobSelect, knobText, KnobValue, KnobValues, processKnob, shouldRender, slotKnobs } from './storybook.knobs.utils'; - -jest.mock('@storybook/addon-knobs', () => ({ - boolean: jest.fn(), - color: jest.fn(), - date: jest.fn(), - number: jest.fn(), - optionsKnob: jest.fn(), - radios: jest.fn(), - select: jest.fn(), - text: jest.fn(), -})); - -describe('KnobValue', () => { - it('should initialize with correct default values', () => { - const knobValue = new KnobValue(); - expect(knobValue.getValue()).toBe(null); - expect(knobValue.isUsingDefault()).toBe(false); - }); - - it('should initialize with provided values', () => { - const knobValue = new KnobValue('test', true); - expect(knobValue.getValue()).toBe('test'); - expect(knobValue.isUsingDefault()).toBe(true); - }); -}); - -describe('KnobValues', () => { - it('should return default knob values if not provided', () => { - const knobValues = new KnobValues(); - expect(knobValues.nonExistentProp).toEqual(new KnobValue(null, true)); - }); - - it('should return provided knob values', () => { - const knobValues = new KnobValues({ testProp: 'testValue' }); - expect(knobValues.testProp).toBe('testValue'); - }); - - it('should return parent knob values if provided', () => { - const parentKnobs = { parentProp: 'parentValue' }; - const knobValues = new KnobValues({}, true, parentKnobs); - expect(knobValues.parentProp).toBe('parentValue'); - }); -}); - -describe('processKnob', () => { - it('should return default value and render knob if parent is undefined', () => { - const callback = jest.fn(); - processKnob('test', 'default', undefined, 'General', callback); - expect(callback).toHaveBeenCalledWith('test', 'default', 'General'); - }); - - it('should return parent value if parent is not a KnobValue instance', () => { - expect(processKnob('test', 'default', 'parentValue', 'General', jest.fn())) - .toBe('parentValue'); - }); - - it('should return default value if KnobValue is using default', () => { - const knobValue = new KnobValue(null, true); - expect(processKnob('test', 'default', knobValue, 'General', jest.fn())) - .toBe('default'); - }); - - it('should return knob value if KnobValue has a null value', () => { - const callback = jest.fn(); - const knobValue = new KnobValue(null, false); - processKnob('test', 'default', knobValue, 'General', callback); - expect(callback).toHaveBeenCalledWith('test', 'default', 'General'); - }); - - it('should return KnobValue value if it is set', () => { - const callback = jest.fn(); - const knobValue = new KnobValue('knobValue', false); - processKnob('test', 'default', knobValue, 'General', callback); - expect(callback).toHaveBeenCalledWith('test', 'knobValue', 'General'); - }); -}); - -describe('Knob Wrappers', () => { - it('knobText should process knob correctly', () => { - const parent = new KnobValue('parentValue', false); - knobText('test', 'default', parent, 'General'); - expect(text).toHaveBeenCalledWith('test', 'parentValue', 'General'); - }); - - it('knobRadios should process knob correctly', () => { - const parent = new KnobValue('parentValue', false); - knobRadios('test', { option1: 'Option 1' }, 'default', parent, 'General'); - expect(radios) - .toHaveBeenCalledWith('test', { option1: 'Option 1' }, 'parentValue', 'General'); - }); - - it('knobBoolean should process knob correctly', () => { - const parent = new KnobValue(true, false); - knobBoolean('test', false, parent, 'General'); - expect(boolean).toHaveBeenCalledWith('test', true, 'General'); - }); - - it('knobNumber should process knob correctly', () => { - const parent = new KnobValue(42, false); - knobNumber('test', 0, {}, parent, 'General'); - expect(number).toHaveBeenCalledWith('test', 42, {}, 'General'); - }); - - it('knobSelect should process knob correctly', () => { - const parent = new KnobValue('parentValue', false); - knobSelect('test', { option1: 'Option 1' }, 'default', parent, 'General'); - expect(select) - .toHaveBeenCalledWith('test', { option1: 'Option 1' }, 'parentValue', 'General'); - }); - - it('knobColor should process knob correctly', () => { - const parent = new KnobValue('#ffffff', false); - knobColor('test', '#000000', parent, 'General'); - expect(color).toHaveBeenCalledWith('test', '#ffffff', 'General'); - }); - - it('knobOptions should process knob correctly', () => { - const parent = new KnobValue('parentValue', false); - knobOptions('test', { option1: 'Option 1' }, 'default', {}, parent, 'General'); - expect(optionsKnob) - .toHaveBeenCalledWith('test', { option1: 'Option 1' }, 'parentValue', {}, 'General'); - }); - - it('knobDate should process knob correctly', () => { - const parent = new KnobValue(new Date(), false); - knobDate('test', new Date(), parent, 'General'); - expect(dateKnob).toHaveBeenCalledWith('test', parent.getValue(), 'General'); - }); -}); - -describe('shouldRender', () => { - it('should return true if parentKnobs is null or not an object', () => { - expect(shouldRender(null)).toBe(true); - expect(shouldRender('not an object')).toBe(true); - }); - - it('should return true if parentKnobs is an empty object', () => { - expect(shouldRender({})).toBe(true); - }); - - it('should return the shouldRender flag if parentKnobs is an instance of KnobValues', () => { - const knobValues = new KnobValues({}, false); - expect(shouldRender(knobValues)).toBe(false); - }); - - it('should return false if any KnobValue has a non-null value or is not using default', () => { - const parentKnobs = { - prop1: new KnobValue('value', false), - prop2: new KnobValue(null, false), - }; - expect(shouldRender(parentKnobs)).toBe(false); - }); - - it('should return true if all KnobValues are null and using default', () => { - const parentKnobs = { - prop1: new KnobValue(null, true), - prop2: new KnobValue(null, true), - }; - expect(shouldRender(parentKnobs)).toBe(true); - }); -}); - -describe('slotKnobs', () => { - beforeEach(() => { - boolean.mockClear(); - boolean.mockImplementation(() => true); // Mock implementation to return true - }); - - it('should return slot knobs with provided names', () => { - const names = ['slot1', 'slot2']; - const result = slotKnobs(names); - expect(result).toEqual({ - slot1: '
{{ slot1 }}
', - slot2: '
{{ slot2 }}
', - }); - }); - - it('should return an empty object if showSlots is false', () => { - boolean.mockImplementation(() => false); - const names = ['slot1', 'slot2']; - const result = slotKnobs(names); - expect(result).toEqual({}); - }); -}); - -// eslint-disable-next-line no-unused-vars -const knobCallback = jest.fn((name, value, group) => value); - -const dataProviderProcessKnob = () => [ - ['name', 'default', undefined, 'group', knobCallback, 'default'], - ['name', 'default', null, 'group', knobCallback, null], - ['name', 'default', false, 'group', knobCallback, false], - ['name', 'default', true, 'group', knobCallback, true], - - ['name', 'default', 'direct value', 'group', knobCallback, 'direct value'], - ['name', 'default', { a: 'b' }, 'group', knobCallback, { a: 'b' }], - - ['name', 'default', new KnobValue(null), 'group', knobCallback, 'default'], - ['name', 'default', new KnobValue(null, true), 'group', knobCallback, 'default'], - ['name', 'default', new KnobValue(null, false), 'group', knobCallback, 'default'], - - ['name', 'default', new KnobValue('value'), 'group', knobCallback, 'value'], - ['name', 'default', new KnobValue('value', true), 'group', knobCallback, 'default'], - ['name', 'default', new KnobValue('value', false), 'group', knobCallback, 'value'], -]; - -describe.each(dataProviderProcessKnob())( - 'processKnob(%s, %s, %p, %s, knobCallback)', - (name, defaultValue, parent, group, cb, expected) => { - test(`returns ${expected}`, () => { - if (typeof expected === 'object') { - expect(processKnob(name, defaultValue, parent, group, cb)) - .toStrictEqual(expected); - } else { - expect(processKnob(name, defaultValue, parent, group, cb)) - .toBe(expected); - } - }); - }, -); diff --git a/components/00-base/storybook/storybook.layout.utils.test.js b/components/00-base/storybook/storybook.layout.utils.test.js index 44b1f730..9a0ab2c7 100644 --- a/components/00-base/storybook/storybook.layout.utils.test.js +++ b/components/00-base/storybook/storybook.layout.utils.test.js @@ -1,4 +1,5 @@ import { decoratorStoryLayout } from './storybook.layout.utils'; +import { jest } from '@jest/globals'; describe('decoratorStoryLayout', () => { const mockContent = jest.fn(() => '
Content
'); diff --git a/components/00-base/storybook/storybook.random.utils.js b/components/00-base/storybook/storybook.random.utils.js deleted file mode 100644 index ee586a3a..00000000 --- a/components/00-base/storybook/storybook.random.utils.js +++ /dev/null @@ -1,98 +0,0 @@ -// -// Random and demo generators for all Storybook stories. -// -/* eslint max-classes-per-file: 0 */ - -import { LoremIpsum } from 'lorem-ipsum'; - -import seedrandom from 'seedrandom'; -import { capitalizeFirstLetter, convertDate } from './storybook.helpers.utils'; - -export const randomBool = (skew) => { - skew = skew || 0.5; - return Math.random() > skew; -}; - -export const randomInt = (min = 1, max = 100) => { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min) + min); -}; - -export const randomId = (prefix = '') => `random-id-${prefix.length > 0 ? `${prefix}-` : ''}${randomInt(10000, 99999)}`; - -export const randomArrayItem = (array) => array[Math.floor(Math.random() * array.length)]; - -export const randomText = (words, seed = null) => { - seed = seed || Math.random().toString(); - - const lorem = new LoremIpsum({ - sentencesPerParagraph: { - max: 8, - min: 4, - }, - wordsPerSentence: { - max: 16, - min: 4, - }, - random: seedrandom(seed), - }); - - return lorem.generateWords(words); -}; - -export const randomString = (length, seed = null) => randomText(length, seed) - .substring(0, length) - .trim(); - -export const randomName = (length = 8, seed = null) => randomText(length, seed) - .replace(' ', '') - .substring(0, length).trim(); - -export const randomSentence = (words, seed = null) => { - words = words || randomInt(5, 25); - return capitalizeFirstLetter(randomText(words, seed)); -}; - -export const randomUrl = (domain) => { - domain = domain || 'http://example.com'; - return `${domain}/${(Math.random() + 1).toString(36).substring(7)}`; -}; - -export const randomFutureDate = (days = 30) => { - const now = new Date(); - const endDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); - const randomDate = new Date(now.getTime() + Math.random() * (endDate.getTime() - now.getTime())); - - return convertDate(randomDate); -}; - -export const randomLink = (text, url, isNewWindow, isExternal) => `${text || randomSentence(3)}`; - -export const randomLinks = (count, length, domain, prefix) => { - const links = []; - prefix = prefix || 'Link'; - length = length || 0; - - for (let i = 0; i < count; i++) { - links.push({ - text: `${prefix} ${i + 1}${length ? ` ${randomString(randomInt(3, length))}` : ''}`, - url: randomUrl(domain), - is_new_window: randomBool(), - is_external: randomBool(0.8), - }); - } - - return links; -}; - -export const randomTags = (count, rand) => { - const tags = []; - rand = rand || false; - - for (let i = 0; i < count; i++) { - tags.push(`Topic ${i + 1}${rand ? ` ${randomString(randomInt(2, 5))}` : ''}`); - } - - return tags; -}; diff --git a/components/00-base/storybook/storybook.random.utils.test.js b/components/00-base/storybook/storybook.random.utils.test.js deleted file mode 100644 index 0b222311..00000000 --- a/components/00-base/storybook/storybook.random.utils.test.js +++ /dev/null @@ -1,83 +0,0 @@ -import { randomArrayItem, randomBool, randomId, randomInt, randomLinks, randomName, randomSentence, randomString, randomTags, randomText, randomUrl } from './storybook.random.utils'; - -describe('Random Generators', () => { - test.each([ - [0.1, expect.any(Boolean)], - [0.9, expect.any(Boolean)], - [undefined, expect.any(Boolean)], - ])('randomBool(%s) returns %s', (skew, expected) => { - expect(randomBool(skew)).toEqual(expected); - }); - - test.each([ - [1, 100, expect.any(Number)], - [10, 20, expect.any(Number)], - [undefined, undefined, expect.any(Number)], - ])('randomInt(%s, %s) returns %s', (min, max, expected) => { - expect(randomInt(min, max)).toEqual(expected); - }); - - test.each([ - ['', expect.stringContaining('random-id-')], - ['prefix', expect.stringContaining('random-id-prefix-')], - ])('randomId(%s) returns %s', (prefix, expected) => { - expect(randomId(prefix)).toEqual(expected); - }); - - test.each([ - [['a', 'b', 'c'], expect.any(String)], - [[1, 2, 3], expect.any(Number)], - [[], undefined], - ])('randomArrayItem(%s) returns %s', (array, expected) => { - expect(randomArrayItem(array)).toEqual(expected); - }); - - test.each([ - [5, null, expect.any(String)], - [10, 'seed', expect.any(String)], - ])('randomText(%s, %s) returns %s', (words, seed, expected) => { - expect(randomText(words, seed)).toEqual(expected); - }); - - test.each([ - [5, null, expect.any(String)], - [10, 'seed', expect.any(String)], - ])('randomString(%s, %s) returns %s', (length, seed, expected) => { - expect(randomString(length, seed)).toEqual(expected); - }); - - test.each([ - [8, null, expect.any(String)], - [12, 'seed', expect.any(String)], - ])('randomName(%s, %s) returns %s', (length, seed, expected) => { - expect(randomName(length, seed)).toEqual(expected); - }); - - test.each([ - [5, null, expect.any(String)], - [10, 'seed', expect.any(String)], - ])('randomSentence(%s, %s) returns %s', (words, seed, expected) => { - expect(randomSentence(words, seed)).toEqual(expected); - }); - - test.each([ - [undefined, expect.stringContaining('http://')], - ['http://custom.com', expect.stringContaining('http://custom.com/')], - ])('randomUrl(%s) returns %s', (domain, expected) => { - expect(randomUrl(domain)).toEqual(expected); - }); - - test.each([ - [3, 5, 'http://example.com', 'Prefix', expect.any(Array)], - [2, 0, 'http://test.com', 'Test', expect.any(Array)], - ])('randomLinks(%s, %s, %s, %s) returns %s', (count, length, domain, prefix, expected) => { - expect(randomLinks(count, length, domain, prefix)).toEqual(expected); - }); - - test.each([ - [3, false, expect.any(Array)], - [5, true, expect.any(Array)], - ])('randomTags(%s, %s) returns %s', (count, rand, expected) => { - expect(randomTags(count, rand)).toEqual(expected); - }); -}); diff --git a/components/04-templates/page/page.test.js b/components/04-templates/page/page.test.js index 745fec0f..4d51ffcc 100644 --- a/components/04-templates/page/page.test.js +++ b/components/04-templates/page/page.test.js @@ -1,4 +1,5 @@ -import each from 'jest-each'; +import jestEach from 'jest-each'; +const each = jestEach.default; const template = 'components/04-templates/page/page.twig'; diff --git a/jest.config.js b/jest.config.js index 387032ff..63fee1ce 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,18 +1,8 @@ -module.exports = { - transform: { - '^.+\\.jsx?$': ['babel-jest', { - presets: [ - ['@babel/preset-env', { targets: { node: 'current' } }], - ], - }], - }, +export default { testEnvironment: 'jsdom', moduleNameMapper: { '\\.(jpg|jpeg|png|svg|ico|woff|woff2|ttf|eot|webm|avi|mp4)$': 'jest-transform-stub', }, - transformIgnorePatterns: [ - 'node_modules/(?!(@storybook/addon-knobs)/)', - ], coverageDirectory: '.logs/coverage', collectCoverageFrom: [ '**/components/**/*.{js,jsx,twig}', diff --git a/package.json b/package.json index bdf461f8..b88a20d8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "build:watch:new": "node build.js storybook build watch styles styles_variables js assets constants", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "dev": "node build.js cli \"build:watch:new\" \"storybook\"" + "dev": "node build.js cli \"build:watch:new\" \"storybook\"", + "test": "node --experimental-vm-modules node_modules/.bin/jest" }, "devDependencies": { "@alexskrypnyk/scss-variables-extractor": "^0.1.1", @@ -30,8 +31,12 @@ "@storybook/addon-essentials": "^8.4.6", "@storybook/addon-links": "^8.4.6", "@storybook/html-vite": "^8.4.6", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-transform-stub": "^2.0.0", "sass": "1.77.5", "storybook": "^8.4.6", + "twig-testing-library": "^1.2.0", "vite": "^5.4.8", "vite-plugin-twig-drupal": "^1.4.2" } diff --git a/tests/jest.helpers.js b/tests/jest.helpers.js index ea73b41e..eb6e4591 100644 --- a/tests/jest.helpers.js +++ b/tests/jest.helpers.js @@ -1,7 +1,8 @@ import { render, Twig } from 'twig-testing-library'; import * as fs from 'node:fs'; -const dir = __dirname; +const dir = new URL('.', import.meta.url).pathname; + Twig.extendFunction('source', (src) => { if (src.startsWith('@civictheme')) { src = src.replace('@civictheme', dir);