Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions ui/elements/AlertBanner/AlertBanner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,38 @@ describe('AlertBanner', () => {
render(<AlertBanner className="custom-class">Banner</AlertBanner>);
expect(screen.getByRole('alert')).toHaveClass('custom-class');
});

// Regression: passing an unknown variant (e.g. typo "information") used to
// crash the component because `ALERT_BANNER_COLOR_MAPPINGS[type][variant]`
// returned undefined and the consumer destructured it. The util now falls
// back to a safe variant + type and the banner renders.
it('does not crash on an unknown variant — falls back to primary', () => {
const warn = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
render(
<AlertBanner variant={'information' as never}>
Banner with unknown variant
</AlertBanner>
);
expect(
screen.getByText('Banner with unknown variant')
).toBeInTheDocument();
warn.mockRestore();
});

it('does not crash on an unknown type — falls back to default', () => {
const warn = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
render(
<AlertBanner type={'fancy' as never}>
Banner with unknown type
</AlertBanner>
);
expect(
screen.getByText('Banner with unknown type')
).toBeInTheDocument();
warn.mockRestore();
});
});
81 changes: 76 additions & 5 deletions ui/elements/AlertBanner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,71 @@ import { BUTTON_VARIANTS_ENUM } from '../Button';
import {
ALERT_BANNER_COLOR_MAPPINGS,
ALERT_BANNER_TYPE_AND_VARIANT_TO_BUTTON_COLOR_MAPPING,
ALERT_BANNER_TYPES_ENUM,
ALERT_BANNER_VARIANTS_ENUM,
DEFAULT_ALERT_VARIANT_ICON_MAPPING,
} from './constants';
import { AlertBannerProps, AlertBannerType, AlertBannerVariant } from './types';

/**
* Resolve a variant value to a safe one, falling back to PRIMARY if the
* provided value is not in the supported enum.
*
* This guards against runtime crashes when consumers pass a string outside
* the `AlertBannerVariant` union (e.g. `'information'`) — without this guard
* the downstream `ALERT_BANNER_COLOR_MAPPINGS[type][variant]` lookup returns
* `undefined`, which then crashes destructuring inside the component.
*/
const resolveVariant = (
variant: AlertBannerVariant | AlertVariant | string | undefined | null
): AlertBannerVariant => {
if (
variant != null &&
Object.values(ALERT_BANNER_VARIANTS_ENUM).includes(
variant as ALERT_BANNER_VARIANTS_ENUM
)
) {
return variant as AlertBannerVariant;
}
if (variant != null && process?.env?.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(
`[grauity] Unknown AlertBanner variant "${variant}" — falling back to "primary". ` +
`Supported variants: ${Object.values(
ALERT_BANNER_VARIANTS_ENUM
).join(', ')}.`
);
}
return ALERT_BANNER_VARIANTS_ENUM.PRIMARY;
};

/**
* Resolve a type value to a safe one, falling back to DEFAULT if the
* provided value is not in the supported enum.
*/
const resolveType = (
type: AlertBannerType | AlertType | string | undefined | null
): AlertBannerType => {
if (
type != null &&
Object.values(ALERT_BANNER_TYPES_ENUM).includes(
type as ALERT_BANNER_TYPES_ENUM
)
) {
return type as AlertBannerType;
}
if (type != null && process?.env?.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(
`[grauity] Unknown AlertBanner type "${type}" — falling back to "default". ` +
`Supported types: ${Object.values(ALERT_BANNER_TYPES_ENUM).join(
', '
)}.`
);
}
return ALERT_BANNER_TYPES_ENUM.DEFAULT;
};

/**
* Get alert banner icon name based on variant and icon prop
*
Expand All @@ -25,33 +86,43 @@ export const getAlertIconName = (
return icon;
}

return DEFAULT_ALERT_VARIANT_ICON_MAPPING[variant];
return DEFAULT_ALERT_VARIANT_ICON_MAPPING[resolveVariant(variant)];
};

/**
* Get alert banner colors based on variant and type
* Get alert banner colors based on variant and type.
*
* Unknown variant or type values fall back to `primary` / `default` instead
* of returning `undefined` (which previously crashed the consumer that
* destructured the result — see https://github.com/Newton-School/grauity/issues
* for context).
*
* @param variant - Alert banner variant
* @param type - Alert banner type
* @returns Alert banner colors
* @returns Alert banner colors (always defined)
*/
export const getAlertBannerColors = (
variant: AlertBannerVariant | AlertVariant,
type: AlertBannerType | AlertType
) => ALERT_BANNER_COLOR_MAPPINGS[type][variant];
) => ALERT_BANNER_COLOR_MAPPINGS[resolveType(type)][resolveVariant(variant)];

/**
* Get button color based on alert banner variant and type.
* Useful for showing correct color for close button.
*
* Unknown variant or type values fall back to `primary` / `default`.
*
* @param variant - Alert banner variant
* @param type - Alert banner type
* @returns Button color
*/
export const getButtonColorFromAlertBannerTypeVariant = (
variant: AlertBannerVariant | AlertVariant,
type: AlertBannerType | AlertType
) => ALERT_BANNER_TYPE_AND_VARIANT_TO_BUTTON_COLOR_MAPPING[type][variant];
) =>
ALERT_BANNER_TYPE_AND_VARIANT_TO_BUTTON_COLOR_MAPPING[resolveType(type)][
resolveVariant(variant)
];

/**
* Get button variant based on alert banner variant and type.
Expand Down
Loading