Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 713d6af

Browse files
authoredApr 11, 2025··
Merge branch 'develop' into feature/CXSPA-9753
2 parents 79775ea + 78f228c commit 713d6af

File tree

12 files changed

+190
-89
lines changed

12 files changed

+190
-89
lines changed
 

Diff for: ‎.github/workflows/pr-title-check.yml

+6-3
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ on:
66

77
jobs:
88
check-pr-title:
9+
name: PR - Check PR title
910
runs-on: ubuntu-latest
1011
steps:
1112
- name: Validate PR title
1213
id: validate
1314
env:
1415
PR_TITLE: ${{ github.event.pull_request.title }}
1516
run: |
16-
echo "Checking PR title: $PR_TITLE"
17+
echo "Checking PR title: ${PR_TITLE}"
1718
18-
if [[ ! "$PR_TITLE" =~ ^(docs|feat|fix|perf|refactor|style|test|chore):\ .+ ]]; then
19-
echo "❌ PR title is invalid."
19+
REGEX='^(docs|feat|fix|perf|refactor|style|test|chore)(\([^)]*\))?:\ .+'
20+
21+
if [[ ! "$PR_TITLE" =~ $REGEX ]]; then
22+
echo "❌ PR title is invalid. Example of a valid title: \`feat: Add user authentication\`, \`chore(deps): update dependency babel-loader to v10\`. Allowed prefixes: docs, feat, fix, perf, refactor, style, test, chore"
2023
exit 1
2124
fi
2225

Diff for: ‎projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/header-and-footer.a11y-e2e.cy.ts

+39-18
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,60 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import { viewportContext } from '../../helpers/viewport-context';
78
import { standardUser } from '../../sample-data/shared-users';
89

