Skip to content

Commit cee36c5

Browse files
MENDELU/Fixed integration tests (#1294)
* Fix a11y link-name on thumbnail anchors in search-result list elements * fix(metadata-link): cache resolver config permanently to stabilize /full item-page render MetadataLinkService is a singleton (providedIn: root) that fetches 5 backend configuration properties in parallel via combineLatest. The template on the full item page subscribes to it via async pipes inside an @for loop over metadata entries; subscriber count fluctuates as rows render, and with shareReplay({ refCount: true }) the inner subscription was torn down and re-created repeatedly, causing the 5 HTTP calls to repeat and the metadata table to render slowly or inconsistently on Cypress CI runners. - Switch shareReplay to refCount: false so resolver values are cached for the lifetime of the service. - Eagerly subscribe in the constructor so the fetches start as soon as the service is created (during the first FullItemPageComponent instantiation), warming the replay buffer before template subscriptions run. * Revert "fix(metadata-link): cache resolver config permanently to stabilize /full item-page render" This reverts commit 3b852c4. * fix(a11y): satisfy aria-required-children on edit-item tablist and wait for inner content before axe checks - edit-item-page tablist had disabled <button> children without role=tab; when all tabs start with enabled|async = false the <ul role=tablist> momentarily has no role=tab children, triggering aria-required-children. Add role=tab + aria-disabled=true + aria-selected=false + tabindex=-1 to the disabled button so the tablist always has valid children. - For 4 axe checks that failed with 'No elements found for include in page Context' on CI runners (collection delete, community delete, item-edit status, item /full), wait for a stable inner element to render before calling testA11y; the host element passes :visible due to its padding/header even when its main content hasn't streamed in yet. * fix(a11y,metadata-link): wait for host content before axe + cache resolver config permanently Previous push reduced failures from 5 to 3 tests; remaining 3 all fail with 'No elements found for include in page Context': /full (despite 30s wait for .item-page + ds-item-page-title-field that PASSED), item-edit Bitstreams tab, community-edit Assign Roles tab. 1) testA11y(): when include is a CSS string, wait up to 30s for any child to exist on the host before running axe. Eliminates host-without-content races uniformly across every axe-checked spec. 2) MetadataLinkService: shareReplay refCount true -> false, plus eager subscribe in constructor. Service is providedIn root with combineLatest of 5 backend configuration HTTP calls. On /full item page, async pipes inside @for over metadata rows churn subscribers; with refCount true each churn tears down the inner subscription and re-fetches all 5 properties, causing the template to repeatedly re-evaluate and produce empty intermediate render states (visible as blank main content in the CI failure screenshot). Caching for the service lifetime stabilises the render. * fix(a11y): pass resolved Element to axe instead of selector string to avoid 'No elements found' race After previous push, CI improved 3->3 failures but the remaining 3 (community Delete, item-edit Curate, /full) all still fail with 'No elements found for include in page Context'. Root cause: cypress-axe forwards the string selector context to axe.run, which re-resolves it via document.querySelectorAll at axe.run time. Between Cypress commands (injectAxe reads the axe-core source file, configureAxe evals window.axe.configure) several ms elapse; Angular can re-render and remove the host element from the document for one microtask, making the selector resolve to zero elements. Fix: in testA11y, after waiting for the host with rendered descendants, resolve the selector ourselves and pass the live DOM Element reference (not the selector string) to cy.checkA11y. axe-core uses the Element directly and skips selector resolution, eliminating the race. * test(a11y): pass all matched elements to checkA11y, not just first Addresses Copilot review on PR #1294: when selector matches multiple elements, scanning only \[0] reduced coverage. Use \.toArray() so axe sees all matches like the original string-context did.
1 parent 8bd9dfb commit cee36c5

12 files changed

Lines changed: 68 additions & 7 deletions

File tree

cypress/e2e/collection-edit.cy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ describe('Edit Collection > Delete page', () => {
122122
// <ds-delete-collection> tag must be loaded
123123
cy.get('ds-delete-collection').should('be.visible');
124124

125+
// Wait for inner content to render before running axe
126+
cy.get('ds-delete-collection h1#header', { timeout: 30000 }).should('be.visible');
127+
125128
// Analyze for accessibility issues
126129
testA11y('ds-delete-collection');
127130
});

cypress/e2e/community-edit.cy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ describe('Edit Community > Delete page', () => {
8080
// <ds-delete-community> tag must be loaded
8181
cy.get('ds-delete-community').should('be.visible');
8282

83+
// Wait for inner content to render before running axe
84+
cy.get('ds-delete-community h1#header', { timeout: 30000 }).should('be.visible');
85+
8386
// Analyze for accessibility issues
8487
testA11y('ds-delete-community');
8588
});

cypress/e2e/item-edit.cy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ describe('Edit Item > Status tab', () => {
4646
// <ds-item-status> tag must be loaded
4747
cy.get('ds-item-status').should('be.visible');
4848

49+
// Wait for the actual status content to render before running axe
50+
cy.get('ds-item-status .status-label', { timeout: 30000 }).should('be.visible');
51+
4952
// Analyze for accessibility issues
5053
testA11y('ds-item-status');
5154
});

cypress/e2e/item-page.cy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('Item Page', () => {
2626
// <ds-full-item-page> tag must be loaded
2727
cy.get('ds-full-item-page').should('be.visible');
2828

29+
// Wait for the inner content (item-page) to actually render — the host
30+
// element gets its size from padding/header even before the item details
31+
// resolve, so visibility alone isn't enough for axe to find any nodes.
32+
cy.get('ds-full-item-page .item-page', { timeout: 30000 }).should('exist');
33+
cy.get('ds-full-item-page ds-item-page-title-field', { timeout: 30000 }).should('be.visible');
34+
2935
// Analyze <ds-full-item-page> for accessibility issues
3036
testA11y('ds-full-item-page');
3137
});

cypress/support/utils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,29 @@ export const testA11y = (context?: any, options?: Options) => {
4040
{ id: 'color-contrast', enabled: false },
4141
],
4242
});
43+
// When the include context is a CSS selector string, axe-core re-resolves
44+
// that selector at axe.run time via document.querySelectorAll. Between
45+
// Cypress commands (injectAxe + configureAxe take a few ms each) Angular
46+
// may re-render and the host element can briefly disappear from the
47+
// document, causing axe to throw
48+
// "No elements found for include in page Context".
49+
//
50+
// Fix: wait for the host to exist with rendered content, then resolve the
51+
// selector to a live DOM Element here and pass that Element reference (not
52+
// the selector string) to cy.checkA11y. axe-core uses the Element directly
53+
// and does not re-query the document by selector, eliminating the race.
54+
if (typeof context === 'string') {
55+
cy.get(context, { timeout: 30000 }).should('exist');
56+
cy.get(`${context} *`, { timeout: 30000 }).should('exist');
57+
cy.get(context).then(($el) => {
58+
// Pass ALL matched elements (not just the first) so that selectors which
59+
// resolve to multiple nodes preserve the original string-context coverage.
60+
// axe-core accepts an Array of Elements as Context.
61+
const elements = $el.toArray();
62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63+
cy.checkA11y(elements as any, options, terminalLog);
64+
});
65+
return;
66+
}
4367
cy.checkA11y(context, options, terminalLog);
4468
};

