diff --git a/.changeset/pf-alert.md b/.changeset/pf-alert.md new file mode 100644 index 0000000000..f0642c0364 --- /dev/null +++ b/.changeset/pf-alert.md @@ -0,0 +1,15 @@ +--- +"@patternfly/elements": minor +--- + +### Minor Changes + +- Added `pf-alert` component for displaying alert messages of different types: + - Types: info, warning, danger, success, cogear, neutral, custom + - Features: optional heading, description, actions, dismiss button +- Enables consistent alert messaging across apps and demos + +```html + + This is a warning alert with optional description and actions. + \ No newline at end of file diff --git a/elements/package.json b/elements/package.json index 3f4e952bc7..549ab23384 100644 --- a/elements/package.json +++ b/elements/package.json @@ -15,6 +15,7 @@ "./pf-accordion/pf-accordion-header.js": "./pf-accordion/pf-accordion-header.js", "./pf-accordion/pf-accordion-panel.js": "./pf-accordion/pf-accordion-panel.js", "./pf-accordion/pf-accordion.js": "./pf-accordion/pf-accordion.js", + "./pf-alert/pf-alert.js": "./pf-alert/pf-alert.js", "./pf-avatar/pf-avatar.js": "./pf-avatar/pf-avatar.js", "./pf-back-to-top/pf-back-to-top.js": "./pf-back-to-top/pf-back-to-top.js", "./pf-background-image/pf-background-image.js": "./pf-background-image/pf-background-image.js", diff --git a/elements/pf-alert/README.md b/elements/pf-alert/README.md new file mode 100644 index 0000000000..32622b8a0c --- /dev/null +++ b/elements/pf-alert/README.md @@ -0,0 +1,38 @@ +# pf-alert + +The `pf-alert` web component displays PatternFly-styled alerts. It can be used inline in pages or as a toast notification. Alerts support several visual states (for example: `info`, `success`, `warning`, `danger`), an optional header slot, body content, and an `actions` slot for interactive controls. + +## Installation + +Import the element in your page or application as an ES module: + +```html + +``` + +## Basic usage + +Inline alert example: + +```html + +

Success

