Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(ProductiveCard): add avt test coverage #6518

250 changes: 250 additions & 0 deletions e2e/components/ProductiveCard/ProductiveCard-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { expect, test } from '@playwright/test';
import { visitStory } from '../../test-utils/storybook';
import { carbon, pkg } from '../../../packages/ibm-products/src/settings';

test.describe('ProductiveCard @avt', () => {
test('@avt-default-state', async ({ page }) => {
Expand All @@ -23,4 +24,253 @@ test.describe('ProductiveCard @avt', () => {
'ProductiveCard @avt-default-state'
);
});

test('@avt-with-caption', async ({ page }) => {
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--with-caption',
globals: {
carbonTheme: 'white',
},
});
await expect(page).toHaveNoACViolations('ProductiveCard @avt-with-caption');
});

// Disabled state test
test('@avt-disabled: validates disabled button state', async ({ page }) => {
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--with-action-ghost-button',
});

await expect(page).toHaveNoACViolations('ProductiveCard @avt-disabled');
const editButton = page.getByRole('button', { name: 'Edit' });
const deleteButton = page.getByRole('button', { name: 'Delete' });
const disabledButton = page.getByRole('button', { name: 'Read more' });
expect(disabledButton.getAttribute('disabled')).not.toBeNull();

await page.keyboard.press('Tab');
expect(editButton).toBeFocused();

await page.keyboard.press('Tab');
expect(deleteButton).toBeFocused();
// disabled button
await page.keyboard.press('Tab');
expect(
await disabledButton.evaluate((btn) => document.activeElement !== btn)
).toBe(true);
});

// Overflow menu open/close states test
test('@avt-overflow-menu: validates overflow menu interactions', async ({
page,
}) => {
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--with-overflow',
});

const menuButton = page.getByRole('button', { label: 'Option' });
const menu = page.getByRole('menu');

// Check initial state
expect(await menuButton.getAttribute('aria-expanded')).toBe('false');

// Open the menu
await menuButton.click();

// Wait for menu to be visible
await expect(menu).toBeVisible();

expect(await menuButton.getAttribute('aria-expanded')).toBe('true');
await expect(page).toHaveNoACViolations('ProductiveCard @menu-open');

// Close the menu with Escape
await page.keyboard.press('Escape');
await expect(menu).not.toBeVisible();

expect(await menuButton.getAttribute('aria-expanded')).toBe('false');
await expect(page).toHaveNoACViolations('ProductiveCard @menu-closed');

// Reopen the menu via keyboard
await page.keyboard.press('Tab');
expect(
await menuButton.evaluate((btn) => document.activeElement === btn)
).toBe(true);

await page.keyboard.press('Enter');
await expect(menu).toBeVisible();

// Check menu item count and focus
const menuItems = page.locator(`li.${carbon.prefix}--menu-item`);
expect(await menuItems.count()).toBeGreaterThan(0);
expect(
await menuItems.first().evaluate((btn) => document.activeElement === btn)
).toBe(true);
expect(await menuButton.getAttribute('aria-expanded')).toBe('true');

// Ensure the menu is closed when pressing Escape
await page.keyboard.press('Escape');
// Focus returns to menu button
expect(
await menuButton.evaluate((btn) => document.activeElement === btn)
).toBe(true);

// Check final state
await expect(menu).not.toBeVisible();
});

test('@avt-keyboard: validates keyboard navigation for all interactive elements', async ({
page,
}) => {
// Navigate to the "Supplemental Bottom Bar" story for ProductiveCard, that has all interactive elements
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--supplemental-bottom-bar',
});

// Ensure no accessibility violations for the story
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - Supplemental Bottom Bar'
);

// Move focus to the Edit button and validate
await page.keyboard.press('Tab');
const editButton = page.getByLabel('Edit');
await expect(editButton).toBeVisible();
await expect(editButton).toBeFocused();
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - Edit Button'
);

// Move focus to the Delete button and validate
await page.keyboard.press('Tab');
const deleteButton = page.getByLabel('Delete');
await expect(deleteButton).toBeVisible();
await expect(deleteButton).toBeFocused();
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - Delete Button'
);

// Move focus to the Read more button and validate
await page.keyboard.press('Tab');
const readMoreButton1 = page.getByText('Read more');
await expect(readMoreButton1).toBeVisible();
await expect(readMoreButton1).toBeFocused();
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - Read more Button'
);

// Tab Navigation in "Clickable Card" story for ProductiveCard, (zone one is default, whole card recieves focus)
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--clickable',
});

// Ensure no accessibility violations for the story
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - Clickable Card'
);

// Move focus to the card element and validate
await page.keyboard.press('Tab');
const zone1 = page.locator(`.${pkg.prefix}--card__clickable`);
await expect(zone1).toBeFocused();
await expect(zone1).toHaveAttribute('role', 'button');

// Move focus to the Read more button and validate
await page.keyboard.press('Tab');
const readMoreButton2 = page.getByText('Read more');
await expect(readMoreButton2).toBeVisible();
await expect(readMoreButton2).toBeFocused();

// Validate zone two focus
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--clickable&args=clickZone:two',
});
await page.keyboard.press('Tab');

const zone2 = page.locator(`.${pkg.prefix}--card__header-body-container`);
await expect(zone2).toBeFocused();
await expect(zone2).toHaveAttribute('role', 'button');