9-
describe('Header and Footer Continuum tests', () => {
10+
describe('Header and Footer Continuum tests', { testIsolation: false }, () => {
1011
beforeEach(() => {
1112
cy.a11yContinuumSetup();
12-
cy.visit('/');
13-
cy.get('[section="header"]').as('header');
14-
cy.get('[section="footer"]').as('footer');
13+
cy.clearLocalStorage();
14+
cy.clearCookies();
1515
});
16+
17+
context('Footer', () => {
18+
it('Main footer body', () => {
19+
cy.visit('/');
20+
cy.get('footer a');
21+
cy.get('footer').a11yRunContinuumTest();
22+
});
23+
24+
it('Consent Management dialog', () => {
25+
cy.get('footer').get('button').contains(' Consent Management').click();
26+
cy.get('.modal-dialog').contains(' Select all ').click();
27+
cy.get('.modal-dialog').a11yRunContinuumTest();
28+
cy.get('.close').first().click();
29+
});
30+
});
31+
1632
context('Header', () => {
1733
it('Main header body', () => {
18-
cy.get('@header').get('a').contains('Brands');
19-
cy.get('@header').a11yRunContinuumTest();
34+
cy.get('header').get('nav button[aria-label="Brands"]').click();
35+
cy.get('header').get('a').contains('Canon');
36+
cy.get('header').a11yRunContinuumTest();
2037
});
2138

2239
it('My Account dropdown', () => {
2340
cy.requireLoggedIn(standardUser);
2441
cy.reload();
25-
cy.get('@header')
26-
.get('nav[aria-label="My Account"]')
27-
.a11yRunContinuumTest();
42+
cy.get('header').get('.accNavComponent button').click();
43+
cy.get('nav a').contains(' Order History ');
44+
cy.get('nav[aria-label="My Account"]').a11yRunContinuumTest();
2845
});
29-
});
3046

31-
context('Footer', () => {
32-
it('Main footer body', () => {
33-
cy.get('@footer').a11yRunContinuumTest();
34-
});
47+
viewportContext(['mobile'], () => {
48+
it('Hamburger menu', () => {
49+
cy.get('cx-hamburger-menu button').click();
50+
cy.get('a').contains('Brands');
51+
cy.get('header').a11yRunContinuumTest();
3552

36-
it('Consent Management dialog', () => {
37-
cy.get('@footer').get('button').contains(' Consent Management').click();
38-
cy.get('.modal-dialog').contains(' Select all ').click();
39-
cy.get('.modal-dialog').a11yRunContinuumTest();
53+
cy.get('button[aria-label="Brands"]').click();
54+
cy.get('button').contains('Cameras');
55+
cy.get('nav[aria-label="Category menu"]').a11yRunContinuumTest();
56+
57+
cy.get('button').contains('Cameras').click();
58+
cy.get('a').contains('Canon');
59+
cy.get('nav[aria-label="Category menu"]').a11yRunContinuumTest();
60+
});
4061
});
4162
});
4263
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { viewportContext } from '../../helpers/viewport-context';
8+
9+
describe('Product Search Page', { testIsolation: false }, () => {
10+
beforeEach(() => {
11+
cy.a11yContinuumSetup();
12+
});
13+
14+
it('Searchbar', () => {
15+
cy.visit('/search/camera');
16+
cy.get('cx-product-list-item');
17+
cy.get('cx-searchbox input').type('cam').get('.products a');
18+
cy.get('cx-searchbox').a11yRunContinuumTest();
19+
cy.get('body').click(0, 0);
20+
});
21+
22+
it('Page content with list view', () => {
23+
cy.get('cx-product-list').a11yRunContinuumTest();
24+
});
25+
26+
it('Grid view', () => {
27+
cy.get('button.cx-product-grid').first().click();
28+
cy.get('#product-results-list').a11yRunContinuumTest();
29+
});
30+
31+
it('Sorting dropdown', () => {
32+
cy.get('cx-sorting').first().click();
33+
cy.get('ng-dropdown-panel').a11yRunContinuumTest();
34+
});
35+
36+
it('Facets', () => {
37+
cy.visit('/search/camera?query=camera:relevance:availableInStores:Chiba');
38+
cy.get('cx-active-facets a');
39+
cy.get('cx-product-facet-navigation').a11yRunContinuumTest();
40+
});
41+
42+
viewportContext(['mobile'], () => {
43+
it('Facets Modal', () => {
44+
cy.get('cx-product-facet-navigation .dialog-trigger').click();
45+
cy.get('cx-facet-list .value');
46+
cy.get('.dialog.active').a11yRunContinuumTest();
47+
});
48+
});
49+
});

Diff for: ‎projects/storefrontlib/cms-components/content/tab/tab.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<ng-container *ngIf="mode$ | async as mode">
22
<!-- Tab List is the list of buttons to open tabs -->
33
<div
4-
[attr.role]="mode === TAB_MODE.ACCORDIAN ? 'presentation' : 'tablist'"
4+
[attr.role]="mode === TAB_MODE.ACCORDIAN ? 'null' : 'tablist'"
55
[attr.aria-label]="config.label | cxTranslate"
66
[class]="mode.toLowerCase()"
77
>

Diff for: ‎projects/storefrontlib/cms-components/content/tab/tab.component.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ describe('TabComponent', () => {
352352

353353
it('should display menu buttons for tabs', () => {
354354
const accordianEl = document.querySelector('div[class="accordian"]');
355-
expect(accordianEl?.role).toEqual('presentation');
355+
expect(accordianEl?.role).toEqual('null');
356356
const buttonEls = document.querySelectorAll('button[role="button"]');
357357
expect(buttonEls.length).toEqual(4);
358358

Diff for: ‎projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.html

+27-18
Original file line numberDiff line numberDiff line change
@@ -128,25 +128,34 @@
128128
</button>
129129
</ng-container>
130130
<ng-container *cxFeature="'a11yNavigationButtonsAriaFixes'">
131-
<button
132-
[attr.role]="(isDesktop$ | async) && depth ? 'heading' : 'button'"
133-
[attr.aria-haspopup]="true"
134-
[attr.aria-expanded]="false"
135-
[attr.aria-label]="getAriaLabelAndControl(node)"
136-
[attr.aria-controls]="getAriaLabelAndControl(node)"
137-
(click)="toggleOpen($any($event))"
138-
(mouseenter)="onMouseEnter($event)"
139-
(keydown.space)="onSpace($any($event))"
140-
(keydown.enter)="onSpace($any($event))"
141-
(keydown.esc)="back()"
131+
<h4
132+
*ngIf="(isDesktop$ | async) && depth; else dropdownHeader"
142133
(keydown.arrowDown)="focusOnNode($any($event))"
143-
(focus)="depth || reinitializeMenu()"
134+
(keydown.space)="onSpace($any($event))"
135+
tabindex="0"
144136
>
145-
<ng-container *ngIf="!node.url">
146-
{{ node.title }}
147-
</ng-container>
148-
<cx-icon [type]="iconType.CARET_DOWN"></cx-icon>
149-
</button>
137+
{{ node.title }}
138+
</h4>
139+
<ng-template #dropdownHeader>
140+
<button
141+
[attr.aria-haspopup]="true"
142+
[attr.aria-expanded]="false"
143+
[attr.aria-label]="node.url ? node.title : null"
144+
[attr.aria-controls]="transformIntoValidID(node.title)"
145+
(click)="toggleOpen($any($event))"
146+
(mouseenter)="onMouseEnter($event)"
147+
(keydown.space)="onSpace($any($event))"
148+
(keydown.enter)="onSpace($any($event))"
149+
(keydown.esc)="back()"
150+
(keydown.arrowDown)="focusOnNode($any($event))"
151+
(focus)="depth || reinitializeMenu()"
152+
>
153+
<ng-container *ngIf="!node.url">
154+
{{ node.title }}
155+
</ng-container>
156+
<cx-icon [type]="iconType.CARET_DOWN"></cx-icon>
157+
</button>
158+
</ng-template>
150159
</ng-container>
151160
</ng-container>
152161
</ng-container>
@@ -164,7 +173,7 @@
164173

165174
<!-- we add a wrapper to allow for better layout handling in CSS -->
166175
<div
167-
[id]="getSanitizedTitle(node.title)"
176+
[id]="transformIntoValidID(node.title)"
168177
class="wrapper"
169178
*ngIf="node.children && node.children.length > 0"
170179
>

Diff for: ‎projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.spec.ts

+25-32
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,17 @@ describe('Navigation UI Component', () => {
312312
spyOn(navigationComponent, 'closeIfClickedTheSameLink').and.callThrough();
313313
spyOn(navigationComponent, 'reinitializeMenu').and.callThrough();
314314
spyOn(hamburgerMenuService, 'toggle').and.stub();
315+
navigationComponent.isDesktop$ = of(false);
315316
fixture.detectChanges();
316317

317318
element
318319
.query(By.css('nav > ul > li:nth-child(2) > button'))
319320
.nativeElement.click();
320321
element
321-
.query(By.css('button[aria-controls="child-1"]'))
322+
.query(By.css('button[aria-controls="Child-1"]'))
322323
.nativeElement.click();
323324
element
324-
.query(By.css('button[aria-controls="sub-child-1"]'))
325+
.query(By.css('button[aria-controls="Sub-child-1"]'))
325326
.nativeElement.click();
326327

327328
expect(element.queryAll(By.css('li.is-open:not(.back)')).length).toBe(1);
@@ -363,17 +364,17 @@ describe('Navigation UI Component', () => {
363364
});
364365
});
365366

366-
it('should apply role="heading" to nested dropdown trigger button while on desktop', () => {
367+
it('on desktop, display headings for nested nodes instead of dropdown triggers', () => {
367368
fixture.detectChanges();
368-
const nestedTriggerButton = fixture.debugElement.query(
369-
By.css('button[aria-controls="child-1"]')
369+
const nestedNodeHeading = fixture.debugElement.query(
370+
By.css('#Root-1 h4')
370371
).nativeElement;
371372
const rootTriggerButton = fixture.debugElement.query(
372-
By.css('button[aria-controls="root-1"]')
373+
By.css('button[aria-controls="Root-1"]')
373374
).nativeElement;
374375

375-
expect(nestedTriggerButton.getAttribute('role')).toEqual('heading');
376-
expect(rootTriggerButton.getAttribute('role')).toEqual('button');
376+
expect(nestedNodeHeading.tagName).toEqual('H4');
377+
expect(rootTriggerButton.tagName).toEqual('BUTTON');
377378
});
378379
});
379380

@@ -386,7 +387,7 @@ describe('Navigation UI Component', () => {
386387
const spy = spyOn(navigationComponent, 'toggleOpen');
387388
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
388389
const dropDownButton = element.query(
389-
By.css('button[aria-controls="sub-child-1"]')
390+
By.css('nav button[aria-expanded="false"')
390391
).nativeElement;
391392
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
392393

@@ -400,7 +401,7 @@ describe('Navigation UI Component', () => {
400401
const spy = spyOn(firstChild.nativeElement, 'focus');
401402
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
402403
const dropDownButton = element.query(
403-
By.css('button[aria-controls="sub-child-1"]')
404+
By.css('[depth="2"] h4')
404405
).nativeElement;
405406
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
406407

@@ -421,7 +422,7 @@ describe('Navigation UI Component', () => {
421422
});
422423
const spaceEvent = new KeyboardEvent('keydown', { code: 'Space' });
423424
const dropDownButton = element.query(
424-
By.css('button[aria-controls="sub-child-1"]')
425+
By.css('[depth="2"] h4')
425426
).nativeElement;
426427
Object.defineProperty(spaceEvent, 'target', { value: dropDownButton });
427428
Object.defineProperty(arrowDownEvent, 'target', {
@@ -466,27 +467,19 @@ describe('Navigation UI Component', () => {
466467
}));
467468
});
468469

469-
describe('trigger buttions ariaLabel/title', () => {
470-
it('should have the ariaLabel and title set', () => {
471-
const rootNode = mockNode.children?.[0];
472-
const childNode = rootNode?.children?.[0];
473-
const rootTitle = rootNode?.title;
474-
const childTitle = childNode?.title;
475-
const sanitizedRootTitle =
476-
navigationComponent.getSanitizedTitle(rootTitle);
477-
const sanitizedChildTitle =
478-
navigationComponent.getSanitizedTitle(childTitle);
479-
480-
fixture.detectChanges();
481-
const nestedTriggerButton = fixture.debugElement.query(
482-
By.css(`button[aria-label="${sanitizedRootTitle}"]`)
483-
).nativeElement;
484-
const rootTriggerButton = fixture.debugElement.query(
485-
By.css(`button[aria-label="${sanitizedChildTitle}"]`)
486-
).nativeElement;
487-
488-
expect(nestedTriggerButton).toBeDefined();
489-
expect(rootTriggerButton).toBeDefined();
470+
describe('transformIntoValidID', () => {
471+
it('should replace invalid characters and provide a valid ID', () => {
472+
const invalidIDs = [
473+
{ input: 'Invalid ID', expected: 'Invalid-ID' },
474+
{ input: 'Inv@lid$Char!', expected: 'Inv-lid-Char-' },
475+
{ input: 'ValidId', expected: 'ValidId' },
476+
];
477+
478+
invalidIDs.forEach(({ input, expected }) => {
479+
expect(navigationComponent.transformIntoValidID(input)).toEqual(
480+
expected
481+
);
482+
});
490483
});
491484
});
492485
});
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.