src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
@if (linkType !== linkTypes.None) {
55
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
66
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
7-
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
7+
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1"
8+
[attr.aria-label]="dsoNameService.getName(dso)">
89
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
910
</ds-thumbnail>
1011
</a>

src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
@if (linkType !== linkTypes.None) {
55
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
66
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
7-
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
7+
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1"
8+
[attr.aria-label]="dsoNameService.getName(dso)">
89
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
910
</ds-thumbnail>
1011
</a>

src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<div class="col-3 col-md-2">
44
@if (linkType !== linkTypes.None) {
55
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
6-
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1">
6+
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" tabindex="-1"
7+
[attr.aria-label]="dsoNameService.getName(dso)">
78
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
89
</ds-thumbnail>
910
</a>

src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
@if (linkType !== linkTypes.None) {
55
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
66
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
7-
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1">
7+
[routerLink]="[itemPageRoute]" class="dont-break-out" tabindex="-1"
8+
[attr.aria-label]="dsoNameService.getName(dso)">
89
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
910
[defaultImage]="'assets/images/project-placeholder.svg'"
1011
[alt]="'thumbnail.project.alt'"

src/app/item-page/edit-item-page/edit-item-page.component.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ <h1 class="border-bottom">{{'item.edit.head' | translate}}</h1>
2121
<span [ngbTooltip]="'item.edit.tabs.disabled.tooltip' | translate">
2222
@if ((page.enabled | async) !== true) {
2323
<button
24-
class="nav-link disabled">
24+
class="nav-link disabled"
25+
role="tab"
26+
aria-disabled="true"
27+
[attr.aria-selected]="false"
28+
tabindex="-1">
2529
{{'item.edit.tabs.' + page.page + '.head' | translate}}
2630
</button>
2731
}

0 commit comments

Comments
 (0)