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

223 changes: 223 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,226 @@ 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 buttons = page.locator(`button.${carbon.prefix}--btn`);
const disabledButton = buttons.nth(2);
expect(await disabledButton.getAttribute('disabled')).not.toBeNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we typically try to query by role or text over a specific query selector.

In this case, we could probably do

const disabledButton = page.getByRole('button', { name: 'Read more' });

Or some similar variation of that.


await page.keyboard.press('Tab');
expect(
await buttons.nth(0).evaluate((btn) => document.activeElement === btn)
).toBe(true);

await page.keyboard.press('Tab');
expect(
await buttons.nth(1).evaluate((btn) => document.activeElement === btn)
).toBe(true);
// disabled button
await page.keyboard.press('Tab');
expect(
await buttons.nth(2).evaluate((btn) => document.activeElement === btn)
).toBe(false);
});

// 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.locator('button[aria-haspopup="true"]');
const menu = page.locator('ul[role="menu"]');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably use the roles here as well. :)

https://playwright.dev/docs/locators#locate-by-role


// 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.devtoolsAttribute}="${pkg.prefix}--ProductiveCard"]`
);
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'
);
});
});
3 changes: 1 addition & 2 deletions packages/ibm-products/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ export const Card = forwardRef(
autoAlign
menuAlignment={pos}
size={size}
aria-label={overflowAriaLabel}
label={iconDescription}
label={iconDescription || overflowAriaLabel}
>
{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,7 @@ export interface ProductiveCardProps extends PropsWithChildren {
*/
overflowActions?: overflowAction[];
/**
* Aria label prop required for OverflowMenu
* Aria label prop required for OverflowMenu, also applies to tooltip text on menu button. gets overridden by `iconDescription`
*/
overflowAriaLabel?: string;
/**
Expand Down Expand Up @@ -149,7 +149,7 @@ export interface ProductiveCardProps extends PropsWithChildren {
titleSize?: 'default' | 'large';

/**
* Tooltip icon description
* Tooltip icon description, also applies to Aria label prop required for OverflowMenu. overrides `overflowAriaLabel`
*/
iconDescription?: string;
}
Expand Down Expand Up @@ -229,7 +229,7 @@ ProductiveCard.propTypes = {
PropTypes.node,
]),
/**
* Tooltip icon description
* Tooltip icon description, also applies to Aria label prop required for OverflowMenu. overrides `overflowAriaLabel`
*/
iconDescription: PropTypes.string,
/**
Expand Down Expand Up @@ -265,7 +265,7 @@ ProductiveCard.propTypes = {
})
),
/**
* Aria label prop required for OverflowMenu
* Aria label prop required for OverflowMenu, also applies to tooltip text on menu button. gets overridden by `iconDescription`
*/
overflowAriaLabel: PropTypes.string,
/**
Expand Down
Loading