Skip to main content

Component development guidelines

File structure

└── Component
├── __tests__
├── __snapshots__
├── Subcomponent1.test.tsx.snap - Subcomponent1 auto-generated snapshot
├── Subcomponent2.test.tsx.snap - Subcomponent2 auto-generated snapshot
├── Subcomponent1.test.tsx - tests for Subcomponent1
├── Subcomponent2.test.tsx - tests for Subcomponent2

├── styles
├── Subcomponent1.module.css - Subcomponent1 stylings
├── Subcomponent2.module.css - Subcomponent2 stylings

├── declarations
├── Subcomponent1.types.ts
├── Subcomponent2.types.ts

├── docs
├── Subcomponent1.stories.mdx
├── Subcomponent2.stories.mdx

├── utils
├── useComponent.tsx - hooks/helper functions for the component

├── Subcomponent1.tsx - Subcomponent1 presentation and logic implementation
├── Subcomponent2.tsx - Subcomponent2 presentation and logic implementation
└── index.ts - default exports

Presentation

// Global impors (libraries, frameworks)
import React from 'react';

// Local Imports (components, custom hooks, helpers)
import { helperFunction, useComponent } from '../../utils';

// Types Imports
import { ComponentProps } from './Component.types';

// Styles Imports
import styles from './Component.module.css';

export const Component = forwardRef<HTMLDivElement, ComponentProps>(
({
prop1 = "default",
prop2 = false,
}) => {
const data = useComponent({ prop1, prop2 })
return (
<Fragment>
{children}
</Fragment>
)
}
);

Styles

[component].module.css is a css module containing classes that are applied to the component. Global CSS vars should be uses where appropriate.

.component {
background-color: var(--interactive---action---primary);
color: var(--text---action--primary);
border-radius: 4px;
min-width: 100px;
min-height: 50px;
}

.component:hover {
background-color: var(--interactive---action---primary--hover);
}

Declarations

[component].types.ts contains types and interfaces for the component

import { StandardProps } from '@overdose/components'
import { ElementType, ReactNode } from 'react';

export interface ComponentProps extends StandardProps {
/**
* The component used for the root node. Either a string to use a HTML element or a component.
*/
Component?: ElementType;
/**
* @ignore Component children.
*/
children?: ReactNode;
/**
* @ignore Test identificator.
*/
dataTestId?: string;
}

Tests

Components must have at least 65% test coverage, but you should always aim for more where reasonable.

  • Attribute tests - to test all attributes/props of the component
  • Snapshot tests - test to ensure the component node structure is still the same, would require snapshot test update if there is change in component node structure
  • Behaviour tests - to test the component’s behaviour when being interacted by user
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Component } from '../components/button';

const dataTestId = 'test-id';

describe('Button', () => {
describe('Attribute tests', () => {
it('should set `data-test-id` attribute', () => {
const { getByTestId } = render(<Button dataTestId={dataTestId} />);

expect(getByTestId(dataTestId).tagName).toBe('BUTTON');
});

it('should set rel="noreferrer noopener" if "href" and target="_blank" are passed', () => {
const { container } = render(<Button href='#' target='_blank' />);

const relAttr = container.firstElementChild?.getAttribute('rel');

expect(relAttr).toBe('noreferrer noopener');
});

it('should set type="button" by default', () => {
const { container } = render(<Button />);
expect(container.firstElementChild).toHaveAttribute('type', 'button');
});

it('should set type attribute', () => {
const type = 'submit';
const { container } = render(<Button type='submit' />);
expect(container.firstElementChild).toHaveAttribute('type', type);
});

it('should add classes with className props', () => {
const className = 'cool-button';
const { container } = render(<Button className={className} />);
expect(container.firstChild).toHaveClass('cool-button');
});
});
describe('Snapshots tests', () => {
it('should match snapshot', () => {
expect(render(<Button>Test</Button>)).toMatchSnapshot();
});
});

describe('Behaviour tests', () => {
it('should trigger onClick function when clicked', () => {
const onClick = jest.fn();
const { getByTestId } = render(<Button dataTestId={dataTestId} onClick={onClick} />);

const button = getByTestId(dataTestId);
fireEvent.click(button);
expect(onClick).toBeCalledTimes(1);
});

it('should not trigger onClick function when clicked while being disabled', () => {
const onClick = jest.fn();
const { getByTestId } = render(
<Button dataTestId={dataTestId} onClick={onClick} disabled={true} />,
);

const button = getByTestId(dataTestId);
fireEvent.click(button);
expect(onClick).toBeCalledTimes(0);
});

it('should not trigger onClick function when clicked while loading', () => {
const onClick = jest.fn();
const { getByTestId } = render(
<Button dataTestId={dataTestId} onClick={onClick} loading={true} />,
);

const button = getByTestId(dataTestId);
fireEvent.click(button);
expect(onClick).toBeCalledTimes(0);
});
});
});