@@ -61,6 +65,25 @@ const TestNavigator = ({
aria-labelledby="test-navigator-heading"
className="test-navigator-list"
>
+ {shouldShowFailingAssertionsSummary && (
+
+ )}
{tests.map((test, index) => {
let resultClassName = isReadOnly ? 'missing' : 'not-started';
let resultStatus = isReadOnly ? 'Missing' : 'Not Started';
diff --git a/client/components/TestRun/TestRun.css b/client/components/TestRun/TestRun.css
index 4731f8d00..6356acc57 100644
--- a/client/components/TestRun/TestRun.css
+++ b/client/components/TestRun/TestRun.css
@@ -62,7 +62,7 @@ button.test-navigator-toggle:focus {
list-style: unset;
}
-.test-name-wrapper:before {
+.test-name-wrapper:not(.summary):before {
content: '';
height: 100%;
left: -2.8em;
@@ -290,6 +290,45 @@ button.test-navigator-toggle:focus {
left: 9px;
}
+.test-name-wrapper.summary {
+ border-bottom: 1px solid #dee2e6;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ list-style: none; /* Hide the number for just this item */
+ counter-increment: none;
+}
+
+.test-name-wrapper.summary .progress-indicator {
+ background: #295fa6;
+}
+
+.test-name-wrapper.summary .progress-indicator:before {
+ display: inline-block;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ font-family: 'Font Awesome 5 Free';
+ font-weight: 900;
+ content: '\f03a'; /* List icon */
+ color: white;
+ font-size: 10px;
+ position: relative;
+ top: -3px;
+ left: 4px;
+}
+
+.test-name-wrapper.summary .test-name {
+ font-weight: normal;
+}
+
+.test-name-wrapper.summary .test-name[aria-current='true'] {
+ font-weight: bold;
+}
+
+/* Reset the counter after the summary */
+.test-name-wrapper.summary + li {
+ counter-reset: list-counter;
+}
+
.test-iframe-container {
padding: 0;
}
diff --git a/client/components/TestRun/index.jsx b/client/components/TestRun/index.jsx
index 3112497a1..af1363d04 100644
--- a/client/components/TestRun/index.jsx
+++ b/client/components/TestRun/index.jsx
@@ -123,7 +123,9 @@ const TestRun = () => {
const [testPlanReport, setTestPlanReport] = useState({});
const [testPlanVersion, setTestPlanVersion] = useState();
const [currentTest, setCurrentTest] = useState({});
- const [currentTestIndex, setCurrentTestIndex] = useUrlTestIndex(tests.length);
+ const [currentTestIndex, setCurrentTestIndex] = useUrlTestIndex({
+ maxTestIndex: tests.length
+ });
const [currentTestAtVersionId, setCurrentTestAtVersionId] = useState('');
const [currentTestBrowserVersionId, setCurrentTestBrowserVersionId] =
useState('');
diff --git a/client/components/common/proptypes/index.js b/client/components/common/proptypes/index.js
index 27d0fad2c..fcf197471 100644
--- a/client/components/common/proptypes/index.js
+++ b/client/components/common/proptypes/index.js
@@ -208,6 +208,39 @@ export const TestPlanReportConflictPropType = PropTypes.shape({
).isRequired
});
+export const TestPlanReportMetricsPropType = PropTypes.shape({
+ __typename: PropTypes.string,
+ assertionsPassedCount: PropTypes.number,
+ assertionsFailedCount: PropTypes.number,
+ mustAssertionsPassedCount: PropTypes.number,
+ mustAssertionsCount: PropTypes.number,
+ mustAssertionsFailedCount: PropTypes.number,
+ shouldAssertionsPassedCount: PropTypes.number,
+ shouldAssertionsCount: PropTypes.number,
+ shouldAssertionsFailedCount: PropTypes.number,
+ mayAssertionsPassedCount: PropTypes.number,
+ mayAssertionsCount: PropTypes.number,
+ mayAssertionsFailedCount: PropTypes.number,
+ testsPassedCount: PropTypes.number,
+ testsCount: PropTypes.number,
+ testsFailedCount: PropTypes.number,
+ unexpectedBehaviorCount: PropTypes.number,
+ severeImpactPassedAssertionCount: PropTypes.number,
+ severeImpactFailedAssertionCount: PropTypes.number,
+ moderateImpactPassedAssertionCount: PropTypes.number,
+ moderateImpactFailedAssertionCount: PropTypes.number,
+ commandsCount: PropTypes.number,
+ mustFormatted: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
+ shouldFormatted: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
+ mayFormatted: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
+ unexpectedBehaviorsFormatted: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.string
+ ]),
+ supportLevel: PropTypes.string,
+ supportPercent: PropTypes.number
+});
+
export const TestPlanReportStatusPropType = PropTypes.shape({
__typename: PropTypes.string,
isRequired: PropTypes.bool,
@@ -217,7 +250,7 @@ export const TestPlanReportStatusPropType = PropTypes.shape({
exactAtVersion: AtVersionPropType,
testPlanReport: PropTypes.shape({
id: PropTypes.string.isRequired,
- metrics: PropTypes.object,
+ metrics: TestPlanReportMetricsPropType,
isFinal: PropTypes.bool,
markedFinalAt: PropTypes.string,
issues: PropTypes.arrayOf(IssuePropType),
diff --git a/client/hooks/useFailingAssertions.js b/client/hooks/useFailingAssertions.js
new file mode 100644
index 000000000..447fe6c8e
--- /dev/null
+++ b/client/hooks/useFailingAssertions.js
@@ -0,0 +1,37 @@
+import { useMemo } from 'react';
+
+export const useFailingAssertions = testPlanReport => {
+ return useMemo(() => {
+ if (!testPlanReport?.finalizedTestResults) {
+ return [];
+ }
+
+ return testPlanReport.finalizedTestResults.flatMap(
+ (testResult, testIndex) => {
+ return testResult.scenarioResults.flatMap(scenarioResult => {
+ return (
+ scenarioResult.assertionResults
+ .filter(assertionResult => !assertionResult.passed)
+ // We only want to show MUST and SHOULD assertions
+ .filter(
+ assertionResult =>
+ assertionResult.assertion.priority !== 'MAY' &&
+ assertionResult.assertion.priority !== 'EXCLUDE'
+ )
+ .map(assertionResult => ({
+ testResultId: testResult.id,
+ testIndex,
+ testTitle: testResult.test.title,
+ scenarioCommands: scenarioResult.scenario.commands
+ .map(cmd => cmd.text)
+ .join(', '),
+ assertionText: assertionResult.assertion.text,
+ priority: assertionResult.assertion.priority,
+ output: scenarioResult.output
+ }))
+ );
+ });
+ }
+ );
+ }, [testPlanReport]);
+};
diff --git a/client/hooks/useUrlTestIndex.js b/client/hooks/useUrlTestIndex.js
index 68802665e..4d312ad19 100644
--- a/client/hooks/useUrlTestIndex.js
+++ b/client/hooks/useUrlTestIndex.js
@@ -1,18 +1,22 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
-export const useUrlTestIndex = maxTestIndex => {
+export const useUrlTestIndex = ({ minTestIndex = 0, maxTestIndex }) => {
const location = useLocation();
const navigate = useNavigate();
- const [currentTestIndex, setCurrentTestIndex] = useState(0);
+ const [currentTestIndex, setCurrentTestIndex] = useState(minTestIndex);
const getTestIndex = () => {
+ if (location.hash === '#summary') {
+ return -1;
+ }
+
// Remove the '#' character
const fragment = parseInt(location.hash.slice(1), 10) || 1;
if (!maxTestIndex || maxTestIndex < 1) {
- // If the max test index is less than 1, return 0
- return 0;
+ // If the max test index is less than 1, return the min test index
+ return minTestIndex;
}
// Subtract 1 to make it 0-indexed
// and clamp to the max test index
@@ -24,6 +28,12 @@ export const useUrlTestIndex = maxTestIndex => {
}, [location.hash, maxTestIndex]);
const updateTestIndex = index => {
+ // Special case for summary
+ if (index === -1) {
+ navigate(`${location.pathname}#summary`, { replace: true });
+ return;
+ }
+
// Add 1 to make it 1-indexed
const newIndex = index + 1;
navigate(`${location.pathname}#${newIndex}`, { replace: true });
diff --git a/client/tests/e2e/CandidateReview.e2e.test.js b/client/tests/e2e/CandidateReview.e2e.test.js
index 98c7811c1..3784b555d 100644
--- a/client/tests/e2e/CandidateReview.e2e.test.js
+++ b/client/tests/e2e/CandidateReview.e2e.test.js
@@ -110,4 +110,101 @@ describe('Candidate Review when signed in as vendor', () => {
expect(afterClickTestResultDisclosureDisplay).not.toBe('none');
});
});
+
+ it('shows summary view as first page and navigates correctly', async () => {
+ await getPage(
+ { role: 'vendor', url: '/candidate-review' },
+ async (page, { consoleErrors }) => {
+ await page.waitForSelector('h1 ::-p-text(Candidate Review)');
+
+ // Select first possible link from the table in the Candidate Test Plans
+ // column
+ await page.click('table[aria-label] a');
+
+ await page.waitForSelector('nav#test-navigator-nav ol');
+ await page.waitForSelector('h1 ::-p-text(1.)');
+ await page.waitForSelector('h1[class="current-test-title"]');
+
+ await page.click('a[href="#summary"]');
+
+ // Check navigation buttons in summary view
+ const previousButton = await page.$('button::-p-text(Previous Test)');
+ expect(previousButton).toBeNull(); // No Previous button on summary
+
+ const nextButton = await page.waitForSelector(
+ 'button::-p-text(Begin Review)'
+ );
+ await nextButton.click();
+
+ // Should navigate to first test
+ await page.waitForSelector('h1::-p-text(1.)');
+ // Check summary link aria-current attribute
+ const summaryLinkAriaCurrent = await page.$eval(
+ 'a[href="#summary"]',
+ el => el.getAttribute('aria-current')
+ );
+ expect(summaryLinkAriaCurrent).toBe('false');
+
+ // Previous button should go back to summary from first test
+ const previousFromFirstTest = await page.waitForSelector(
+ 'button::-p-text(Summary)'
+ );
+ await previousFromFirstTest.click();
+
+ // Should be back on summary
+ await page.waitForSelector('a[href="#summary"][aria-current="true"]');
+
+ expect(consoleErrors).toHaveLength(0);
+ }
+ );
+ });
+
+ it('shows failing assertions summary content', async () => {
+ await getPage(
+ { role: 'vendor', url: '/candidate-review' },
+ async (page, { consoleErrors }) => {
+ await page.waitForSelector('h1 ::-p-text(Candidate Review)');
+
+ // Select first possible link from the table in the Candidate Test Plans
+ // column
+ await page.click('table[aria-label] a');
+
+ await page.waitForSelector('nav#test-navigator-nav ol');
+ await page.waitForSelector('h1 ::-p-text(1.)');
+ await page.waitForSelector('h1[class="current-test-title"]');
+
+ // Check for summary specific content
+ await page.click('a[href="#summary"]');
+ await page.waitForSelector(
+ 'h1::-p-text(Summary of Failing Assertions)'
+ );
+
+ // Check for table
+ await page.waitForSelector(
+ 'table[aria-labelledby="failing-assertions-heading"]'
+ );
+
+ // Get the text from the first link in the table
+ const firstLinkText = await page.$eval(
+ 'table[aria-labelledby="failing-assertions-heading"] a',
+ el => el.textContent
+ );
+
+ // Click on the first link in the table
+ await page.click(
+ 'table[aria-labelledby="failing-assertions-heading"] a'
+ );
+
+ // Ensure we are on the correct test
+ await page.waitForSelector('nav#test-navigator-nav ol');
+
+ // Check if the text from the first link in the table is in the h1
+ // So that we know we are on the correct test
+ const h1Text = await text(page, 'h1');
+ expect(h1Text).toContain(firstLinkText);
+
+ expect(consoleErrors).toHaveLength(0);
+ }
+ );
+ });
});
diff --git a/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1#summary.html b/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1#summary.html
new file mode 100644
index 000000000..83cbddfd2
--- /dev/null
+++ b/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1#summary.html
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+
+
+
Candidate Test Run Page | ARIA-AT
+
+
+
+
+
+
+
+
+
+
+
+ Candidate Test Plan Review
+
+ Summary of Failing Assertions (2 must, 1 should)
+
+
+
+
+
+
+ Review status by JAWS Representative: In Progress
+
+
+
+
+ Target Completion Date: January 2, 2023
+
+
+
+
+
+
+
+
+
+
+
+ 2 assertions failed across 26 commands in 2 tests.
+
+
+
+
+
+
+
+
+
+
Test Review Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1.html b/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1.html
index 81b0004b7..a90440370 100644
--- a/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1.html
+++ b/client/tests/e2e/snapshots/saved/_candidate-test-plan_24_1.html
@@ -124,6 +124,13 @@
+
Open a Modal Dialog in reading mode
+ class="css-10jxhio">
Run History
aria-labelledby="test-toolbar-heading"
class="test-run-toolbar mt-1">
-
- Previous Test
+
+ Summary
diff --git a/client/tests/e2e/snapshots/saved/_report_67_targets_20.html b/client/tests/e2e/snapshots/saved/_report_67_targets_20.html
index e131a9c75..7d0493541 100644
--- a/client/tests/e2e/snapshots/saved/_report_67_targets_20.html
+++ b/client/tests/e2e/snapshots/saved/_report_67_targets_20.html
@@ -367,6 +367,9 @@ Results for NVDA 2023.3.3 and Chrome
+