+ The operation completed successfully. +
+ Details +
+
+``` + +Toast usage (static helper): + +```html + +``` diff --git a/elements/pf-alert/demo/custom-icon.html b/elements/pf-alert/demo/custom-icon.html new file mode 100644 index 0000000000..20028e7a2d --- /dev/null +++ b/elements/pf-alert/demo/custom-icon.html @@ -0,0 +1,32 @@ +
+ +

Success alert title

+
+ +

Success alert title

+
+
+ + + + \ No newline at end of file diff --git a/elements/pf-alert/demo/expandable.html b/elements/pf-alert/demo/expandable.html new file mode 100644 index 0000000000..6b23af11d2 --- /dev/null +++ b/elements/pf-alert/demo/expandable.html @@ -0,0 +1,47 @@ +
+ +

Success alert title

+ View details + lgnore +
+ + +

Success alert title (expanded)

+

Success alert description. This should tell the user more information about the alert.

+ View details + lgnore +
+ + +

Success alert title

+ View details + lgnore +
+ + +

Success alert title (expanded)

+

Success alert description. This should tell the user more information about the alert.

+ View details + lgnore +
+
+ + + + \ No newline at end of file diff --git a/elements/pf-alert/demo/inline-plain.html b/elements/pf-alert/demo/inline-plain.html new file mode 100644 index 0000000000..afc68f4905 --- /dev/null +++ b/elements/pf-alert/demo/inline-plain.html @@ -0,0 +1,23 @@ +
+ +

Success alert title

+
+
+ + + \ No newline at end of file diff --git a/elements/pf-alert/demo/inline-types.html b/elements/pf-alert/demo/inline-types.html new file mode 100644 index 0000000000..a719ada220 --- /dev/null +++ b/elements/pf-alert/demo/inline-types.html @@ -0,0 +1,40 @@ +
+ + +

Custom inline alert title

+
+ + +

Info inline alert title

+
+ + +

Success inline alert title

+
+ + +

Warning inline alert title

+
+ + + +

Danger inline alert title

+
+
+ + + + + \ No newline at end of file diff --git a/elements/pf-alert/demo/inline-variations.html b/elements/pf-alert/demo/inline-variations.html new file mode 100644 index 0000000000..8a3036009c --- /dev/null +++ b/elements/pf-alert/demo/inline-variations.html @@ -0,0 +1,44 @@ +
+ + +

Success alert title

+

Success alert description. This should tell the user more information about the alert.

+ View details + lgnore +
+ + +

Success alert title

+

Success alert description. This should tell the user more information about the alert. + This is + a link. +

+
+ + +

Success alert title

+ View details + lgnore +
+ + +

Success alert title

+
+
+ + + + \ No newline at end of file diff --git a/elements/pf-alert/demo/types.html b/elements/pf-alert/demo/types.html new file mode 100644 index 0000000000..cf1d6d36a1 --- /dev/null +++ b/elements/pf-alert/demo/types.html @@ -0,0 +1,42 @@ +
+ + +

Default alert title

+
+ + +

Info alert title

+
+ + +

Success alert title

+
+ + +

Warning alert title

+
+ + +

Danger alert title

+
+
+ + + + + + \ No newline at end of file diff --git a/elements/pf-alert/demo/variations.html b/elements/pf-alert/demo/variations.html new file mode 100644 index 0000000000..c8cc02f6a3 --- /dev/null +++ b/elements/pf-alert/demo/variations.html @@ -0,0 +1,66 @@ +
+ + +

Success alert title

+

Success alert description. This should tell the user more information about the alert.

+ View details + lgnore +
+ + +

Success alert title

+

Success alert description. This should tell the user more information about the alert. This is a link.

+
+ + +

Success alert title

+ View details + lgnore +
+ + +

Success alert title

+
+ + +

Success alert title

+
+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur pellentesque neque cursus + enim + fringilla...

+

This example uses ".pf-m-truncate" to limit the title to a single line and truncate any overflow text with + ellipses.

+
+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur pellentesque neque cursus + enim + fringilla tincidunt. Proin lobortis aliquam dictum. Nam vel ullamcorper nulla, nec blandit dolor. Vivamus + pellentesque...

+

This example uses ".pf-m-truncate" and sets "--pf-c-alert__title--max-lines: 2" to limit title to two lines and + truncate any overflow text with ellipses.

+
+
+ + + + + diff --git a/elements/pf-alert/docs/pf-alert.md b/elements/pf-alert/docs/pf-alert.md new file mode 100644 index 0000000000..61f5ec0add --- /dev/null +++ b/elements/pf-alert/docs/pf-alert.md @@ -0,0 +1,17 @@ +{% renderOverview %} + +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/pf-alert/pf-alert.css b/elements/pf-alert/pf-alert.css new file mode 100644 index 0000000000..ec3d0214de --- /dev/null +++ b/elements/pf-alert/pf-alert.css @@ -0,0 +1,148 @@ + +header { + display: flex; + align-items: center; + justify-content: flex-start; +} + +#left-column pf-icon#icon { + margin-right: 0.5rem; +} + + +#container { + align-items: flex-start; + background-color: var(--_background-color); + border-width: var(--pf-global--BorderWidth--md); + border-style: solid; + border-color: var(--_border-color, var(--pf-global--default-color--200)); + border-inline-start-color: transparent; + border-block-end-color: transparent; + border-inline-end-color: transparent; + margin-bottom: 1.5rem; + padding: var(--pf-global--spacer--md); + display: grid; + grid-template-columns: max-content 1fr max-content; + grid-template-areas: + "icon title action" + ". description description" + ". actiongroup actiongroup"; + gap: var(--pf-global--spacer--xs); + font-family: var(--pf-global--FontFamily--text, RedHatText, 'Red Hat Text', Helvetica, Arial, sans-serif); + font-size: var(--pf-global--FontSize--sm); + line-height: var(--pf-global--lineHeight--md); + max-width: var(--pf-c-alert--MaxWidth, initial); + box-shadow: var(--_box-shadow); + + & header ::slotted(*) { + font-family: var(--pf-global--FontFamily--text, RedHatText, 'Red Hat Text', Helvetica, Arial, sans-serif) !important; + font-size: var(--pf-global--FontSize--sm) !important; + line-height: var(--pf-global--lineHeight--md) !important; + margin: 0 !important; + } + +} + +#container.info { + --_border-color: var(--pf-global--palette--purple-500, + #2b9af3); + --_icon-color: var(--pf-global--palette--purple-500, + #2b9af3); + --title--Color: var(--pf-global--palette--purple-500, + #002952); + --_background-color: var(--pf-global--palette--purple-50, + #e7f1fa); +} + +#container.cogear { + --_border-color: var(--pf-global--success-color--100, + #3e8635); + --_icon-color: var(--pf-global--success-color--100, + #3e8635); + --title--Color: var(--pf-global--palette--purple-500, + #1e4f18); + --_background-color: var(--pf-global--palette--green-50, + #f3faf2); +} + +#container.success { + --_border-color: var(--pf-global--success-color--100, + #3e8635); + --_icon-color: var(--pf-global--success-color--100, + #3e8635); + --title--Color: var(--pf-global--palette--purple-500, + #1e4f18); + --_background-color: var(--pf-global--palette--green-50, + #f3faf2); +} + +#container.warning { + --_border-color: var(--pf-global--warning-color--100, + #f0ab00); + --_icon-color: var(--pf-global--warning-color--100, + #f0ab00); + --title--Color: var(--pf-global--palette--purple-500, + #795600); + --_background-color: var(--pf-global--palette--gold-50, + #fdf7e7); +} + +#container.custom { + --_background-color: var(--pf-global--BackgroundColor--100, + #f2f9f9); + --_border-color: var(--pf-global--BorderColor--100, + #009596); + --_icon-color: var(--pf-global--icon--Color--light, + #009596); + --title--Color: var(--pf-global--palette--gold-50, + #003737); + +} + +#container.danger { + --_border-color: var(--pf-global--danger-color--200, + #c9190b); + --_icon-color: var(--pf-global--danger-color--200, + #c9190b); + --title--Color: var(--pf-global--palette--purple-500, + #a30000); + --_background-color: var(--pf-global--palette--red-50, + #faeae8); +} +#container header ::slotted(h3) { + font-weight: bold; + color: var(--title--Color); +} + + + +#header { + flex: 1 1 auto; +} + +#icon { + --pf-icon--size: 18px; + display: flex; + align-items: center; + justify-content: center; + color: var(--_icon-color); +} + +#left-column pf-icon#arrow-icon, +#header-actions pf-icon#close-button { + --pf-icon--size: 16px; + --pf-c-icon--Color: #6a6e73; + color: #6a6e73; + cursor: pointer; + transition: color 0.2s ease, --pf-c-icon--Color 0.2s ease; +} + +#left-column pf-icon#arrow-icon:hover, +#left-column pf-icon#arrow-icon:active, +#left-column pf-icon#arrow-icon.active, +#header-actions pf-icon#close-button:hover, +#header-actions pf-icon#close-button:active, +#header-actions pf-icon#close-button.active { + --pf-c-icon--Color: #000000; + color: #000000; +} \ No newline at end of file diff --git a/elements/pf-alert/pf-alert.ts b/elements/pf-alert/pf-alert.ts new file mode 100644 index 0000000000..b69aeea0f5 --- /dev/null +++ b/elements/pf-alert/pf-alert.ts @@ -0,0 +1,189 @@ +import { LitElement, type TemplateResult, html, isServer } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; +import styles from './pf-alert.css'; +import '@patternfly/elements/pf-icon/pf-icon.js'; +import '@patternfly/elements/pf-button/pf-button.js'; + + +interface AlertAction { + action: 'dismiss' | 'confirm' | string; + text: string; +} +interface ToastOptions { + id?: string; + message: string | TemplateResult; + heading?: string; + state?: PfAlert['status']; + persistent?: boolean; + actions?: AlertAction[]; +} +const ICONS = new Map(Object.entries({ + neutral: 'minus-circle', + info: 'info-circle', + success: 'check-circle', + custom: 'bell', + cogear: 'cog', + warning: 'exclamation-triangle', + danger: 'exclamation-circle', + close: 'times', + +})); + + +export class AlertCloseEvent extends Event { + constructor(public action: 'close' | 'confirm' | 'dismiss' | string) { + super('close', { bubbles: true, cancelable: true }); + } +} +// let toaster: HTMLElement; +const toasts = new Set>(); + + +@customElement('pf-alert') +export class PfAlert extends LitElement { + static readonly styles: CSSStyleSheet[] = [styles]; + + @property({ reflect: true }) + status: + | 'warning' + | 'custom' + | 'neutral' + | 'info' + | 'success' + | 'danger' = 'neutral'; + + @property({ reflect: true }) variant?: 'alternate' | 'inline'; + + @property({ reflect: true, type: Boolean }) dismissable = false; + + #slots = new SlotController(this, 'header', null, 'actions'); + + get #icon() { + const internalStatus = this.closest('.demo-with-arrows') + && this.status === 'neutral' && this.classList.contains('cogear-demo') ? + 'cogear' + : this.status; + switch (internalStatus) { + // @ts-expect-error: support for deprecated props + case 'note': return ICONS.get('info'); + // @ts-expect-error: support for deprecated props + case 'default': return ICONS.get('neutral'); + // @ts-expect-error: support for deprecated props + case 'error': return ICONS.get('danger'); + default: return ICONS.get(internalStatus); + } + } + + // #aliasState(state: string) { + // switch (state.toLowerCase()) { + // // the first three are deprecated pre-DPO status names + // case 'note': return 'info'; + // case 'default': return 'neutral'; + // case 'error': return 'danger'; + // // the following are DPO-approved status names + // case 'danger': + // case 'warning': + // case 'custom': + // case 'neutral': + // case 'info': + // case 'success': + // case 'cogear': + // return state.toLowerCase() as this['status']; + // default: + // return 'neutral'; + // } + // } + + connectedCallback(): void { + super.connectedCallback(); + if (!isServer) { + this.requestUpdate(); + } + } + + render(): TemplateResult<1> { + const _isServer = isServer && !this.hasUpdated; + const hasActions = _isServer || this.#slots.hasSlotted('actions'); + const hasBody = + _isServer || this.#slots.hasSlotted(SlotController.default as unknown as string); + const { variant = 'inline' } = this; + // const state = this.#aliasState(this.status); + const inDemo = this.closest('.demo-with-arrows') !== null; + const hasDescription = this.querySelector('p') !== null; + const showArrow = inDemo; + const internalStatus = ( + this.closest('.demo-with-arrows') + && this.classList.contains('cogear-demo')) ? + 'cogear' + : this.status; + const arrowDirection = hasDescription ? 'angle-down' : 'angle-right'; + const footer = html``; + return html` + + `; + } +} + + +declare global { + interface HTMLElementTagNameMap { + 'pf-alert': PfAlert; + } +} diff --git a/elements/pf-alert/test/MANUAL_TESTS.md b/elements/pf-alert/test/MANUAL_TESTS.md new file mode 100644 index 0000000000..bc066ff899 --- /dev/null +++ b/elements/pf-alert/test/MANUAL_TESTS.md @@ -0,0 +1,88 @@ +# pf-alert — Manual Tests + +This document outlines manual testing procedures for the `pf-alert` component. + +## Setup +1. Start the development server: +```bash +npm run start +``` +2. Open the demo page in your browser +3. Have a screen reader ready (e.g., NVDA, VoiceOver) + +## Visual Tests + +### Base Alert +- [ ] Renders with correct default styling +- [ ] Title text is clearly visible +- [ ] Content text is properly formatted +- [ ] Icon matches the alert state +- [ ] Close button (if dismissable) is properly positioned + +### States +Test each state and verify proper styling: +- [ ] Default +- [ ] Success (green) +- [ ] Warning (orange/yellow) +- [ ] Danger (red) +- [ ] Info (blue) + +### Variants +Test each variant: +- [ ] Inline +- [ ] Toast +- [ ] Default + +## Interaction Tests + +### Keyboard Navigation +- [ ] Tab key focuses interactive elements in correct order +- [ ] Enter/Space triggers action buttons +- [ ] Escape key dismisses alert (if dismissable) +- [ ] Focus is properly trapped in modal alerts (if applicable) + +### Mouse Interaction +- [ ] Click on close button dismisses alert +- [ ] Action buttons respond to clicks +- [ ] Toast alerts can be dismissed via close button + +### Screen Reader Tests +Using NVDA or VoiceOver: +- [ ] Alert role is announced +- [ ] Alert state (success/warning/etc) is announced +- [ ] Title and content are read in correct order +- [ ] Interactive elements are properly announced +- [ ] Dismissal is announced + +## Functional Tests + +### Toast API +- [ ] `PfAlert.toast()` creates visible toast +- [ ] Toast appears in correct position +- [ ] Auto-dismiss works with specified duration +- [ ] Multiple toasts stack correctly + +### State Changes +- [ ] Changing state updates styling immediately +- [ ] Changing variant updates layout immediately +- [ ] Adding/removing `dismissable` updates UI +- [ ] Slot content updates reflect immediately + +## Accessibility Tests +- [ ] Run aXe or similar tool +- [ ] Verify color contrast meets WCAG standards +- [ ] Verify all interactive elements are keyboard accessible +- [ ] Check aria attributes are present and correct +- [ ] Test with screen reader in different browsers + +## Browser Testing +Test in: +- [ ] Chrome +- [ ] Firefox +- [ ] Safari +- [ ] Edge + +## Notes +- Document any bugs or inconsistencies found +- Note any browser-specific issues +- Record accessibility concerns \ No newline at end of file diff --git a/elements/pf-alert/test/pf-alert.e2e.ts b/elements/pf-alert/test/pf-alert.e2e.ts new file mode 100644 index 0000000000..82bff11ab3 --- /dev/null +++ b/elements/pf-alert/test/pf-alert.e2e.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; +import { SSRPage } from '@patternfly/pfe-tools/test/playwright/SSRPage.js'; + +const tagName = 'pf-alert'; + +test.describe(tagName, () => { + test.beforeEach(async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + }); + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); + + test('ssr', async ({ browser }) => { + const fixture = new SSRPage({ + tagName, + browser, + demoDir: new URL('../demo/', import.meta.url), + importSpecifiers: [ + `@patternfly/elements/${tagName}/${tagName}.js`, + ], + }); + await fixture.snapshots(); + }); + + test('keyboard navigation works correctly', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + + // Start with focus on body + await page.focus('body'); + + // Tab should move focus to first interactive element + await page.keyboard.press('Tab'); + const focusedElement = await page.evaluate(() => document.activeElement?.tagName); + expect(focusedElement).toBeTruthy(); + + // If alert has actions, they should be focusable + const hasActions = await page.$('pf-alert [slot="actions"]'); + if (hasActions) { + await page.keyboard.press('Tab'); + const actionFocused = await page.evaluate(() => + document.activeElement?.closest('[slot="actions"]') !== null + ); + expect(actionFocused).toBeTruthy(); + } + }); + + test('WAI-ARIA compliance', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + + // Test inline alert role + const inlineAlert = await page.$('pf-alert[variant="inline"]'); + if (inlineAlert) { + const role = await inlineAlert.getAttribute('role'); + expect(role).toBe('alert'); + } + + // Test toast alert role + const toastAlert = await page.$('pf-alert[variant="toast"]'); + if (toastAlert) { + const role = await toastAlert.getAttribute('role'); + expect(role).toBe('status'); + } + + // Check dismissable alerts have proper close button + const dismissableAlert = await page.$('pf-alert[dismissable]'); + if (dismissableAlert) { + const closeButton = await dismissableAlert.$('#close-button'); + expect(await closeButton?.getAttribute('role')).toBe('button'); + expect(await closeButton?.getAttribute('tabindex')).toBe('0'); + expect(await closeButton?.getAttribute('aria-label')).toBe('Close'); + } + }); + + test('accessibility - roles and attributes', async ({ page }) => { + // Test inline alert role + const inlineAlert = await page.locator('pf-alert[variant="inline"]').first(); + if (await inlineAlert.count() > 0) { + expect(await inlineAlert.getAttribute('role')).toBe('alert'); + } + + // Test toast alert role + const toastAlert = await page.locator('pf-alert[variant="toast"]').first(); + if (await toastAlert.count() > 0) { + expect(await toastAlert.getAttribute('role')).toBe('status'); + } + + // Verify aria attributes + const alert = await page.locator('pf-alert').first(); + expect(await alert.getAttribute('aria-hidden')).not.toBe('true'); + }); + + test('screen reader content structure', async ({ page }) => { + // Test header content is properly structured + const alertWithHeader = await page.locator('pf-alert:has([slot="header"])').first(); + if (await alertWithHeader.count() > 0) { + const header = await alertWithHeader.locator('[slot="header"]'); + expect(await header.count()).toBe(1); + } + + // Test main content area + const alertWithContent = await page.locator('pf-alert:has(p)').first(); + if (await alertWithContent.count() > 0) { + const content = await alertWithContent.locator('p'); + expect(await content.count()).toBeGreaterThan(0); + } + }); + + + test('visual statuses and variants', async ({ page }) => { + // Test each status renders + for (const status of ['success', 'warning', 'danger', 'info']) { + const alert = await page.locator(`pf-alert[status ="${status}"]`).first(); + if (await alert.count() > 0) { + // Verify icon exists for status + const icon = await alert.locator('#icon'); + expect(await icon.count()).toBe(1); + } + } + + // Test variants render + for (const variant of ['inline', 'toast']) { + const alert = await page.locator(`pf-alert[variant="${variant}"]`).first(); + if (await alert.count() > 0) { + // Verify basic structure + const container = await alert.locator('#container'); + expect(await container.count()).toBe(1); + } + } + }); +}); diff --git a/elements/pf-alert/test/pf-alert.spec.ts b/elements/pf-alert/test/pf-alert.spec.ts new file mode 100644 index 0000000000..eb2683a8eb --- /dev/null +++ b/elements/pf-alert/test/pf-alert.spec.ts @@ -0,0 +1,73 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfAlert, AlertCloseEvent } from '@patternfly/elements/pf-alert/pf-alert.js'; +import { oneEvent } from '@open-wc/testing'; + +describe('', function() { + describe('simply instantiating', function() { + let element: PfAlert; + + it('imperatively instantiates', function() { + expect(document.createElement('pf-alert')).to.be.an.instanceof(PfAlert); + }); + + it('should upgrade', async function() { + element = await createFixture(html``); + const klass = customElements.get('pf-alert'); + expect(element) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfAlert); + }); + }); + + describe('attributes and properties', function() { + it('reflects state attribute', async function() { + const el = await createFixture(html``); + expect(el.getAttribute('state')).to.equal('success'); + expect(el.status).to.equal('success'); + }); + + it('reflects variant attribute', async function() { + const el = await createFixture(html``); + expect(el.getAttribute('variant')).to.equal('inline'); + expect(el.variant).to.equal('inline'); + }); + }); + + describe('slots and rendering', function() { + it('renders header slot content', async function() { + const el = await createFixture(html` + +

Alert Title

+

Alert content

+
+ `); + const header = el.querySelector('[slot="header"]'); + expect(header).to.exist; + expect(header?.textContent?.trim()).to.equal('Alert Title'); + }); + + it('renders action buttons with correct attributes', async function() { + const el = await createFixture(html` + +
+ Action +
+
+ `); + const actionButton = el.querySelector('[slot="actions"] pf-button'); + expect(actionButton).to.exist; + }); + }); + + describe('events and interactions', function() { + it('emits close event when close button clicked', async function() { + const el = await createFixture(html``); + const closeButton = el.shadowRoot?.querySelector('#close-button'); + setTimeout(() => closeButton?.dispatchEvent(new MouseEvent('click'))); + const event = await oneEvent(el, 'close') as AlertCloseEvent; + expect(event.action).to.equal('dismiss'); + }); + }); +});