Skip to content

Commit

Permalink
test(ProductiveCard): add avt test coverage (#6518)
Browse files Browse the repository at this point in the history
* test(ProductiveCard): add avt test coverage

* chore: docs update

* test: add tests for all clickable zones and test focus

* fix: prefix

* fix: pr comment changes

* chore: add deprecation notice for iconDescription

* test: add mouse hover states
  • Loading branch information
devadula-nandan authored Dec 11, 2024
1 parent ea0989a commit dd60a2e
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 9 deletions.
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

0 comments on commit dd60a2e

Please sign in to comment.