Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Legends:

- 💥 `useSendMessage`: `init.attachments[].thumbnailURL` must be data URI, otherwise will be ignored, in PR [#5871](https://github.com/microsoft/BotFramework-WebChat/pull/5871), by [@compulim](https://github.com/compulim)
- 👷🏻 Visual regression tests now use 5 consecutive screenshots for stability indication, up from 2, in PR [#5871](https://github.com/microsoft/BotFramework-WebChat/pull/5871), by [@compulim](https://github.com/compulim)
- 💥 Card action buttons the user pressed are styled with the `adaptive-card-renderer__action--selected` class instead of `[aria-pressed='true']`: host CSS that targets `.ac-pushButton[aria-pressed='true']` should target `.ac-pushButton.adaptive-card-renderer__action--selected` instead, in PR [#5900](https://github.com/microsoft/BotFramework-WebChat/pull/5900), by [@OEvgeny](https://github.com/OEvgeny)

### Removed

Expand Down Expand Up @@ -62,6 +63,7 @@ Legends:
- Fixed the "Positive" card action button having less than 4.5:1 color contrast, fixes [#5713](https://github.com/microsoft/BotFramework-WebChat/issues/5713), in PR [#5905](https://github.com/microsoft/BotFramework-WebChat/pull/5905), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed screen readers announcing a single audio, video, or animation in a card as a list item, such as "1 of 1", fixes [#5722](https://github.com/microsoft/BotFramework-WebChat/issues/5722), in PR [#5903](https://github.com/microsoft/BotFramework-WebChat/pull/5903), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed card action buttons being announced as toggle buttons, and Submit being marked as selected when the card's inputs fail validation, fixes [#4583](https://github.com/microsoft/BotFramework-WebChat/issues/4583), [#5716](https://github.com/microsoft/BotFramework-WebChat/issues/5716), [#5719](https://github.com/microsoft/BotFramework-WebChat/issues/5719), [#5745](https://github.com/microsoft/BotFramework-WebChat/issues/5745), and [#5750](https://github.com/microsoft/BotFramework-WebChat/issues/5750), in PR [#5900](https://github.com/microsoft/BotFramework-WebChat/pull/5900), by [@OEvgeny](https://github.com/OEvgeny)

## [4.19.2] - 2026-10-08

Expand Down
137 changes: 76 additions & 61 deletions __tests__/html2/accessibility/adaptiveCard/ariaPushed.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand All @@ -8,30 +8,29 @@
</head>
<body>
<main id="webchat"></main>
<script>
function createActivity(text) {
return {
from: { role: 'bot' },
id: Math.random().toString(36).substr(2, 5),
text,
timestamp: new Date().toISOString(),
type: 'message'
};
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';

/** Checks if the element has an ancestor that fulfill the predicate. */
function hasAncestor(element, predicate) {
let current = element.parentElement;
async function getSelectedButtonTexts() {
const selectedButtonTexts = [];

while (current) {
if (predicate.call(element, current)) {
return true;
}
for (const pushButton of document.querySelectorAll('.ac-pushButton')) {
const { description, properties } = await testHelpers.getAccessibilityNode(pushButton);

current = current.parentElement;
// Action buttons are plain buttons, not toggle buttons.
expect(properties?.find(({ name }) => name === 'pressed')).toBeUndefined();

description?.value === 'Selected' && selectedButtonTexts.push(pushButton.innerText);
}

return false;
return selectedButtonTexts;
}

run(async function () {
Expand Down Expand Up @@ -97,7 +96,8 @@
directLine,
store: testHelpers.createStore(),
styleOptions: {
autoScrollSnapOnActivity: 1
autoScrollSnapOnActivity: 1,
hideSendBox: true
}
},
document.getElementById('webchat')
Expand All @@ -107,53 +107,68 @@
await pageConditions.numActivitiesShown(1);
await pageConditions.scrollToBottomCompleted();

// SETUP: Focus on the send box.
await pageObjects.focusSendBoxTextBox();

// SETUP: There should be 2 set of `ac-actionSet` containers.
expect(document.querySelectorAll('.ac-actionSet')).toHaveLength(2);

// SETUP: All 'ac-pushButton' should not have `aria-pressed="true"`.
Array.from(document.querySelectorAll('.ac-pushButton')).every(pushButton =>
expect(pushButton.hasAttribute('aria-pressed')).not.toBe('true')
);
// SETUP: No 'ac-pushButton' should be selected.
expect(await getSelectedButtonTexts()).toEqual([]);

// SETUP: Focus on the transcript, the card is the last and only activity.
pageObjects.focusTranscript();

// WHEN: Pressing ENTER to focus inside the card.
await host.sendKeys('ENTER');

// THEN: The "Yes" button should have focus and nothing should be selected.
await waitFor(() => expect(document.activeElement).toHaveProperty('innerText', 'Yes'));
expect(await getSelectedButtonTexts()).toEqual([]);
await host.snapshot('local');

// WHEN: Pressing TAB.
await host.sendTab();

// THEN: The "No" button should have focus.
await waitFor(() => expect(document.activeElement).toHaveProperty('innerText', 'No'));
await host.snapshot('local');

// WHEN: Pressing TAB.
await host.sendTab();

// THEN: The "Submit card" button should have focus.
await waitFor(() => expect(document.activeElement).toHaveProperty('innerText', 'Submit card'));
await host.snapshot('local');

// WHEN: Pressing ENTER on the "Submit card" button.
await host.sendKeys('ENTER');

// THEN: Only the "Submit card" button should be selected, and it should keep focus.
expect(document.activeElement).toHaveProperty('innerText', 'Submit card');
await waitFor(() => expect(document.activeElement.hasAttribute('aria-describedby')).toBe(true)); // "Selected" description attached
expect(await getSelectedButtonTexts()).toEqual(['Submit card']);
await host.snapshot('local');

// WHEN: Pressing SHIFT-TAB.
await host.sendShiftTab();

// THEN: The "No" button should have focus, the "Submit card" button should stay selected.
await waitFor(() => expect(document.activeElement).toHaveProperty('innerText', 'No'));
await host.snapshot('local');

// WHEN: Pressing SHIFT-TAB.
await host.sendShiftTab();

// THEN: The "Yes" button should have focus, the "Submit card" button should stay selected.
await waitFor(() => expect(document.activeElement).toHaveProperty('innerText', 'Yes'));
await host.snapshot('local');

// WHEN: Clicking on the card action button ("Submit card").
await host.sendShiftTab(3);
await host.sendKeys('ENTER', 'TAB', 'TAB', 'ENTER');

// THEN: Selected `ac-pushButton` should have `aria-pressed` set to `true`.
Array.from(document.querySelectorAll('.ac-pushButton'))
.filter(pushButton => pushButton.innerText === 'Submit card')
.forEach(pushButton => expect(pushButton.getAttribute('aria-pressed')).toBe('true'));

// THEN: Only one button is selected.
expect(
Array.from(document.querySelectorAll('.ac-pushButton')).filter(
pushButton => pushButton.getAttribute('aria-pressed') === 'true'
)
).toHaveLength(1);

// THEN: Non-selection should not have `aria-pressed` set.
Array.from(document.querySelectorAll('.ac-pushButton'))
.filter(pushButton => pushButton.innerText !== 'Submit card')
.forEach(pushButton => expect(pushButton.getAttribute('aria-pressed')).not.toBe('true'));

// WHEN: Click on the first action set button ("Yes")
await host.sendShiftTab(2);
// WHEN: Pressing ENTER on the "Yes" button.
await host.sendKeys('ENTER');

// THEN: Selected `ac-pushButton` should have `aria-pressed` set to `true`.
Array.from(document.querySelectorAll('.ac-pushButton'))
.filter(pushButton => pushButton.innerText === 'Yes')
.forEach(pushButton => expect(pushButton.getAttribute('aria-pressed')).toBe('true'));

// THEN: Two buttons are selected.
expect(
Array.from(document.querySelectorAll('.ac-pushButton')).filter(
pushButton => pushButton.getAttribute('aria-pressed') === 'true'
)
).toHaveLength(2);
// THEN: Both the "Yes" and "Submit card" buttons should be selected, the "Yes" button should keep focus.
expect(document.activeElement).toHaveProperty('innerText', 'Yes');
await waitFor(() => expect(document.activeElement.hasAttribute('aria-describedby')).toBe(true)); // "Selected" description attached
expect(await getSelectedButtonTexts()).toEqual(['Yes', 'Submit card']);
await host.snapshot('local');
});
</script>
</body>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<body>
<main id="webchat"></main>
<script>
async function getDescription(element) {
return (await testHelpers.getAccessibilityNode(element)).description?.value;
}

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

Expand Down Expand Up @@ -50,40 +54,50 @@
const [firstPushButton, secondPushButton] = Array.from(document.querySelectorAll('.ac-pushButton'));

expect(firstPushButton).toHaveProperty('textContent', 'Button 1');
expect(firstPushButton.getAttribute('aria-pressed')).toBe('false');
expect(firstPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(firstPushButton)).toBeUndefined();
expect(firstPushButton.hasAttribute('aria-posinset')).toBe(false);
expect(firstPushButton.hasAttribute('aria-setsize')).toBe(false);
expect(firstPushButton.hasAttribute('role')).toBe(false);

expect(secondPushButton).toHaveProperty('textContent', 'Show card');
expect(secondPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(secondPushButton)).toBeUndefined();
expect(secondPushButton.hasAttribute('aria-posinset')).toBe(false);
expect(secondPushButton.hasAttribute('aria-setsize')).toBe(false);
expect(secondPushButton.hasAttribute('role')).toBe(false);

await (await directLine.actPostActivity(() => host.click(firstPushButton))).resolveAll();

expect(firstPushButton.getAttribute('aria-pressed')).toBe('true');
expect(firstPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(firstPushButton)).toBe('Selected');

await host.click(secondPushButton);

// `Action.ShowCard` is never selected, its state is indicated by `aria-expanded`.
expect(secondPushButton.getAttribute('aria-expanded')).toBe('true');
expect(await getDescription(secondPushButton)).toBeUndefined();

const [, , thirdPushButton, fourthPushButton] = Array.from(document.querySelectorAll('.ac-pushButton'));

expect(thirdPushButton).toHaveProperty('textContent', 'Button 2');
expect(thirdPushButton.getAttribute('aria-pressed')).toBe('false');
expect(thirdPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(thirdPushButton)).toBeUndefined();
expect(thirdPushButton.hasAttribute('aria-posinset')).toBe(false);
expect(thirdPushButton.hasAttribute('aria-setsize')).toBe(false);
expect(thirdPushButton.hasAttribute('role')).toBe(false);

await (await directLine.actPostActivity(() => host.click(thirdPushButton))).resolveAll();

expect(thirdPushButton.getAttribute('aria-pressed')).toBe('true');
expect(thirdPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(thirdPushButton)).toBe('Selected');

await WebChat.renderWebChat({ directLine, disabled: true, store }, document.getElementById('webchat'));

await host.click(fourthPushButton);

expect(fourthPushButton.getAttribute('aria-pressed')).toBe('false');
expect(fourthPushButton.hasAttribute('aria-pressed')).toBe(false);
expect(await getDescription(fourthPushButton)).toBeUndefined();
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './selectedAction?variant=copilot';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './selectedAction?variant=fluent';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading