Skip to content
218 changes: 211 additions & 7 deletions bulma-ui/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,216 @@
import * as IndexExports from '../index';

describe('index.ts', () => {
it('exports Button component', () => {
expect(IndexExports.Button).toBeDefined();
});
// The package's public runtime API, in full. `index.ts` re-exports whole
// modules with `export *`, so anything a module exports, even for its own
// tests, ships unless this list stops it (#763). Adding or removing a name
// here is a public-API change: make it on purpose, and pick the release type
// to match. `isBrowser` is deprecated and leaves at the next major.
const PUBLIC_EXPORTS = [
'Autocomplete',
'Avatar',
'Avatars',
'Badge',
'Block',
'Box',
'Breadcrumb',
'Button',
'Buttons',
'Card',
'Carousel',
'CarouselItem',
'Cell',
'Checkbox',
'Checkboxes',
'Code',
'Collapse',
'Column',
'Columns',
'ConfigProvider',
'Container',
'Content',
'Control',
'DateInput',
'DateInputBase',
'DateTimeInput',
'DateTimeInputBase',
'Delete',
'Dialog',
'DialogContainer',
'Divider',
'Dropdown',
'DropdownDivider',
'DropdownItem',
'Emphasis',
'Field',
'FieldBody',
'FieldLabel',
'Figure',
'File',
'Footer',
'Grid',
'Hero',
'HeroBody',
'HeroFoot',
'HeroHead',
'Icon',
'IconText',
'Image',
'Input',
'InputBase',
'Level',
'LevelItem',
'LevelLeft',
'LevelRight',
'Link',
'LinkButton',
'ListItem',
'Loading',
'Media',
'MediaContent',
'MediaLeft',
'MediaRight',
'Menu',
'MenuItem',
'MenuLabel',
'MenuList',
'Message',
'Modal',
'Navbar',
'NavbarBrand',
'NavbarBurger',
'NavbarDivider',
'NavbarDropdown',
'NavbarDropdownMenu',
'NavbarEnd',
'NavbarItem',
'NavbarLink',
'NavbarMenu',
'NavbarStart',
'Notification',
'NotificationContainer',
'NumberInput',
'Numberinput',
'OrderedList',
'Pagination',
'PaginationEllipsis',
'PaginationLink',
'PaginationList',
'PaginationNext',
'PaginationPrevious',
'Panel',
'PanelBlock',
'PanelButtonBlock',
'PanelCheckboxBlock',
'PanelHeading',
'PanelIcon',
'PanelInputBlock',
'PanelTabs',
'Paragraph',
'Pre',
'Progress',
'Radio',
'Radios',
'Rate',
'Reveal',
'Section',
'Select',
'SelectBase',
'Sidebar',
'Skeleton',
'Slider',
'Span',
'Step',
'Steps',
'Strong',
'SubTitle',
'Switch',
'Tab',
'TabContentItem',
'TabItem',
'TabList',
'Table',
'Tabs',
'TabsContent',
'Tag',
'TagInput',
'Taginput',
'Tags',
'Tbody',
'Td',
'TextArea',
'TextAreaBase',
'Tfoot',
'Th',
'Thead',
'Theme',
'TimeInput',
'TimeInputBase',
'Title',
'Toast',
'ToastContainer',
'Tooltip',
'Tr',
'UnorderedList',
'checkboxColors',
'checkboxSizes',
'classNames',
'createPrefixedClassNames',
'dialog',
'isBrowser',
'mergeBulmaStyles',
'notification',
'prefixedClassNames',
'radioColors',
'radioSizes',
'switchColors',
'switchSizes',
'toast',
'useBulmaClasses',
'useClassPrefix',
'useColorClasses',
'useColorStyles',
'useConfig',
'useFlexboxClasses',
'useIconLibrary',
'useInsideControl',
'useInsideField',
'useOtherClasses',
'usePrefixedClass',
'usePrefixedClassNames',
'useSpacingClasses',
'useTypographyClasses',
'useVisibilityClasses',
'validAlignContents',
'validAlignItems',
'validAlignSelfs',
'validAlignments',
'validColorShades',
'validColors',
'validCursors',
'validDisplays',
'validFlexDirections',
'validFlexGrowShrink',
'validFlexWraps',
'validFloats',
'validFontFamilies',
'validInteractions',
'validJustifyContents',
'validOverflows',
'validRadii',
'validResponsives',
'validSchemeColors',
'validShadows',
'validSizes',
'validTableColors',
'validTextSizes',
'validTextTransforms',
'validTextWeights',
'validViewports',
'validVisibilities',
];

it('has expected export names', () => {
const exportNames = Object.keys(IndexExports);
expect(exportNames).toContain('Button');
describe('index.ts', () => {
it('exports exactly the public API', () => {
expect(Object.keys(IndexExports).sort()).toEqual(PUBLIC_EXPORTS);
});
});
25 changes: 0 additions & 25 deletions bulma-ui/src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,6 @@ export interface CardProps
children?: React.ReactNode;
}

/**
* Wrap each footer item in a `.card-footer-item` span.
* @param {CardProps['footer']} footer - Footer content (single node or array).
* @param {string | undefined} classPrefix - Bulma class prefix.
* @returns {React.ReactNode[] | null} Wrapped footer items, or null if no footer.
*/
const renderFooter = (
footer: CardProps['footer'],
classPrefix: string | undefined
) => {
if (!footer) return null;
const items = Array.isArray(footer) ? footer : [footer];
return items.map((item, idx) => (
<span
className={prefixedClassNames(classPrefix, 'card-footer-item')}
key={idx}
>
{item}
</span>
));
};

/**
* Check if children contain any Card compound sub-components.
* @param {React.ReactNode} children - The children to inspect.
Expand Down Expand Up @@ -652,6 +630,3 @@ export const Card = withSubComponents(
},
'Card'
);

/** Internal test-only exports. Not part of the public API. */
export const __test_exports__ = { renderFooter };
2 changes: 2 additions & 0 deletions bulma-ui/src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {

/**
* Checks if code is running in a browser environment.
* @deprecated Dropdown's internal SSR guard, exported by accident; it will
* be removed in the next major. Check `typeof window !== 'undefined'` instead.
Comment thread
allxsmith marked this conversation as resolved.
Resolved
* @param win - Window object.
* @param doc - Document object.
* @returns {boolean} True if in browser, false otherwise.
Expand Down
86 changes: 35 additions & 51 deletions bulma-ui/src/components/__tests__/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Card, __test_exports__ } from '../Card';
import { Card } from '../Card';
import { ConfigProvider } from '../../helpers/Config';

describe('Card Component', () => {
Expand Down Expand Up @@ -254,69 +254,53 @@ describe('Card Component', () => {
expect(img).toHaveAttribute('alt', 'Custom Alt');
});

test('renderFooter returns null if footer is falsy', () => {
expect(__test_exports__.renderFooter(undefined, undefined)).toBeNull();
expect(__test_exports__.renderFooter(null, undefined)).toBeNull();
expect(__test_exports__.renderFooter(false, undefined)).toBeNull();
expect(__test_exports__.renderFooter('', undefined)).toBeNull();
expect(__test_exports__.renderFooter(0, undefined)).toBeNull();
});

test('renderFooter wraps a single ReactNode in one card-footer-item', () => {
const { container } = render(
<div>
{__test_exports__.renderFooter(<a href="#one">One</a>, undefined)}
</div>
);
const items = container.querySelectorAll('.card-footer-item');
expect(items).toHaveLength(1);
expect(screen.getByText('One')).toBeInTheDocument();
});
test.each([undefined, null, false, ''])(
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
'renders no footer when footer is %p',
footer => {
const { container } = render(<Card footer={footer}>Test</Card>);
expect(container.querySelector('.card-footer')).toBeNull();
}
);

test('renderFooter wraps each item in card-footer-item when given an array', () => {
test('wraps a single footer node in one card-footer-item', () => {
const { container } = render(
<div>
{__test_exports__.renderFooter(
['Item 1', 'Item 2', 'Item 3'],
undefined
)}
</div>
<Card footer={<a href="#one">One</a>}>Test</Card>
);
const items = container.querySelectorAll('.card-footer-item');
expect(items).toHaveLength(3);
expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
expect(screen.getByText('Item 3')).toBeInTheDocument();
expect(container.querySelectorAll('.card-footer-item')).toHaveLength(1);
expect(screen.getByRole('link', { name: 'One' })).toBeInTheDocument();
});

test('renderFooter renders mixed ReactNode array elements', () => {
test('wraps each footer array item in its own card-footer-item', () => {
const { container } = render(
<div>
{__test_exports__.renderFooter(
[
<a key="a" href="#one">
One
</a>,
<button key="b" type="button">
Two
</button>,
],
undefined
)}
</div>
<Card
footer={[
<a key="a" href="#one">
One
</a>,
<button key="b" type="button">
Two
</button>,
'Three',
]}
>
Test
</Card>
);
const items = container.querySelectorAll('.card-footer-item');
expect(items).toHaveLength(2);
expect(container.querySelectorAll('.card-footer-item')).toHaveLength(3);
expect(screen.getByRole('link', { name: 'One' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Two' })).toBeInTheDocument();
expect(screen.getByText('Three')).toBeInTheDocument();
});

test('renderFooter applies classPrefix to card-footer-item', () => {
test('prefixes footer item classes under ConfigProvider', () => {
const { container } = render(
<div>{__test_exports__.renderFooter(['A', 'B'], 'bulma-')}</div>
<ConfigProvider classPrefix="bulma-">
<Card footer={['A', 'B']}>Test</Card>
</ConfigProvider>
);
expect(container.querySelectorAll('.bulma-card-footer-item')).toHaveLength(
2
);
const items = container.querySelectorAll('.bulma-card-footer-item');
expect(items).toHaveLength(2);
});

test('applies classPrefix when provided via ConfigProvider', () => {
Expand Down
5 changes: 4 additions & 1 deletion bulma-ui/src/elements/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ export interface NotificationOptions {
isLight?: boolean;
/** Duration in ms before auto-close. Default 3000. */
duration?: number;
/** Position on the screen. Default 'top-right'. */
/**
* Position on the screen. Currently ignored: `NotificationContainer`'s own
* `position` places every notification it shows, so set it there.
*/
position?: NotificationPosition;
/** When true, notifications enter a FIFO queue and display one at a time. Default false. */
queue?: boolean;
Expand Down
Loading
Loading