Skip to content

Commit

Permalink
feat: add keyboard navigation short sentences, closes #642
Browse files Browse the repository at this point in the history
  • Loading branch information
luifr10 committed May 31, 2024
1 parent f9a2496 commit 0f3a3ab
Show file tree
Hide file tree
Showing 26 changed files with 834 additions and 348 deletions.
7 changes: 1 addition & 6 deletions packages/assistant/src/UuvAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,11 @@ function UuvAssistant(props: UuvAssistantProps) {
.find((el: BaseSentence) => el.key === "key.then.element.withSelectorFocused");
const focusByRoleAndNameControlSentence = jsonBase
.find((el: BaseSentence) => el.key === "key.then.element.withRoleAndNameFocused");
const nextFocusedElementSentence = jsonBase
.find((el: BaseSentence) => el.key === "key.when.keyboard.nextElement");

if (focusBySelectorControlSentence && focusByRoleAndNameControlSentence && nextFocusedElementSentence) {
if (focusBySelectorControlSentence && focusByRoleAndNameControlSentence) {
const result = await translate(node, ActionEnum.KEYBOARD_GLOBAL_NAVIGATION);
result.sentences.forEach((element) => {
sentences.push(element);
if (node !== focusableElements[focusableElements.length - 1]) {
sentences.push(StepCaseEnum.AND + nextFocusedElementSentence.wording);
}
});
} else {
console.error("sentences next focus element or check focused element is undefined");
Expand Down
136 changes: 87 additions & 49 deletions packages/assistant/src/helper/TranslateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,78 +94,119 @@ export class TranslateHelper {
/* eslint-disable @typescript-eslint/no-explicit-any */
public static async translateEngine(htmlElem: FocusableElement, checkAction: string, isDisabled: boolean): Promise<TranslateSentences> {
const jsonBase: BaseSentence[] = enSentences;
let computedKey = "";
let stepCase = "";
let response = this.getSentenceFromDomSelector(checkAction, jsonBase, htmlElem);
const accessibleRole = getRole(htmlElem);
const accessibleName = computeAccessibleName(htmlElem);
const content = htmlElem.getAttribute("value") ?? htmlElem.firstChild?.textContent?.trim();
if (accessibleRole && accessibleName) {
response = this.getSentenceFromAccessibleRoleAndNameAndContent(
checkAction,
accessibleRole,
accessibleName,
jsonBase,
content,
isDisabled
);
} else {
response.suggestion = new Suggestion();
}
return response;
}

private static getSentenceFromDomSelector(checkAction: string, jsonBase: BaseSentence[], htmlElem: HTMLElement | SVGElement) {
const response: TranslateSentences = {
suggestion: undefined,
sentences: []
};
let computedKey = "";
let stepCase = StepCaseEnum.THEN;
let nextFocusedElementSentence;
if (checkAction === ActionEnum.EXPECT) {
computedKey = "key.then.element.withSelector";
stepCase = StepCaseEnum.THEN;
} else if (checkAction === ActionEnum.WITHIN || checkAction === ActionEnum.CLICK ) {
} else if (checkAction === ActionEnum.WITHIN || checkAction === ActionEnum.CLICK) {
computedKey = "key.when.withinElement.selector";
stepCase = StepCaseEnum.WHEN;
} else if (checkAction === ActionEnum.KEYBOARD_GLOBAL_NAVIGATION) {
computedKey = "key.then.element.withSelectorFocused";
stepCase = StepCaseEnum.THEN;
nextFocusedElementSentence = jsonBase
.find((el: BaseSentence) => el.key === "key.when.keyboard.nextElement");
}
const sentence = jsonBase
.filter((el: BaseSentence) => el.key === computedKey)
.map((el: BaseSentence) =>
el.wording.replace("{string}", `"${this.getSelector(htmlElem)}"`)
)[0];
.filter((el: BaseSentence) => el.key === computedKey)
.map((el: BaseSentence) =>
el.wording.replace("{string}", `"${this.getSelector(htmlElem)}"`)
)[0];
if (checkAction === ActionEnum.CLICK) {
const clickSentence: BaseSentence = jsonBase.filter((el: BaseSentence) => el.key === "key.when.click.withContext")[0];
response.sentences = [stepCase + sentence, StepCaseEnum.THEN + clickSentence.wording];
} else if (computedKey === "key.then.element.withSelectorFocused" && nextFocusedElementSentence) {
response.sentences = [
stepCase + nextFocusedElementSentence.wording,
StepCaseEnum.AND + sentence
];
} else {
response.sentences = [stepCase + sentence];
}
const accessibleRole = getRole(htmlElem);
const accessibleName = computeAccessibleName(htmlElem);
const content = htmlElem.getAttribute("value") ?? htmlElem.firstChild?.textContent?.trim();
if (accessibleRole && accessibleName) {
const jsonEnriched: EnrichedSentenceWrapper = enBasedRoleSentences;
if (checkAction === ActionEnum.EXPECT) {
computedKey = "key.then.element.withRoleAndName";
stepCase = StepCaseEnum.THEN;
} else if (checkAction === ActionEnum.WITHIN || checkAction === ActionEnum.CLICK) {
computedKey = "key.when.withinElement.roleAndName";
stepCase = StepCaseEnum.WHEN;
} else if (checkAction === ActionEnum.KEYBOARD_GLOBAL_NAVIGATION) {
computedKey = "key.then.element.withRoleAndNameFocused";
stepCase = StepCaseEnum.THEN;
}
const sentence = jsonEnriched.enriched.filter((value: EnrichedSentence) => value.key === computedKey).map((enriched: EnrichedSentence) => {
const sentenceAvailable = enriched.wording;
const role = EN_ROLES.filter((role: EnrichedSentenceRole) => role.id === accessibleRole)[0];
return sentenceAvailable
return response;
}

private static getSentenceFromAccessibleRoleAndNameAndContent(
checkAction: string,
accessibleRole: string,
accessibleName: string,
jsonBase: BaseSentence[],
content: string | undefined,
isDisabled: boolean
) {
const response: TranslateSentences = {
suggestion: undefined,
sentences: []
};
let computedKey: string;
let stepCase = StepCaseEnum.THEN;
const jsonEnriched: EnrichedSentenceWrapper = enBasedRoleSentences;
if (checkAction === ActionEnum.EXPECT) {
computedKey = "key.then.element.withRoleAndName";
stepCase = StepCaseEnum.THEN;
} else if (checkAction === ActionEnum.WITHIN || checkAction === ActionEnum.CLICK) {
computedKey = "key.when.withinElement.roleAndName";
stepCase = StepCaseEnum.WHEN;
} else if (checkAction === ActionEnum.KEYBOARD_GLOBAL_NAVIGATION) {
computedKey = "key.then.element.nextWithRoleAndNameFocused";
stepCase = StepCaseEnum.THEN;
}
const sentence = jsonEnriched.enriched.filter((value: EnrichedSentence) => value.key === computedKey).map((enriched: EnrichedSentence) => {
const sentenceAvailable = enriched.wording;
const role = EN_ROLES.filter((role: EnrichedSentenceRole) => role.id === accessibleRole)[0];
return sentenceAvailable
.replaceAll("(n)", "")
.replaceAll("$roleName", role?.name ?? accessibleRole)
.replaceAll("$definiteArticle", role?.getDefiniteArticle())
.replaceAll("$indefiniteArticle", role?.getIndefiniteArticle())
.replaceAll("$namedAdjective", role?.namedAdjective())
.replaceAll("$ofDefiniteArticle", role?.getOfDefiniteArticle())
.replace("{string}", `"${accessibleName}"`);
})[0];
response.sentences = this.getSentenceList(checkAction, jsonBase, accessibleRole, stepCase, accessibleName, sentence);
if (content) {
if (checkAction === ActionEnum.EXPECT) {
if (isDisabled) {
computedKey = "key.then.element.withRoleAndNameAndContentDisabled";
stepCase = StepCaseEnum.THEN;
} else {
computedKey = "key.then.element.withRoleAndNameAndContent";
stepCase = StepCaseEnum.THEN;
}
} else if (checkAction === ActionEnum.WITHIN) {
computedKey = "key.when.withinElement.roleAndName";
stepCase = StepCaseEnum.WHEN;
})[0];
response.sentences = this.getSentenceList(checkAction, jsonBase, accessibleRole, stepCase, accessibleName, sentence);
if (content) {
if (checkAction === ActionEnum.EXPECT) {
if (isDisabled) {
computedKey = "key.then.element.withRoleAndNameAndContentDisabled";
stepCase = StepCaseEnum.THEN;
} else {
computedKey = "key.then.element.withRoleAndNameAndContent";
stepCase = StepCaseEnum.THEN;
}
const sentence = jsonEnriched.enriched.filter((value: EnrichedSentence) => value.key === computedKey).map((enriched: EnrichedSentence) => {
const sentenceAvailable = enriched.wording;
const role = EN_ROLES.filter((role: EnrichedSentenceRole) => role.id === accessibleRole)[0];
return sentenceAvailable
} else if (checkAction === ActionEnum.WITHIN) {
computedKey = "key.when.withinElement.roleAndName";
stepCase = StepCaseEnum.WHEN;
}
const sentence = jsonEnriched.enriched.filter((value: EnrichedSentence) => value.key === computedKey).map((enriched: EnrichedSentence) => {
const sentenceAvailable = enriched.wording;
const role = EN_ROLES.filter((role: EnrichedSentenceRole) => role.id === accessibleRole)[0];
return sentenceAvailable
.replaceAll("(n)", "")
.replaceAll("$roleName", role?.name ?? accessibleRole)
.replaceAll("$definiteArticle", role?.getDefiniteArticle())
Expand All @@ -174,11 +215,8 @@ export class TranslateHelper {
.replaceAll("$ofDefiniteArticle", role?.getOfDefiniteArticle())
.replace("{string}", `"${accessibleName}"`)
.replace("{string}", `"${content}"`);
})[0];
response.sentences = this.getSentenceList(checkAction, jsonBase, accessibleRole, stepCase, accessibleName, sentence);
}
} else {
response.suggestion = new Suggestion();
})[0];
response.sentences = this.getSentenceList(checkAction, jsonBase, accessibleRole, stepCase, accessibleName, sentence);
}
return response;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/assistant/tests/translate-helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ describe("translateEngine - keyboard", () => {
const { selectorWithNth, buttonWithRoleName } = dom();
test("translateEngine - focus - with selector", async () => {
expect(await TranslateHelper.translateEngine(selectorWithNth, ActionEnum.KEYBOARD_GLOBAL_NAVIGATION, false)).toEqual(
{ "sentences": ["Then the element with selector \"div#myDiv > span:nth-of-type(3)\" should be keyboard focused"],
{
"sentences": [
"Then I go to next keyboard element",
"And the element with selector \"div#myDiv > span:nth-of-type(3)\" should be keyboard focused"
],
suggestion: {
accessibleAttribute: "",
accessibleValue: "",
Expand All @@ -154,6 +158,6 @@ describe("translateEngine - keyboard", () => {

test("translateEngine - focus - with role and name", async () => {
expect(await TranslateHelper.translateEngine(buttonWithRoleName, ActionEnum.KEYBOARD_GLOBAL_NAVIGATION, false)).toEqual(
{ "sentences": ["Then I should see a button named \"myButton\" keyboard focused"] });
{ "sentences": ["Then the next keyboard element focused should be a button named \"myButton\""] });
});
});
2 changes: 1 addition & 1 deletion packages/assistant/uuv/e2e/assistant.feature
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Feature: UUV Assistant vital features
When I click on element with role "menuitem" and name "Keyboard actions"
And I click on element with role "menuitem" and name "Keyboard navigation"
Then I should see a title named "Result of Keyboard Navigation"
And I should see a text box named "Generated UUV Script" and containing "Feature: Your amazing feature name Scenario: Keyboard Navigation Given I visit path \"http://127.0.0.1:3200/\" And I start a keyboard navigation from the top of the page Then I should see a text box named \"Last name\" keyboard focused And I go to next keyboard element And I should see a button named \"Submit\" keyboard focused And I go to next keyboard element And I should see a button named \"Reset\" keyboard focused And I go to next keyboard element And I should see a button named \"Submit\" keyboard focused"
# And I should see a radio named "Current"
# And I should see a radio named "Expected"
And I should see a text box named "Generated UUV Script" and containing "Feature: Your amazing feature name Scenario: Keyboard Navigation Given I visit path \"http://127.0.0.1:3200/\" And I start a keyboard navigation from the top of the page Then the next keyboard element focused should be a text box named \"Last name\" And the next keyboard element focused should be a button named \"Submit\" And the next keyboard element focused should be a button named \"Reset\" And the next keyboard element focused should be a button named \"Submit\""

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "primereact/resources/themes/soho-dark/theme.css";
To improve the accessibility of your application, we recommend that you :
- Write your scenarios according to [our recommendations](/docs/recommendations/writing-good-e2e-tests)
- Write keyboard navigation test, here's an example :
<iframe src="https://carbon.now.sh/embed?bg=rgba%280%2C0%2C0%2C1%29&t=seti&wt=sharp&l=gherkin&width=795&ds=true&dsyoff=0px&dsblur=0px&wc=true&wa=true&pv=0px&ph=0px&ln=true&fl=1&fm=Hack&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=Feature%253A%2520Keyboard%2520Navigation%250A%250A%2520%2520Scenario%253A%2520Focus%2520on%2520app%2520link%2520with%2520back%2520nav%250A%2520%2520%2509Given%2520I%2520visit%2520path%2520%2522https%253A%252F%252Fe2e-test-quest.github.io%252Fweather-app%252F%2522%250A%2520%2520%2520%2520When%2520I%2520start%2520a%2520keyboard%2520navigation%2520from%2520the%2520top%2520of%2520the%2520page%250A%2520%2520%2520%2520%2523%2520Check%2520keyboard%2520navigation%2520%250A%2520%2520%2520%2520Then%2520I%2520should%2520see%2520a%2520link%2520named%2520%2522Weather%2520App%27s%2520Logo%2522%2520keyboard%2520focused%250A%2520%2520%2520%2520%2520And%2520I%2520go%2520to%2520next%2520keyboard%2520element%250A%2520%2520%2520%2520%2520And%2520I%2520should%2520see%2520a%2520link%2520named%2520%2522Home%2522%2520keyboard%2520focused%250A%2520%2520%2520%2520%2520And%2520I%2520go%2520to%2520next%2520keyboard%2520element%250A%2520%2520%2520%2520%2520And%2520I%2520should%2520see%2520a%2520button%2520named%2520%2522Get%2520started%2522%2520keyboard%2520focused%250A%2520%2520%2520%2520%2520And%2520I%2520go%2520to%2520previous%2520keyboard%2520element%250A%2520%2520%2520%2520%2520And%2520I%2520should%2520see%2520a%2520link%2520named%2520%2522Home%2522%2520keyboard%2520focused" style={{ margin: '20px 0 0 0', width: '800px', maxWidth: '100%', minHeight: '295px', border: '0', transform: 'scale(1)', overflow:'hidden'}} sandbox="allow-scripts allow-same-origin"></iframe>
<iframe src="https://carbon.now.sh/embed?bg=rgba%280%2C0%2C0%2C1%29&t=seti&wt=sharp&l=gherkin&width=795&ds=true&dsyoff=0px&dsblur=0px&wc=true&wa=true&pv=0px&ph=0px&ln=true&fl=1&fm=Hack&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=Feature%253A%2520Keyboard%2520Navigation%250A%250A%2520%2520Scenario%253A%2520Focus%2520on%2520app%2520link%2520with%2520back%2520nav%250A%2520%2520%2509Given%2520I%2520visit%2520path%2520%2522https%253A%252F%252Fe2e-test-quest.github.io%252Fweather-app%252F%2522%250A%2520%2520%2520%2520When%2520I%2520start%2520a%2520keyboard%2520navigation%2520from%2520the%2520top%2520of%2520the%2520page%250A%2520%2520%2520%2520%2523%2520Check%2520keyboard%2520navigation%2520%250A%2520%2520%2520%2520Then%2520the%2520next%2520keyboard%2520element%2520focused%2520should%2520be%2520a%2520link%2520named%2520%2522Weather%2520App%27s%2520Logo%2522%250A%2520%2520%2520%2520%2520And%2520the%2520next%2520keyboard%2520element%2520focused%2520should%2520be%2520a%2520link%2520named%2520%2522Home%2522%250A%2520%2520%2520%2520%2520And%2520the%2520next%2520keyboard%2520element%2520focused%2520should%2520be%2520a%2520button%2520named%2520%2522Get%2520started%2522%250A%2520%2520%2520%2520%2520And%2520the%2520previous%2520keyboard%2520element%2520focused%2520should%2520be%2520a%2520link%2520named%2520%2522Home%2522" style={{ margin: '20px 0 0 0', width: '800px', maxWidth: '100%', minHeight: '295px', border: '0', transform: 'scale(1)', overflow:'hidden'}} sandbox="allow-scripts allow-same-origin"></iframe>
- Use accessibility check sentences where the following tools are availables :
- [axe-core](/docs/wordings/generated-wording-description/en-generated-wording-description/#i-should-not-have-any-axe-core-accessibility-issue) to perform Axe-Core checks
- [@uuv/a11y](/docs/wordings/generated-wording-description/en-generated-wording-description/#i-should-not-have-any-rgaa-accessibility-issue) : to perform RGAA checks
Expand Down
Loading

0 comments on commit 0f3a3ab

Please sign in to comment.