// Move focus to the Read more button and validate
await page.keyboard.press('Tab');
const readMoreButton3 = page.getByText('Read more');
await expect(readMoreButton3).toBeVisible();
await expect(readMoreButton3).toBeFocused();

// Validate zone three focus
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--clickable&args=clickZone:three',
});
await page.keyboard.press('Tab');
const zone3 = page.locator(`.${pkg.prefix}--card__body`);
await expect(zone3).toBeFocused();
await expect(zone3).toHaveAttribute('role', 'button');

// Move focus to the Read more button and validate
await page.keyboard.press('Tab');
const readMoreButton4 = page.getByText('Read more');
await expect(readMoreButton4).toBeVisible();
await expect(readMoreButton4).toBeFocused();

// Navigate to the "button with href" story for ProductiveCard
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--with-button-href',
});

// Ensure no accessibility violations for the story
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - button with href'
);

// Move focus to the href button and validate
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
const hrefButton = page.getByText('Read more');
await expect(hrefButton).toHaveAttribute('href', '#');
await expect(hrefButton).toBeVisible();
await expect(hrefButton).toBeFocused();
await expect(page).toHaveNoACViolations(
'ProductiveCard @keyboard-navigation - href Button'
);
});

// hover states
test('@avt-hover: validates hover states', async ({ page }) => {
await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--with-overflow',
});
const menuButton = page.getByRole('button', { label: 'Overflow menu' });
const tooltip = page.getByRole('tooltip', { name: 'Overflow menu' });

await menuButton.hover();
await expect(page).toHaveNoACViolations(
'ProductiveCard @hover - with overflow'
);
await expect(tooltip).toBeVisible();

await visitStory(page, {
component: 'ProductiveCard',
id: 'ibm-products-components-cards-productivecard--default',
});
const editButton = page.getByLabel('Edit');
const editTooltip = page.getByRole('tooltip', { name: 'Edit' });
const deleteButton = page.getByLabel('Delete');
const deleteTooltip = page.getByRole('tooltip', { name: 'Delete' });

await editButton.hover();
await expect(page).toHaveNoACViolations('ProductiveCard @hover - default');
await expect(editTooltip).toBeVisible();

await deleteButton.hover();
await expect(page).toHaveNoACViolations('ProductiveCard @hover - default');
await expect(deleteTooltip).toBeVisible();
});
});
5 changes: 2 additions & 3 deletions packages/ibm-products/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ export const Card = forwardRef(
onClick,
onKeyDown,
onPrimaryButtonClick,
onSecondaryButtonClick,
overflowActions = Object.freeze([]),
overflowAriaLabel,
onSecondaryButtonClick,
pictogram: Pictogram,
primaryButtonDisabled,
primaryButtonHref,
Expand Down Expand Up @@ -179,8 +179,7 @@ export const Card = forwardRef(
autoAlign
menuAlignment={pos}
size={size}
aria-label={overflowAriaLabel}
label={iconDescription}
label={overflowAriaLabel || iconDescription}
>
{overflowActions.map(({ id, itemText, ...rest }) => (
<MenuItem key={id} label={itemText} {...rest} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export interface ProductiveCardProps extends PropsWithChildren {
*/
overflowActions?: overflowAction[];
/**
* Aria label prop required for OverflowMenu
* Sets the text for the OverflowMenu aria label and the OverflowMenu trigger button tooltip.
* Overrides `iconDescription` prop.
*/
overflowAriaLabel?: string;
/**
Expand Down Expand Up @@ -149,14 +150,17 @@ export interface ProductiveCardProps extends PropsWithChildren {
titleSize?: 'default' | 'large';

/**
* Tooltip icon description
* Sets the text for the OverflowMenu trigger button tooltip and OverflowMenu aria label,
* gets overridden by the `overflowAriaLabel` prop.
*
* @deprecated Please use the `overflowAriaLabel` prop instead.
*/
iconDescription?: string;
}

export let ProductiveCard = forwardRef(
(
{ actionsPlacement = 'top', iconDescription, ...rest }: ProductiveCardProps,
{ actionsPlacement = 'top', ...rest }: ProductiveCardProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const validProps = prepareProps(rest, [
Expand All @@ -171,7 +175,6 @@ export let ProductiveCard = forwardRef(
<Card
{...{
...validProps,
iconDescription,
actionsPlacement,
ref,
productive: true,
Expand Down Expand Up @@ -229,7 +232,10 @@ ProductiveCard.propTypes = {
PropTypes.node,
]),
/**
* Tooltip icon description
* Sets the text for the OverflowMenu trigger button tooltip and OverflowMenu aria label,
* gets overridden by the `overflowAriaLabel` prop.
*
* @deprecated Please use the `overflowAriaLabel` prop instead.
*/
iconDescription: PropTypes.string,
/**
Expand Down Expand Up @@ -265,7 +271,8 @@ ProductiveCard.propTypes = {
})
),
/**
* Aria label prop required for OverflowMenu
* Sets the text for the OverflowMenu aria label and the OverflowMenu trigger button tooltip.
* Overrides `iconDescription` prop.
*/
overflowAriaLabel: PropTypes.string,
/**
Expand Down
Loading