diff --git a/cypress/integration/breadcrumbs.spec.ts b/cypress/integration/breadcrumbs.spec.ts new file mode 100644 index 0000000000..62b9a8ad1d --- /dev/null +++ b/cypress/integration/breadcrumbs.spec.ts @@ -0,0 +1,15 @@ +import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + +describe('Breadcrumbs', () => { + it('should pass accessibility tests', () => { + // Visit an Item, as those have more breadcrumbs + cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); + + // Wait for breadcrumbs to be visible + cy.get('ds-breadcrumbs').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-breadcrumbs'); + }); +}); diff --git a/cypress/integration/browse-by-author.spec.ts b/cypress/integration/browse-by-author.spec.ts new file mode 100644 index 0000000000..07c20ad7c9 --- /dev/null +++ b/cypress/integration/browse-by-author.spec.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Browse By Author', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/author'); + + // Wait for to be visible + cy.get('ds-browse-by-metadata-page').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-browse-by-metadata-page'); + }); +}); diff --git a/cypress/integration/browse-by-dateissued.spec.ts b/cypress/integration/browse-by-dateissued.spec.ts new file mode 100644 index 0000000000..4d22420227 --- /dev/null +++ b/cypress/integration/browse-by-dateissued.spec.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Browse By Date Issued', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/dateissued'); + + // Wait for to be visible + cy.get('ds-browse-by-date-page').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-browse-by-date-page'); + }); +}); diff --git a/cypress/integration/browse-by-subject.spec.ts b/cypress/integration/browse-by-subject.spec.ts new file mode 100644 index 0000000000..89b791f03c --- /dev/null +++ b/cypress/integration/browse-by-subject.spec.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Browse By Subject', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/subject'); + + // Wait for to be visible + cy.get('ds-browse-by-metadata-page').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-browse-by-metadata-page'); + }); +}); diff --git a/cypress/integration/browse-by-title.spec.ts b/cypress/integration/browse-by-title.spec.ts new file mode 100644 index 0000000000..e4e027586a --- /dev/null +++ b/cypress/integration/browse-by-title.spec.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Browse By Title', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/title'); + + // Wait for to be visible + cy.get('ds-browse-by-title-page').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-browse-by-title-page'); + }); +}); diff --git a/cypress/integration/collection-page.spec.ts b/cypress/integration/collection-page.spec.ts new file mode 100644 index 0000000000..a0140d8faf --- /dev/null +++ b/cypress/integration/collection-page.spec.ts @@ -0,0 +1,15 @@ +import { TEST_COLLECTION } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + +describe('Collection Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/collections/' + TEST_COLLECTION); + + // tag must be loaded + cy.get('ds-collection-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-collection-page'); + }); +}); diff --git a/cypress/integration/collection-statistics.spec.ts b/cypress/integration/collection-statistics.spec.ts new file mode 100644 index 0000000000..90b569c824 --- /dev/null +++ b/cypress/integration/collection-statistics.spec.ts @@ -0,0 +1,32 @@ +import { TEST_COLLECTION } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + +describe('Collection Statistics Page', () => { + const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION; + + it('should load if you click on "Statistics" from a Collection page', () => { + cy.visit('/collections/' + TEST_COLLECTION); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-collection-statistics-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-collection-statistics-page'); + }); +}); diff --git a/cypress/integration/community-list.spec.ts b/cypress/integration/community-list.spec.ts new file mode 100644 index 0000000000..a7ba72b74a --- /dev/null +++ b/cypress/integration/community-list.spec.ts @@ -0,0 +1,25 @@ +import { Options } from 'cypress-axe'; +import { testA11y } from 'cypress/support/utils'; + +describe('Community List Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/community-list'); + + // tag must be loaded + cy.get('ds-community-list-page').should('exist'); + + // Open first Community (to show Collections)...that way we scan sub-elements as well + cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click(); + + // Analyze for accessibility issues + // Disable heading-order checks until it is fixed + testA11y('ds-community-list-page', + { + rules: { + 'heading-order': { enabled: false } + } + } as Options + ); + }); +}); diff --git a/cypress/integration/community-page.spec.ts b/cypress/integration/community-page.spec.ts new file mode 100644 index 0000000000..79e21431ad --- /dev/null +++ b/cypress/integration/community-page.spec.ts @@ -0,0 +1,15 @@ +import { TEST_COMMUNITY } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + +describe('Community Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/communities/' + TEST_COMMUNITY); + + // tag must be loaded + cy.get('ds-community-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-community-page',); + }); +}); diff --git a/cypress/integration/community-statistics.spec.ts b/cypress/integration/community-statistics.spec.ts new file mode 100644 index 0000000000..cbf1783c0b --- /dev/null +++ b/cypress/integration/community-statistics.spec.ts @@ -0,0 +1,32 @@ +import { TEST_COMMUNITY } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + +describe('Community Statistics Page', () => { + const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY; + + it('should load if you click on "Statistics" from a Community page', () => { + cy.visit('/communities/' + TEST_COMMUNITY); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-community-statistics-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-community-statistics-page'); + }); +}); diff --git a/cypress/integration/footer.spec.ts b/cypress/integration/footer.spec.ts new file mode 100644 index 0000000000..656e9d4701 --- /dev/null +++ b/cypress/integration/footer.spec.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Footer', () => { + it('should pass accessibility tests', () => { + cy.visit('/'); + + // Footer must first be visible + cy.get('ds-footer').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-footer'); + }); +}); diff --git a/cypress/integration/header.spec.ts b/cypress/integration/header.spec.ts new file mode 100644 index 0000000000..236208db68 --- /dev/null +++ b/cypress/integration/header.spec.ts @@ -0,0 +1,19 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Header', () => { + it('should pass accessibility tests', () => { + cy.visit('/'); + + // Header must first be visible + cy.get('ds-header').should('be.visible'); + + // Analyze for accessibility + testA11y({ + include: ['ds-header'], + exclude: [ + ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174 + ['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149 + ], + }); + }); +}); diff --git a/cypress/integration/homepage-statistics.spec.ts b/cypress/integration/homepage-statistics.spec.ts new file mode 100644 index 0000000000..fe0311f87e --- /dev/null +++ b/cypress/integration/homepage-statistics.spec.ts @@ -0,0 +1,19 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Site Statistics Page', () => { + it('should load if you click on "Statistics" from homepage', () => { + cy.visit('/'); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', '/statistics'); + }); + + it('should pass accessibility tests', () => { + cy.visit('/statistics'); + + // tag must be loaded + cy.get('ds-site-statistics-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-site-statistics-page'); + }); +}); diff --git a/cypress/integration/homepage.spec.ts b/cypress/integration/homepage.spec.ts index e6c28156bb..ddde260bc7 100644 --- a/cypress/integration/homepage.spec.ts +++ b/cypress/integration/homepage.spec.ts @@ -1,3 +1,5 @@ +import { testA11y } from 'cypress/support/utils'; + describe('Homepage', () => { beforeEach(() => { // All tests start with visiting homepage @@ -20,18 +22,11 @@ describe('Homepage', () => { cy.url().should('include', 'query=' + encodeURI(queryString)); }); - // it('should pass accessibility tests', () => { - // // first must inject Axe into current page - // cy.injectAxe(); + it('should pass accessibility tests', () => { + // Wait for homepage tag to appear + cy.get('ds-home-page').should('be.visible'); - // // Analyze entire page for accessibility issues - // // NOTE: this test checks accessibility of header/footer as well - // cy.checkA11y({ - // exclude: [ - // ['#klaro'], // Klaro plugin (privacy policy popup) has color contrast issues - // ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174 - // ['.dropdownLogin'] // "Log in" link in header has color contrast issues - // ], - // }); - // }); + // Analyze for accessibility issues + testA11y('ds-home-page'); + }); }); diff --git a/cypress/integration/item-page.spec.ts b/cypress/integration/item-page.spec.ts index bd91b6506c..6a454b678d 100644 --- a/cypress/integration/item-page.spec.ts +++ b/cypress/integration/item-page.spec.ts @@ -1,15 +1,31 @@ -describe('Item Page', () => { - const ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067'; - const ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067'; +import { Options } from 'cypress-axe'; +import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; - it('should contain element ds-item-page when navigating to an item page', () => { - cy.visit(ENTITYPAGE); - cy.get('ds-item-page').should('exist'); - }); +describe('Item Page', () => { + const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION; + const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION; // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] it('should redirect to the entity page when navigating to an item page', () => { cy.visit(ITEMPAGE); cy.location('pathname').should('eq', ENTITYPAGE); }); + + it('should pass accessibility tests', () => { + cy.visit(ENTITYPAGE); + + // tag must be loaded + cy.get('ds-item-page').should('exist'); + + // Analyze for accessibility issues + // Disable heading-order checks until it is fixed + testA11y('ds-item-page', + { + rules: { + 'heading-order': { enabled: false } + } + } as Options + ); + }); }); diff --git a/cypress/integration/item-statistics.spec.ts b/cypress/integration/item-statistics.spec.ts index f90195c9fa..66ebc228db 100644 --- a/cypress/integration/item-statistics.spec.ts +++ b/cypress/integration/item-statistics.spec.ts @@ -1,6 +1,14 @@ +import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { testA11y } from 'cypress/support/utils'; + describe('Item Statistics Page', () => { - const ITEMUUID = 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; - const ITEMSTATISTICSPAGE = '/statistics/items/' + ITEMUUID; + const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION; + + it('should load if you click on "Statistics" from an Item/Entity page', () => { + cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); + }); it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { cy.visit(ITEMSTATISTICSPAGE); @@ -8,18 +16,23 @@ describe('Item Statistics Page', () => { cy.get('ds-item-page').should('not.exist'); }); - it('should contain the item statistics page url when navigating to an item statistics page', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); - }); - it('should contain a "Total visits" section', () => { cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + ITEMUUID + '_TotalVisits').should('exist'); + cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist'); }); it('should contain a "Total visits per month" section', () => { cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + ITEMUUID + '_TotalVisitsPerMonth').should('exist'); + cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(ITEMSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-item-statistics-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-item-statistics-page'); }); }); diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts index d124f16982..a2bfbe6a5b 100644 --- a/cypress/integration/search-page.spec.ts +++ b/cypress/integration/search-page.spec.ts @@ -1,3 +1,6 @@ +import { Options } from 'cypress-axe'; +import { testA11y } from 'cypress/support/utils'; + describe('Search Page', () => { // unique ID of the search form (for selecting specific elements below) const SEARCHFORM_ID = '#search-form'; @@ -16,4 +19,54 @@ describe('Search Page', () => { cy.get(SEARCHFORM_ID + ' button.search-button').click(); cy.url().should('include', 'query=' + encodeURI(queryString)); }); + + it('should pass accessibility tests', () => { + cy.visit('/search'); + + // tag must be loaded + cy.get('ds-search-page').should('exist'); + + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('.filter-toggle').click({ multiple: true }); + + // Analyze for accessibility issues + testA11y( + { + include: ['ds-search-page'], + exclude: [ + ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 + ], + }, + { + rules: { + // Search filters fail these two "moderate" impact rules + 'heading-order': { enabled: false }, + 'landmark-unique': { enabled: false } + } + } as Options + ); + }); + + it('should pass accessibility tests in Grid view', () => { + cy.visit('/search'); + + // Click to display grid view + // TODO: These buttons should likely have an easier way to uniquely select + cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click(); + + // tag must be loaded + cy.get('ds-search-page').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-search-page', + { + rules: { + // Search filters fail these two "moderate" impact rules + 'heading-order': { enabled: false }, + 'landmark-unique': { enabled: false } + } + } as Options + ); + }); }); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index c106e08011..c6eb874232 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -1,5 +1,16 @@ // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // For more info, visit https://on.cypress.io/plugins-api -/* tslint:disable:no-empty */ -module.exports = (on, config) => { }; -/* tslint:enable:no-empty */ +module.exports = (on, config) => { + // Define "log" and "table" tasks, used for logging accessibility errors during CI + // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file + on('task', { + log(message: string) { + console.log(message); + return null; + }, + table(message: string) { + console.table(message); + return null; + } + }); +}; diff --git a/cypress/support/index.ts b/cypress/support/index.ts index a1456bfcd4..e8b10b9cfb 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -19,3 +19,8 @@ // Import Cypress Axe tools for all tests // https://github.com/component-driven/cypress-axe import 'cypress-axe'; + +// Global constants used in tests +export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200'; +export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4'; +export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; diff --git a/cypress/support/utils.ts b/cypress/support/utils.ts new file mode 100644 index 0000000000..96575969e8 --- /dev/null +++ b/cypress/support/utils.ts @@ -0,0 +1,44 @@ +import { Result } from 'axe-core'; +import { Options } from 'cypress-axe'; + +// Log violations to terminal/commandline in a table format. +// Uses 'log' and 'table' tasks defined in ../plugins/index.ts +// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file +function terminalLog(violations: Result[]) { + cy.task( + 'log', + `${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected` + ); + // pluck specific keys to keep the table readable + const violationData = violations.map( + ({ id, impact, description, helpUrl, nodes }) => ({ + id, + impact, + description, + helpUrl, + nodes: nodes.length, + html: nodes.map(node => node.html) + }) + ); + + // Print violations as an array, since 'node.html' above often breaks table alignment + cy.task('log', violationData); + // Optionally, uncomment to print as a table + // cy.task('table', violationData); + +} + +// Custom "testA11y()" method which checks accessibility using cypress-axe +// while also ensuring any violations are logged to the terminal (see terminalLog above) +// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load +export const testA11y = (context?: any, options?: Options) => { + cy.injectAxe(); + cy.configureAxe({ + rules: [ + // Disable color contrast checks as they are inaccurate / result in a lot of false positives + // See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast + { id: 'color-contrast', enabled: false }, + ] + }); + cy.checkA11y(context, options, terminalLog); +}; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 39f88f8210..58083003cd 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -6,7 +6,8 @@ "compilerOptions": { "types": [ "cypress", - "cypress-axe" + "cypress-axe", + "node" ] } } \ No newline at end of file diff --git a/package.json b/package.json index 66ef13f471..f6d7314243 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "copy-webpack-plugin": "^6.4.1", "css-loader": "3.4.0", "cssnano": "^4.1.10", - "cypress": "8.3.1", + "cypress": "8.6.0", "cypress-axe": "^0.13.0", "deep-freeze": "0.0.1", "dotenv": "^8.2.0", diff --git a/yarn.lock b/yarn.lock index 40658e305f..7f5bd9b2f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4153,10 +4153,10 @@ cypress-axe@^0.13.0: resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.13.0.tgz#3234e1a79a27701f2451fcf2f333eb74204c7966" integrity sha512-fCIy7RiDCm7t30U3C99gGwQrUO307EYE1QqXNaf9ToK4DVqW8y5on+0a/kUHMrHdlls2rENF6TN9ZPpPpwLrnw== -cypress@8.3.1: - version "8.3.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.3.1.tgz#c6760dbb907df2570b0e1ac235fa31c30f9260a6" - integrity sha512-1v6pfx+/5cXhaT5T6QKOvnkawmEHWHLiVzm3MYMoQN1fkX2Ma1C32STd3jBStE9qT5qPSTILjGzypVRxCBi40g== +cypress@8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22" + integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww== dependencies: "@cypress/request" "^2.88.6" "@cypress/xvfb" "^1.2.4" @@ -4192,6 +4192,7 @@ cypress@8.3.1: minimist "^1.2.5" ospath "^1.2.2" pretty-bytes "^5.6.0" + proxy-from-env "1.0.0" ramda "~0.27.1" request-progress "^3.0.0" supports-color "^8.1.1" @@ -9839,6 +9840,11 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"