diff --git a/client/components/App/App.jsx b/client/components/App/App.jsx index 756a30de2..275a9281e 100644 --- a/client/components/App/App.jsx +++ b/client/components/App/App.jsx @@ -70,20 +70,17 @@ const App = () => { Test Reports - {isSignedIn && isAdmin && ( -
  • - - Test Management - -
  • - )} +
  • + + Data Management + +
  • {
  • - Candidate Tests + Candidate Review
  • )} diff --git a/client/components/CandidateTests/CandidateModals/ProvideFeedbackModal/ProvideFeedbackModal.css b/client/components/CandidateReview/CandidateModals/ProvideFeedbackModal/ProvideFeedbackModal.css similarity index 100% rename from client/components/CandidateTests/CandidateModals/ProvideFeedbackModal/ProvideFeedbackModal.css rename to client/components/CandidateReview/CandidateModals/ProvideFeedbackModal/ProvideFeedbackModal.css diff --git a/client/components/CandidateTests/CandidateModals/ProvideFeedbackModal/index.jsx b/client/components/CandidateReview/CandidateModals/ProvideFeedbackModal/index.jsx similarity index 100% rename from client/components/CandidateTests/CandidateModals/ProvideFeedbackModal/index.jsx rename to client/components/CandidateReview/CandidateModals/ProvideFeedbackModal/index.jsx diff --git a/client/components/CandidateTests/CandidateModals/ThankYouModal/ThankYouModal.css b/client/components/CandidateReview/CandidateModals/ThankYouModal/ThankYouModal.css similarity index 100% rename from client/components/CandidateTests/CandidateModals/ThankYouModal/ThankYouModal.css rename to client/components/CandidateReview/CandidateModals/ThankYouModal/ThankYouModal.css diff --git a/client/components/CandidateTests/CandidateModals/ThankYouModal/index.jsx b/client/components/CandidateReview/CandidateModals/ThankYouModal/index.jsx similarity index 100% rename from client/components/CandidateTests/CandidateModals/ThankYouModal/index.jsx rename to client/components/CandidateReview/CandidateModals/ThankYouModal/index.jsx diff --git a/client/components/CandidateTests/CandidateModals/common.css b/client/components/CandidateReview/CandidateModals/common.css similarity index 100% rename from client/components/CandidateTests/CandidateModals/common.css rename to client/components/CandidateReview/CandidateModals/common.css diff --git a/client/components/CandidateTests/CandidateTestPlanRun/CandidateTestPlanRun.css b/client/components/CandidateReview/CandidateTestPlanRun/CandidateTestPlanRun.css similarity index 100% rename from client/components/CandidateTests/CandidateTestPlanRun/CandidateTestPlanRun.css rename to client/components/CandidateReview/CandidateTestPlanRun/CandidateTestPlanRun.css diff --git a/client/components/CandidateTests/CandidateTestPlanRun/InstructionsRenderer.jsx b/client/components/CandidateReview/CandidateTestPlanRun/InstructionsRenderer.jsx similarity index 100% rename from client/components/CandidateTests/CandidateTestPlanRun/InstructionsRenderer.jsx rename to client/components/CandidateReview/CandidateTestPlanRun/InstructionsRenderer.jsx diff --git a/client/components/CandidateTests/CandidateTestPlanRun/index.jsx b/client/components/CandidateReview/CandidateTestPlanRun/index.jsx similarity index 99% rename from client/components/CandidateTests/CandidateTestPlanRun/index.jsx rename to client/components/CandidateReview/CandidateTestPlanRun/index.jsx index b661ac933..296ab393e 100644 --- a/client/components/CandidateTests/CandidateTestPlanRun/index.jsx +++ b/client/components/CandidateReview/CandidateTestPlanRun/index.jsx @@ -724,7 +724,7 @@ const CandidateTestPlanRun = () => { show={true} handleAction={async () => { setThankYouModalShowing(false); - navigate('/candidate-tests'); + navigate('/candidate-review'); }} githubUrl={generateGithubUrl( false, diff --git a/client/components/CandidateTests/CandidateTestPlanRun/queries.js b/client/components/CandidateReview/CandidateTestPlanRun/queries.js similarity index 98% rename from client/components/CandidateTests/CandidateTestPlanRun/queries.js rename to client/components/CandidateReview/CandidateTestPlanRun/queries.js index 4e5c5e358..3320723f4 100644 --- a/client/components/CandidateTests/CandidateTestPlanRun/queries.js +++ b/client/components/CandidateReview/CandidateTestPlanRun/queries.js @@ -37,7 +37,7 @@ export const CANDIDATE_REPORTS_QUERY = gql` } testPlanReports( atId: $atId - statuses: [CANDIDATE] + testPlanVersionPhases: [CANDIDATE] testPlanVersionId: $testPlanVersionId testPlanVersionIds: $testPlanVersionIds ) { @@ -65,6 +65,7 @@ export const CANDIDATE_REPORTS_QUERY = gql` testPlanVersion { id title + phase gitSha testPlan { directory @@ -142,7 +143,6 @@ export const CANDIDATE_REPORTS_QUERY = gql` } testPlanReport { id - status } testResults { test { diff --git a/client/components/CandidateTests/FeedbackListItem/FeedbackListItem.css b/client/components/CandidateReview/FeedbackListItem/FeedbackListItem.css similarity index 100% rename from client/components/CandidateTests/FeedbackListItem/FeedbackListItem.css rename to client/components/CandidateReview/FeedbackListItem/FeedbackListItem.css diff --git a/client/components/CandidateTests/FeedbackListItem/index.jsx b/client/components/CandidateReview/FeedbackListItem/index.jsx similarity index 100% rename from client/components/CandidateTests/FeedbackListItem/index.jsx rename to client/components/CandidateReview/FeedbackListItem/index.jsx diff --git a/client/components/CandidateTests/TestPlans/TestPlans.css b/client/components/CandidateReview/TestPlans/TestPlans.css similarity index 100% rename from client/components/CandidateTests/TestPlans/TestPlans.css rename to client/components/CandidateReview/TestPlans/TestPlans.css diff --git a/client/components/CandidateTests/TestPlans/index.jsx b/client/components/CandidateReview/TestPlans/index.jsx similarity index 52% rename from client/components/CandidateTests/TestPlans/index.jsx rename to client/components/CandidateReview/TestPlans/index.jsx index ac2e9ea86..980638704 100644 --- a/client/components/CandidateTests/TestPlans/index.jsx +++ b/client/components/CandidateReview/TestPlans/index.jsx @@ -1,9 +1,8 @@ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { useMutation } from '@apollo/client'; import { Helmet } from 'react-helmet'; import styled from '@emotion/styled'; -import { Container, Button, Dropdown, Table } from 'react-bootstrap'; +import { Container, Table } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -18,19 +17,8 @@ import { getTestPlanTargetTitle, getTestPlanVersionTitle } from '@components/Reports/getTitles'; -import { - LoadingStatus, - useTriggerLoad -} from '@components/common/LoadingStatus'; -import { UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION } from '@components/TestQueue/queries'; -import { UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE_MUTATION } from '@components/CandidateTests/queries'; -import UpdateTargetDateModal from '@components/common/UpdateTargetDateModal'; import ClippedProgressBar from '@components/common/ClippedProgressBar'; -import { - convertDateToString, - convertStringFormatToAnotherFormat -} from '@client/utils/formatter'; -import { useThemedModal, THEMES } from '@client/hooks/useThemedModal'; +import { convertDateToString } from '@client/utils/formatter'; import './TestPlans.css'; const FullHeightContainer = styled(Container)` @@ -151,9 +139,14 @@ const DisclosureContainer = styled.div` const CellSubRow = styled.span` display: flex; flex-direction: row; - gap: 1rem; + gap: 0.5rem; margin-top: 0.5rem; font-size: 0.875rem; + + svg { + align-self: center; + margin: 0; + } `; const CenteredTh = styled.th` @@ -183,40 +176,12 @@ const None = styled.span` } `; -const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { - const { triggerLoad, loadingMessage } = useTriggerLoad(); - const { - themedModal, - showThemedModal, - setShowThemedModal, - setThemedModalContent - } = useThemedModal({ - type: THEMES.WARNING, - title: 'Error Updating Test Plan Status' - }); - - const [updateTestPlanReportStatus] = useMutation( - UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION - ); - const [updateTestPlanVersionRecommendedTargetDate] = useMutation( - UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE_MUTATION - ); - - const changeTargetDateButtonRefs = useRef({}); - const focusButtonRef = useRef(); - +const TestPlans = ({ testPlanVersions }) => { const [atExpandTableItems, setAtExpandTableItems] = useState({ 1: true, 2: true, 3: true }); - const [showUpdateTargetDateModal, setShowUpdateTargetDateModal] = - useState(false); - const [updateTargetDateModalTitle, setUpdateTargetDateModalTitle] = - useState(''); - const [updateTargetDateModalDateText, setUpdateTargetDateModalDateText] = - useState(''); - const [testPlanVersionToUpdate, setTestPlanVersionToUpdate] = useState({}); const none = None; const borderedNone = None; @@ -232,29 +197,6 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { }); }; - const updateReportStatus = async (testPlanReports, status) => { - try { - const updateTestPlanReportPromises = testPlanReports.map( - testPlanReport => { - return updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status - } - }); - } - ); - - await triggerLoad(async () => { - await Promise.all(updateTestPlanReportPromises); - await triggerPageUpdate(); - }, 'Updating Test Plan Status'); - } catch (e) { - setShowThemedModal(true); - setThemedModalContent(<>{e.message}); - } - }; - const testPlanReportsExist = testPlanVersions.some( testPlanVersion => testPlanVersion.testPlanReports.length ); @@ -263,9 +205,9 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { return ( - Candidate Tests | ARIA-AT + Candidate Review | ARIA-AT -

    Candidate Tests

    +

    Candidate Review

    There are no results to show just yet. Please check back soon! @@ -466,305 +408,226 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { ); return ( - - -

    - onClickExpandAtTable(atId)} - > - {atName} - - -

    - +

    + onClickExpandAtTable(atId)} > - - - - - Review Status - Results Summary - - - - {Object.values(testPlanVersions) - .sort((a, b) => - a.title < b.title ? -1 : 1 - ) - .map(testPlanVersion => { - const testPlanReports = - testPlanVersion.testPlanReports; - const candidatePhaseReachedAt = - testPlanVersion.candidatePhaseReachedAt; - const recommendedPhaseTargetDate = - testPlanVersion.recommendedPhaseTargetDate; - - const allMetrics = []; - - let testsCount = 0; - let dataExists = false; - - Object.values(testPlanTargetsById).map( - testPlanTarget => { - const testPlanReport = - testPlanReports.find( - testPlanReport => - testPlanReport.at - .id === - testPlanTarget - .at.id && - testPlanReport.at - .id == atId && - testPlanReport - .browser.id === - testPlanTarget - .browser.id - ); - - if (testPlanReport) { - const metrics = - testPlanReport.metrics; - allMetrics.push(metrics); - - if ( - !dataExists && + {atName} + + + + +
    Candidate Test Plans
    + + + + + Candidate Phase Start Date + + Target Completion Date + Review Status + Results Summary + + + + {Object.values(testPlanVersions) + .sort((a, b) => (a.title < b.title ? -1 : 1)) + .map(testPlanVersion => { + const testPlanReports = + testPlanVersion.testPlanReports; + const candidatePhaseReachedAt = + testPlanVersion.candidatePhaseReachedAt; + const recommendedPhaseTargetDate = + testPlanVersion.recommendedPhaseTargetDate; + + const allMetrics = []; + + let testsCount = 0; + let dataExists = false; + + Object.values(testPlanTargetsById).map( + testPlanTarget => { + const testPlanReport = + testPlanReports.find( + testPlanReport => + testPlanReport.at.id === + testPlanTarget.at + .id && testPlanReport.at.id === - atId - ) { - dataExists = true; - } - - testsCount = - metrics.testsCount > - testsCount - ? metrics.testsCount - : testsCount; + atId && + testPlanReport.browser + .id === + testPlanTarget + .browser.id + ); + + if (testPlanReport) { + const metrics = + testPlanReport.metrics; + allMetrics.push(metrics); + + if ( + !dataExists && + testPlanReport.at.id === + atId + ) { + dataExists = true; } - } - ); - const metrics = { - testsCount, - browsersLength: allMetrics.length, - totalTestsFailedCount: - allMetrics.reduce( - (acc, obj) => - acc + - obj.testsFailedCount, - 0 - ), - totalAssertionsFailedCount: + testsCount = + metrics.testsCount > + testsCount + ? metrics.testsCount + : testsCount; + } + } + ); + + const metrics = { + testsCount, + browsersLength: allMetrics.length, + totalTestsFailedCount: + allMetrics.reduce( + (acc, obj) => + acc + obj.testsFailedCount, + 0 + ), + totalAssertionsFailedCount: + allMetrics.reduce( + (acc, obj) => + acc + + obj.optionalAssertionsFailedCount + + obj.requiredAssertionsFailedCount, + 0 + ), + totalSupportPercent: + Math.round( allMetrics.reduce( (acc, obj) => acc + - obj.optionalAssertionsFailedCount + - obj.requiredAssertionsFailedCount, + obj.supportPercent, 0 - ), - totalSupportPercent: - Math.round( - allMetrics.reduce( - (acc, obj) => - acc + - obj.supportPercent, - 0 - ) / allMetrics.length - ) || 0 - }; - - // Make sure issues are unique - const uniqueLinks = []; - const allIssues = testPlanReports - .map(testPlanReport => [ - ...testPlanReport.issues - ]) - .flat() - .filter(t => - uniqueFilter( - t, - uniqueLinks, - 'link' - ) - ); + ) / allMetrics.length + ) || 0 + }; + + // Make sure issues are unique + const uniqueLinks = []; + const allIssues = testPlanReports + .map(testPlanReport => [ + ...testPlanReport.issues + ]) + .flat() + .filter(t => + uniqueFilter(t, uniqueLinks, 'link') + ); - return ( - dataExists && ( - - + + + + {convertDateToString( + candidatePhaseReachedAt, + 'MMM D, YYYY' + )} + + + + + {convertDateToString( + recommendedPhaseTargetDate, + 'MMM D, YYYY' + )} + + + + {getRowStatus({ + issues: allIssues, + isInProgressStatusExists: + testPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'IN_PROGRESS' + ), + isApprovedStatusExists: + testPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'APPROVED' ) - - - - Candidate Phase - Start Date{' '} - - {convertDateToString( - candidatePhaseReachedAt, - 'MMM D, YYYY' - )} - - - - Target - Completion Date{' '} - - {convertDateToString( - recommendedPhaseTargetDate, - 'MMM D, YYYY' - )} - - - - - - - Mark as ... - - - { - await updateReportStatus( - testPlanReports, - 'DRAFT' - ); - }} - > - Draft - - { - await updateReportStatus( - testPlanReports, - 'RECOMMENDED' - ); - }} - disabled={testPlanReports.some( - t => - t.vendorReviewStatus !== - 'APPROVED' - )} - > - Recommended - - - - - - - - {getRowStatus({ - issues: allIssues, - isInProgressStatusExists: - testPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'IN_PROGRESS' - ), - isApprovedStatusExists: - testPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'APPROVED' - ) - })} - - - - - - - - {evaluateTestsAssertionsMessage( - metrics - )} - - - - - ) - ); - })} - -
    Candidate Test Plans
    - - {getTestPlanVersionTitle( - testPlanVersion - )}{' '} - ({testsCount} Test - {testsCount === 0 || - testsCount > 1 - ? `s` - : ''} + return ( + dataExists && ( +
    + + {getTestPlanVersionTitle( + testPlanVersion + )}{' '} + V + {convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}{' '} + ({testsCount} Test + {testsCount === 0 || + testsCount > 1 + ? `s` + : ''} + ) + +
    - - - + })} + + + + + + + + {evaluateTestsAssertionsMessage( + metrics + )} + + + + + ) + ); + })} + + + + ); }; @@ -892,6 +755,11 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { {getTestPlanVersionTitle( testPlanVersion + )}{' '} + V + {convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' )} @@ -960,38 +828,12 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { ); }; - const onUpdateTargetDateAction = async ({ updatedDateText }) => { - onUpdateTargetDateModalClose(); - try { - await triggerLoad(async () => { - await updateTestPlanVersionRecommendedTargetDate({ - variables: { - testPlanVersionId: testPlanVersionToUpdate.id, - recommendedPhaseTargetDate: - convertStringFormatToAnotherFormat(updatedDateText) - } - }); - await triggerPageUpdate(); - if (focusButtonRef.current) focusButtonRef.current.focus(); - }, 'Updating Test Plan Recommended Target Date'); - } catch (e) { - setShowThemedModal(true); - setThemedModalContent(<>{e.message}); - } - }; - - const onUpdateTargetDateModalClose = () => { - setUpdateTargetDateModalDateText(''); - setShowUpdateTargetDateModal(false); - if (focusButtonRef.current) focusButtonRef.current.focus(); - }; - return ( - Candidate Tests | ARIA-AT + Candidate Review | ARIA-AT -

    Candidate Tests

    +

    Candidate Review

    Introduction

    This page summarizes the test results for each AT and Browser @@ -1001,16 +843,6 @@ const TestPlans = ({ testPlanVersions, triggerPageUpdate = () => {} }) => { {constructTableForAtById('2', 'NVDA')} {constructTableForAtById('3', 'VoiceOver for macOS')} {constructTableForResultsSummary()} - {showThemedModal && themedModal} - {showUpdateTargetDateModal && ( - - )} ); }; diff --git a/client/components/CandidateReview/index.jsx b/client/components/CandidateReview/index.jsx new file mode 100644 index 000000000..0a8373c25 --- /dev/null +++ b/client/components/CandidateReview/index.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useQuery } from '@apollo/client'; +import PageStatus from '../common/PageStatus'; +import TestPlans from './TestPlans'; +import { CANDIDATE_REVIEW_PAGE_QUERY } from './queries'; + +const CandidateReview = () => { + const { loading, data, error } = useQuery(CANDIDATE_REVIEW_PAGE_QUERY, { + fetchPolicy: 'cache-and-network' + }); + + if (error) { + return ( + + ); + } + + if (loading) { + return ( + + ); + } + + if (!data) return null; + + const testPlanVersions = data.testPlanVersions; + + return ; +}; + +export default CandidateReview; diff --git a/client/components/CandidateTests/queries.js b/client/components/CandidateReview/queries.js similarity index 60% rename from client/components/CandidateTests/queries.js rename to client/components/CandidateReview/queries.js index b355b9a82..cb167bcc9 100644 --- a/client/components/CandidateTests/queries.js +++ b/client/components/CandidateReview/queries.js @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -export const CANDIDATE_TESTS_PAGE_QUERY = gql` +export const CANDIDATE_REVIEW_PAGE_QUERY = gql` query { testPlanVersions(phases: [CANDIDATE]) { id @@ -14,9 +14,8 @@ export const CANDIDATE_TESTS_PAGE_QUERY = gql` updatedAt candidatePhaseReachedAt recommendedPhaseTargetDate - testPlanReports(isCurrentPhase: true) { + testPlanReports(isFinal: true) { id - status metrics at { id @@ -51,24 +50,3 @@ export const CANDIDATE_TESTS_PAGE_QUERY = gql` } } `; - -export const UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE_MUTATION = gql` - mutation UpdateTestPlanReportRecommendedTargetDate( - $testPlanVersionId: ID! - $recommendedPhaseTargetDate: Timestamp! - ) { - testPlanVersion(id: $testPlanVersionId) { - updateRecommendedPhaseTargetDate( - recommendedPhaseTargetDate: $recommendedPhaseTargetDate - ) { - testPlanVersion { - phase - testPlanReports { - id - status - } - } - } - } - } -`; diff --git a/client/components/CandidateTests/index.jsx b/client/components/CandidateTests/index.jsx deleted file mode 100644 index 3454c9231..000000000 --- a/client/components/CandidateTests/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { useQuery } from '@apollo/client'; -import PageStatus from '../common/PageStatus'; -import TestPlans from './TestPlans'; -import { CANDIDATE_TESTS_PAGE_QUERY } from './queries'; - -const CandidateTests = () => { - const { loading, data, error, refetch } = useQuery( - CANDIDATE_TESTS_PAGE_QUERY, - { - fetchPolicy: 'cache-and-network' - } - ); - - if (error) { - return ( - - ); - } - - if (loading) { - return ( - - ); - } - - if (!data) return null; - - const testPlanVersions = data.testPlanVersions; - - return ( - - ); -}; - -export default CandidateTests; diff --git a/client/components/DataManagement/DataManagement.css b/client/components/DataManagement/DataManagement.css new file mode 100644 index 000000000..ae3752eaf --- /dev/null +++ b/client/components/DataManagement/DataManagement.css @@ -0,0 +1,45 @@ +.data-management.table { + padding: 0; + margin: 0; +} + +.data-management.table thead tr th { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.data-management.table tbody tr th { + padding: 0.75rem; +} + +/* Test Plan column & Covered ATs column */ +.data-management.table thead tr th:nth-of-type(-n + 2), +.data-management.table tbody tr th:nth-of-type(1), +.data-management.table tbody tr td:nth-of-type(1) { + max-width: 200px; +} + +/* RD, DRAFT, CANDIDATE & RECOMMENDED columns */ +.data-management.table thead tr th:nth-last-of-type(-n + 4), +.data-management.table tbody tr td:nth-last-of-type(-n + 4) { + max-width: 150px; +} + +.data-management.table tbody tr td { + position: relative; +} + +.data-management.table tbody tr:nth-of-type(even), +.table-striped tbody tr:nth-of-type(even) { + background-color: #fff; +} + +.change-phase { + margin-left: 12px; + margin-right: 12px; +} + +.change-phase .btn { + display: flex; + align-items: center; +} diff --git a/client/components/DataManagement/DataManagementRow/index.jsx b/client/components/DataManagement/DataManagementRow/index.jsx new file mode 100644 index 000000000..b260a500a --- /dev/null +++ b/client/components/DataManagement/DataManagementRow/index.jsx @@ -0,0 +1,1259 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useMutation } from '@apollo/client'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Button } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { + UPDATE_TEST_PLAN_VERSION_PHASE, + UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE +} from '../queries'; +import { LoadingStatus, useTriggerLoad } from '../../common/LoadingStatus'; +import { + checkTimeBetweenDates, + convertDateToString, + convertStringFormatToAnotherFormat +} from '../../../utils/formatter'; +import { derivePhaseName } from '@client/utils/aria'; +import { THEMES, useThemedModal } from '@client/hooks/useThemedModal'; +import BasicModal from '@components/common/BasicModal'; +import UpdateTargetDateModal from '@components/common/UpdateTargetDateModal'; + +const StatusCell = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + + span:nth-of-type(2) { + margin-top: 1rem; + font-size: 14px; + text-align: center; + + margin-bottom: 88px; + } + + span:nth-of-type(3) { + display: flex; + justify-content: center; + padding: 12px; + font-size: 14px; + + position: absolute; + bottom: 0; + left: 0; + right: 0; + + color: #6a7989; + background: #f6f8fa; + + > span.pill { + display: flex; + width: fit-content; + height: 20px; + + justify-content: center; + align-items: center; + align-self: center; + + margin-right: 6px; + min-width: 40px; + border-radius: 14px; + + background: #6a7989; + color: white; + } + } +`; + +const PhaseCell = styled.div` + > span.version-string { + display: flex; + justify-content: center; + align-items: center; + + //padding: 4px 8px; + height: 2rem; + border-radius: 4px; + + width: 100%; + background: #f6f8fa; + } + + > span.review-complete { + display: block; + font-size: 14px; + text-align: center; + margin-top: 12px; + + color: #333f4d; + } + + > span.more { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + padding: 4px; + font-size: 14px; + + position: absolute; + left: 0; + bottom: 0; + right: 0; + + color: #6a7989; + background: #f6f8fa; + + > span.more-issues-container { + display: flex; + flex-direction: row; + + align-items: center; + } + + > span.target-days-container { + text-align: center; + + button { + appearance: none; + border: none; + background: none; + color: inherit; + font-weight: bold; + + margin: 0; + padding: 0; + } + } + } + + > button { + margin-top: 12px; + } +`; + +const PhaseText = styled.span` + display: inline-block; + width: 100%; + padding: 2px 4px; + border-radius: 14px; + + text-align: center; + overflow: hidden; + white-space: nowrap; + color: white; + + &.rd { + background: #4177de; + } + + &.draft { + background: #818f98; + } + + &.candidate { + background: #ff6c00; + } + + &.recommended { + background: #8441de; + } +`; + +const ReportStatusDot = styled.span` + display: inline-block; + height: 10px; + width: 10px; + padding: 0; + margin-right: 8px; + border-radius: 50%; + + &.issues { + background: #f2ba00; + } + + &.reports-not-started { + background: #7c7c7c; + } + + &.reports-in-progress { + background: #3876e8; + } + + &.reports-complete { + background: #2ba51c; + } + + &.reports-missing { + background: #ce1b4c; + } +`; + +const NoneText = styled.span` + display: flex; + justify-content: center; + align-items: center; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + font-style: italic; + color: #6a7989; +`; + +const DataManagementRow = ({ + isAdmin, + ats, + testPlan, + testPlanVersions, + setTestPlanVersions +}) => { + const { triggerLoad, loadingMessage } = useTriggerLoad(); + const { + themedModal, + showThemedModal, + setShowThemedModal, + setThemedModalTitle, + setThemedModalContent, + setFocusRef + } = useThemedModal({ + type: THEMES.WARNING, + title: 'Error Updating Test Plan Status' + }); + const [updateTestPlanVersionPhaseMutation] = useMutation( + UPDATE_TEST_PLAN_VERSION_PHASE + ); + const [updateTestPlanVersionRecommendedTargetDate] = useMutation( + UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE + ); + + // State + const [activePhases, setActivePhases] = useState({}); + const [rdTestPlanVersions, setRdTestPlanVersions] = useState([]); + const [draftTestPlanVersions, setDraftTestPlanVersions] = useState([]); + const [candidateTestPlanVersions, setCandidateTestPlanVersions] = useState( + [] + ); + const [recommendedTestPlanVersions, setRecommendedTestPlanVersions] = + useState([]); + + const [showAdvanceModal, setShowAdvanceModal] = useState(false); + const [advanceModalData, setAdvanceModalData] = useState({}); + + const [showUpdateTargetModal, setShowUpdateTargetModal] = useState(false); + const [updateTargetModalData, setUpdateTargetModalData] = useState({}); + + const draftVersionStringRef = useRef(); + const candidateVersionStringRef = useRef(); + const recommendedVersionStringRef = useRef(); + const updateTargetRef = useRef(); + + useEffect(() => { + // TestPlanVersions separated by current TestPlan's phase + setActivePhases({}); + setRdTestPlanVersions( + testPlanVersions.filter(({ phase }) => phase === 'RD') + ); + setDraftTestPlanVersions( + testPlanVersions.filter(({ phase }) => phase === 'DRAFT') + ); + setCandidateTestPlanVersions( + testPlanVersions.filter(({ phase }) => phase === 'CANDIDATE') + ); + setRecommendedTestPlanVersions( + testPlanVersions.filter(({ phase }) => phase === 'RECOMMENDED') + ); + }, [testPlanVersions]); + + // Get the version information based on the latest or earliest date info from a group of + // TestPlanVersions + const getVersionData = (testPlanVersions, dateKey = 'updatedAt') => { + const earliestVersion = testPlanVersions.reduce((a, b) => + new Date(a[dateKey]) < new Date(b[dateKey]) ? a : b + ); + const earliestVersionDate = new Date(earliestVersion[dateKey]); + + const latestVersion = testPlanVersions.reduce((a, b) => + new Date(a[dateKey]) > new Date(b[dateKey]) ? a : b + ); + const latestVersionDate = new Date(latestVersion[dateKey]); + + return { + earliestVersion, + earliestVersionDate, + latestVersion, + latestVersionDate + }; + }; + + const getUniqueAtObjects = testPlanReports => { + const uniqueAtObjects = {}; + testPlanReports.forEach(testPlanReport => { + const atId = testPlanReport.at.id; + if (!uniqueAtObjects[atId]) { + uniqueAtObjects[atId] = testPlanReport.at; + } + }); + return uniqueAtObjects; + }; + + const handleClickUpdateTestPlanVersionPhase = async ( + testPlanVersionId, + phase, + testPlanVersionDataToInclude + ) => { + try { + await triggerLoad(async () => { + const result = await updateTestPlanVersionPhaseMutation({ + variables: { + testPlanVersionId, + phase, + testPlanVersionDataToIncludeId: + testPlanVersionDataToInclude?.id + } + }); + + const updatedTestPlanVersion = + result.data.testPlanVersion.updatePhase.testPlanVersion; + setTestPlanVersions(prevTestPlanVersions => { + let testPlanVersions = [...prevTestPlanVersions]; + + const index = testPlanVersions.findIndex( + testPlanVersion => + testPlanVersion.id === updatedTestPlanVersion.id + ); + if (index !== -1) + testPlanVersions[index] = updatedTestPlanVersion; + + return testPlanVersions; + }); + + setTimeout(() => { + if (phase === 'DRAFT' && draftVersionStringRef.current) + draftVersionStringRef.current.focus(); + + if ( + phase === 'CANDIDATE' && + candidateVersionStringRef.current + ) + candidateVersionStringRef.current.focus(); + + if ( + phase === 'RECOMMENDED' && + recommendedVersionStringRef.current + ) + recommendedVersionStringRef.current.focus(); + }, 250); + }, 'Updating Test Plan Version Phase'); + } catch (e) { + console.error(e.message); + setShowThemedModal(true); + setThemedModalTitle('Error Updating Test Plan Version Phase'); + setThemedModalContent(<>{e.message}); + } + }; + + const handleClickUpdateTestPlanVersionRecommendedPhaseTargetDate = async ({ + updatedDateText + }) => { + setShowUpdateTargetModal(false); + try { + await triggerLoad(async () => { + const result = await updateTestPlanVersionRecommendedTargetDate( + { + variables: { + testPlanVersionId: + updateTargetModalData.testPlanVersionId, + recommendedPhaseTargetDate: + convertStringFormatToAnotherFormat( + updatedDateText + ) + } + } + ); + const updatedTestPlanVersion = + result.data.testPlanVersion.updateRecommendedPhaseTargetDate + .testPlanVersion; + setTestPlanVersions(prevTestPlanVersions => { + let testPlanVersions = [...prevTestPlanVersions]; + + const index = testPlanVersions.findIndex( + testPlanVersion => + testPlanVersion.id === updatedTestPlanVersion.id + ); + if (index !== -1) + testPlanVersions[index] = updatedTestPlanVersion; + + return testPlanVersions; + }); + + setTimeout(() => { + if (updateTargetRef.current) + updateTargetRef.current.focus(); + }, 250); + }, 'Updating Test Plan Version Recommended Phase Target Date'); + } catch (e) { + console.error(e.message); + setShowThemedModal(true); + setThemedModalTitle( + 'Error Updating Test Plan Version Recommended Phase Target Date' + ); + setThemedModalContent(<>{e.message}); + } + }; + + const renderCellForCoveredAts = () => { + const atNames = ats.map(({ name }) => name); + + if (atNames.length > 1) { + return ( + <> + {atNames.map((item, index) => ( + + {item} + {index !== atNames.length - 1 ? ( + index === atNames.length - 2 ? ( + and + ) : ( + , + ) + ) : null} + + ))} + + ); + } else if (atNames.length === 1) return {atNames[0]}; + else return N/A; + }; + + const renderCellForOverallStatus = () => { + const phaseView = (phase, versionDate) => { + let phaseText = ''; + + switch (phase) { + case 'RD': + phaseText = 'Complete'; + break; + case 'DRAFT': + case 'CANDIDATE': + phaseText = 'Review Started'; + break; + case 'RECOMMENDED': + phaseText = 'Since'; + break; + } + + return ( + <> + + {derivePhaseName(phase)} + + + {phaseText}{' '} + {convertDateToString(versionDate, 'MMM D, YYYY')} + + + ); + }; + + const versionsInProgressView = versionsCount => { + return versionsCount ? ( + + <> + +{versionsCount} New + Version + {versionsCount === 1 ? '' : 's'} in Progress + + + ) : null; + }; + + const otherVersionsInProgressCount = ( + currentPhase, // To exclude in check + excludedPhases = [] + ) => { + const otherVersionsInProgress = Object.keys(activePhases).filter( + e => ![currentPhase, ...excludedPhases].includes(e) + ); + return otherVersionsInProgress.length; + }; + + if (recommendedTestPlanVersions.length) { + const { earliestVersion, earliestVersionDate } = getVersionData( + recommendedTestPlanVersions, + 'recommendedPhaseReachedAt' + ); + const { phase } = earliestVersion; + const versionsInProgressCount = otherVersionsInProgressCount(phase); + + return ( + + {phaseView(phase, earliestVersionDate)} + {versionsInProgressView(versionsInProgressCount)} + + ); + } + + if (candidateTestPlanVersions.length) { + const { earliestVersion, earliestVersionDate } = getVersionData( + candidateTestPlanVersions, + 'candidatePhaseReachedAt' + ); + const { phase } = earliestVersion; + + const versionsInProgressCount = otherVersionsInProgressCount( + phase, + ['RECOMMENDED'] + ); + + return ( + + {phaseView(phase, earliestVersionDate)} + {versionsInProgressView(versionsInProgressCount)} + + ); + } + + if (draftTestPlanVersions.length) { + const { earliestVersion, earliestVersionDate } = getVersionData( + draftTestPlanVersions, + 'draftPhaseReachedAt' + ); + const { phase } = earliestVersion; + + const versionsInProgressCount = otherVersionsInProgressCount( + phase, + ['RECOMMENDED', 'CANDIDATE'] + ); + + return ( + + {phaseView(phase, earliestVersionDate)} + {versionsInProgressView(versionsInProgressCount)} + + ); + } + + if (rdTestPlanVersions.length) { + const { latestVersion, latestVersionDate } = + getVersionData(rdTestPlanVersions); + const { phase } = latestVersion; + return ( + {phaseView(phase, latestVersionDate)} + ); + } + + // Should never be called but just in case + return null; + }; + + const renderCellForPhase = (phase, testPlanVersions = []) => { + const defaultView = N/A; + + const insertActivePhaseForTestPlan = testPlanVersion => { + if (!activePhases[phase]) { + const result = { + ...activePhases, + [phase]: testPlanVersion + }; + setActivePhases(result); + } + }; + + switch (phase) { + case 'RD': { + // If the latest version of the plan is in the draft, candidate, or recommended + // phase, show string "N/A". This should also apply if there is no R&D phase + // TestPlanVersions + if (!testPlanVersions.length) return defaultView; + + const { latestVersion, latestVersionDate } = + getVersionData(testPlanVersions); + + const otherTestPlanVersions = [ + ...draftTestPlanVersions, + ...candidateTestPlanVersions, + ...recommendedTestPlanVersions + ]; + + if (otherTestPlanVersions.length) { + const { latestVersionDate: otherLatestVersionDate } = + getVersionData(otherTestPlanVersions); + if (otherLatestVersionDate > latestVersionDate) { + return defaultView; + } + } + + // If there is an earlier version that is draft and that version has some test plan + // runs in the test queue, this button will run the process for updating existing + // reports and preserving data for tests that have not changed. + let testPlanVersionDataToInclude; + if (draftTestPlanVersions.length) { + const { + latestVersion: draftLatestVersion, + latestVersionDate: draftLatestVersionDate + } = getVersionData(draftTestPlanVersions); + + if (draftLatestVersionDate < latestVersionDate) + testPlanVersionDataToInclude = draftLatestVersion; + } + + // Otherwise, show VERSION_STRING link with a draft transition button. Phase is + // "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + + + + V + {convertDateToString( + latestVersionDate, + 'YY.MM.DD' + )} + + + + {isAdmin && ( + + )} + + ); + } + case 'DRAFT': { + let latestVersion, latestVersionDate; + + let otherTestPlanVersions = [ + ...candidateTestPlanVersions, + ...recommendedTestPlanVersions + ]; + + if (testPlanVersions.length) { + const { + latestVersion: _latestVersion, + latestVersionDate: _latestVersionDate + } = getVersionData(testPlanVersions); + + latestVersion = _latestVersion; + latestVersionDate = _latestVersionDate; + + if (otherTestPlanVersions.length) + otherTestPlanVersions = otherTestPlanVersions.filter( + other => + new Date(other.updatedAt) > latestVersionDate + ); + } + + // If a version of the plan is not in the draft phase and there are no versions in + // later phases, show string "Not Started" + if (![...testPlanVersions, ...otherTestPlanVersions].length) + return Not Started; + + // If a version of the plan is not in the draft phase and there is a version in at + // least one of candidate or recommended phases, show string "Review of + // VERSION_STRING completed DATE" + if (otherTestPlanVersions.length) { + const { + latestVersion: otherLatestVersion, + latestVersionDate: otherLatestVersionDate + } = getVersionData(otherTestPlanVersions); + + const completionDate = + otherLatestVersion.candidatePhaseReachedAt; + + return ( + + + + + V + {convertDateToString( + otherLatestVersionDate, + 'YY.MM.DD' + )} + + + + Review Completed{' '} + + {convertDateToString( + completionDate, + 'MMM D, YYYY' + )} + + + + ); + } + + // Link with text "VERSION_STRING" that targets the single-page view of the plan. + // If required reports are complete and user is an admin, show "Advance to + // Candidate" button. + if (testPlanVersions.length) { + // If there is an earlier version that is candidate and that version has some + // test plan runs in the test queue, this button will run the process for + // updating existing reports and preserving data for tests that have not + // changed. + let testPlanVersionDataToInclude; + if (candidateTestPlanVersions.length) { + const { + latestVersion: candidateLatestVersion, + latestVersionDate: candidateLatestVersionDate + } = getVersionData(candidateTestPlanVersions); + + if (candidateLatestVersionDate < latestVersionDate) + testPlanVersionDataToInclude = + candidateLatestVersion; + } + + let coveredReports = []; + let finalReportFound = false; + + latestVersion.testPlanReports.forEach(testPlanReport => { + const markedFinalAt = testPlanReport.markedFinalAt; + const atName = testPlanReport.at.name; + const browserName = testPlanReport.browser.name; + + const value = `${atName}_${browserName}`; + + if (markedFinalAt && !coveredReports.includes(value)) { + finalReportFound = true; + coveredReports.push(value); + } + }); + + // Phase is "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + + + + V + {convertDateToString( + latestVersionDate, + 'YY.MM.DD' + )} + + + + {isAdmin && ( + + )} + + ); + } + return defaultView; + } + case 'CANDIDATE': { + let latestVersion, latestVersionDate; + + let otherTestPlanVersions = [...recommendedTestPlanVersions]; + + if (testPlanVersions.length) { + const { + latestVersion: _latestVersion, + latestVersionDate: _latestVersionDate + } = getVersionData(testPlanVersions); + + latestVersion = _latestVersion; + latestVersionDate = _latestVersionDate; + + if (otherTestPlanVersions.length) + otherTestPlanVersions = otherTestPlanVersions.filter( + other => + new Date(other.updatedAt) > latestVersionDate + ); + } + + // If a version of the plan is not in the candidate phase and there has not yet been + // a recommended version, show string "Not Started" + if (![...testPlanVersions, ...otherTestPlanVersions].length) + return Not Started; + + // If a version of the plan is not in the candidate phase and there is a recommended + // version, show string "Review of VERSION_STRING completed DATE" + if (otherTestPlanVersions.length) { + const { + latestVersion: otherLatestVersion, + latestVersionDate: otherLatestVersionDate + } = getVersionData(otherTestPlanVersions); + + const completionDate = + otherLatestVersion.recommendedPhaseReachedAt; + + return ( + + + + + V + {convertDateToString( + otherLatestVersionDate, + 'YY.MM.DD' + )} + + + + Review Completed{' '} + + {convertDateToString( + completionDate, + 'MMM D, YYYY' + )} + + + + ); + } + + // Link with text "VERSION_STRING" that targets the single-page view of the plan. + // + // Show string "N Open Review Issues" and if N>=2, append " from N AT" Examples: "3 + // Open Review Issues from 2 AT" or "0 Open Review Issues" + // + // Show button "Advance to Recommended" when the following conditions are met: + // - If there has not yet been a recommended version and open issues = 0 and days + // in review > 120 and user is admin, show the button. + // - If there is already a recommended version and open review issues = 0 and user + // is admin, show the button. + // - If there is an earlier version that is recommended and that version has some + // test plan runs in the test queue, this button will run the process for + // updating existing reports and preserving data for tests that have not changed. + // - if there is an earlier version in the recommended phase, this button will + // sunset that version. This will also sunset any reports completed using that + // version. + if (testPlanVersions.length) { + const filteredTestPlanReports = + latestVersion.testPlanReports; + const uniqueAtObjects = getUniqueAtObjects( + filteredTestPlanReports + ); + const uniqueAtsCount = Object.keys(uniqueAtObjects).length; + + const issuesCount = filteredTestPlanReports.reduce( + (acc, obj) => acc + obj.issues.length, + 0 + ); + + // If there is an earlier version that is recommended and that version has some + // test plan runs in the test queue, this button will run the process for + // updating existing reports and preserving data for tests that have not + // changed. + let testPlanVersionDataToInclude; + if (recommendedTestPlanVersions.length) { + const { + latestVersion: recommendedLatestVersion, + latestVersionDate: recommendedLatestVersionDate + } = getVersionData(recommendedTestPlanVersions); + + if (recommendedLatestVersionDate < latestVersionDate) + testPlanVersionDataToInclude = + recommendedLatestVersion; + } + + const currentDate = new Date(); + const recommendedPhaseTargetDate = new Date( + latestVersion.recommendedPhaseTargetDate + ); + + let timeToTargetDate = 0; + if (currentDate > recommendedPhaseTargetDate) { + // Indicates that this is in the past + timeToTargetDate = checkTimeBetweenDates( + currentDate, + recommendedPhaseTargetDate + ); + timeToTargetDate = -timeToTargetDate; + } else + timeToTargetDate = checkTimeBetweenDates( + recommendedPhaseTargetDate, + currentDate + ); + + const daysBetweenDates = checkTimeBetweenDates( + currentDate, + latestVersion.candidatePhaseReachedAt + ); + const DAYS_TO_PROVIDE_FEEDBACK = 120; + const shouldShowAdvanceButton = + isAdmin && + issuesCount === 0 && + (recommendedTestPlanVersions.length || + (!recommendedTestPlanVersions.length && + daysBetweenDates > DAYS_TO_PROVIDE_FEEDBACK)); + + let coveredReports = []; + latestVersion.testPlanReports.forEach(testPlanReport => { + const atName = testPlanReport.at.name; + const browserName = testPlanReport.browser.name; + const value = `${atName}_${browserName}`; + + if (!coveredReports.includes(value)) + coveredReports.push(value); + }); + + // Phase is "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + + + + V + {convertDateToString( + latestVersionDate, + 'YY.MM.DD' + )} + + + + {shouldShowAdvanceButton && ( + + )} + + + {' '} + {issuesCount} Open Issue + {`${issuesCount === 1 ? '' : 's'}`} + {`${ + issuesCount >= 2 + ? ` from ${uniqueAtsCount} ATs` + : '' + }`} + + + Target{' '} + {isAdmin ? ( + + ) : ( + <> + + {Math.abs(timeToTargetDate)}{' '} + Days + + + )}{' '} + {timeToTargetDate < 0 ? 'Past' : 'Away'} + + + + ); + } + return defaultView; + } + case 'RECOMMENDED': { + // If a version of the plan is not in the recommended phase, shows the string "None + // Yet" + if (!testPlanVersions.length) + return None Yet; + + // Link with text "VERSION_STRING" that targets the single-page view of the plan + const { latestVersion, latestVersionDate } = + getVersionData(testPlanVersions); + + const completionDate = latestVersion.recommendedPhaseReachedAt; + + // Phase is "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + + + + V + {convertDateToString( + latestVersionDate, + 'YY.MM.DD' + )} + + + + + Approved{' '} + + {convertDateToString( + completionDate, + 'MMM D, YYYY' + )} + + + + ); + } + } + }; + + return ( + + + + {testPlan.title} + + {renderCellForCoveredAts()} + {renderCellForOverallStatus()} + {renderCellForPhase('RD', rdTestPlanVersions)} + {renderCellForPhase('DRAFT', draftTestPlanVersions)} + + {renderCellForPhase('CANDIDATE', candidateTestPlanVersions)} + + + {renderCellForPhase( + 'RECOMMENDED', + recommendedTestPlanVersions + )} + + + + {showThemedModal && themedModal} + {showAdvanceModal && ( + + This version will be updated to{' '} + {advanceModalData.phase}.{' '} + {advanceModalData.coveredReports?.length ? ( + <> +
    +
    + The included reports cover: +

      + {advanceModalData.coveredReports.map( + e => { + const [atName, browserName] = + e.split('_'); + + return ( +
    • + + {atName} and{' '} + {browserName} + +
    • + ); + } + )} +
    + Do you want to continue? + + ) : ( + <>Do you want to continue? + )} + + } + actionLabel="Continue" + closeLabel="Cancel" + handleAction={async () => { + await advanceModalData.advanceFunc(); + }} + handleClose={() => setShowAdvanceModal(false)} + staticBackdrop={true} + /> + )} + {showUpdateTargetModal && ( + setShowUpdateTargetModal(false)} + /> + )} + + ); +}; + +DataManagementRow.propTypes = { + isAdmin: PropTypes.bool, + ats: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string + }) + ), + testPlan: PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + directory: PropTypes.string + }).isRequired, + testPlanVersions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + phase: PropTypes.string, + gitSha: PropTypes.string, + testPlan: PropTypes.shape({ + directory: PropTypes.string + }), + updatedAt: PropTypes.string, + draftPhaseReachedAt: PropTypes.string, + candidatePhaseReachedAt: PropTypes.string, + recommendedPhaseReachedAt: PropTypes.string + }) + ).isRequired, + setTestPlanVersions: PropTypes.func +}; + +export default DataManagementRow; diff --git a/client/components/DataManagement/index.jsx b/client/components/DataManagement/index.jsx new file mode 100644 index 000000000..a58fadcf1 --- /dev/null +++ b/client/components/DataManagement/index.jsx @@ -0,0 +1,197 @@ +import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; +import { Container, Table, Alert } from 'react-bootstrap'; +import { useQuery } from '@apollo/client'; +import { DATA_MANAGEMENT_PAGE_QUERY } from './queries'; +import PageStatus from '../common/PageStatus'; +import ManageTestQueue from '../ManageTestQueue'; +import DataManagementRow from '@components/DataManagement/DataManagementRow'; +import './DataManagement.css'; +import { evaluateAuth } from '@client/utils/evaluateAuth'; + +const DataManagement = () => { + const { loading, data, error, refetch } = useQuery( + DATA_MANAGEMENT_PAGE_QUERY, + { + fetchPolicy: 'cache-and-network' + } + ); + + const [pageReady, setPageReady] = useState(false); + const [ats, setAts] = useState([]); + const [browsers, setBrowsers] = useState([]); + const [testPlans, setTestPlans] = useState([]); + const [testPlanVersions, setTestPlanVersions] = useState([]); + + const auth = evaluateAuth(data && data.me ? data.me : {}); + const { isAdmin } = auth; + + useEffect(() => { + if (data) { + const { + ats = [], + browsers = [], + testPlanVersions = [], + testPlans = [] + } = data; + setAts(ats); + setBrowsers(browsers); + setTestPlans(testPlans); + setTestPlanVersions(testPlanVersions); + setPageReady(true); + } + }, [data]); + + if (error) { + return ( + + ); + } + + if (loading || !pageReady) { + return ( + + ); + } + + const emptyTestPlans = !testPlans.length; + + return ( + + + Data Management | ARIA-AT + +

    Data Management

    + + {emptyTestPlans && ( +

    + There are no Test Plans available +

    + )} + + {emptyTestPlans && isAdmin && ( + + Add a Test Plan to the Queue + + )} + + {isAdmin ? ( + <> +

    + Manage Test Plans in the Test Queue and their phases. +

    + + + + ) : ( +

    + View Test Plans in the Test Queue and their phases. +

    + )} + +

    Test Plans Status Summary

    + + + + + + + + + + + + + + {testPlans + .slice() + .sort((a, b) => { + // First sort by overall status descending: recommended plans, + // then candidate plans, then draft plans, then R&D complete plans. + const phaseOrder = { + RD: 0, + DRAFT: 1, + CANDIDATE: 2, + RECOMMENDED: 3 + }; + + const getTestPlanVersionOverallPhase = t => { + let testPlanVersionOverallPhase = 'RD'; + + Object.keys(phaseOrder).forEach(phaseKey => { + testPlanVersionOverallPhase = + testPlanVersions.filter( + ({ phase, testPlan }) => + testPlan.directory === + t.directory && + phase === phaseKey + ).length + ? phaseKey + : testPlanVersionOverallPhase; + }); + return testPlanVersionOverallPhase; + }; + + const testPlanVersionOverallA = + getTestPlanVersionOverallPhase(a); + const testPlanVersionOverallB = + getTestPlanVersionOverallPhase(b); + + const phaseA = phaseOrder[testPlanVersionOverallA]; + const phaseB = phaseOrder[testPlanVersionOverallB]; + + if (phaseA > phaseB) return -1; + if (phaseA < phaseB) return 1; + + // Then sort by test plan name ascending. + if (a.title < b.title) return -1; + if (a.title > b.title) return 1; + + return 0; + }) + .map(testPlan => { + return ( + + testPlanVersion.testPlan + .directory === + testPlan.directory + )} + setTestPlanVersions={setTestPlanVersions} + /> + ); + })} + +
    Test PlanCovered ATOverall StatusR&D VersionDraft ReviewCandidate ReviewRecommended Version
    +
    + ); +}; + +export default DataManagement; diff --git a/client/components/DataManagement/queries.js b/client/components/DataManagement/queries.js new file mode 100644 index 000000000..5e8d5555d --- /dev/null +++ b/client/components/DataManagement/queries.js @@ -0,0 +1,153 @@ +import { gql } from '@apollo/client'; + +export const DATA_MANAGEMENT_PAGE_QUERY = gql` + query DataManagementPage { + me { + id + username + roles + } + ats { + id + name + atVersions { + id + name + releasedAt + } + } + browsers { + id + name + } + testPlans { + id + directory + title + } + testPlanVersions { + id + title + phase + gitSha + gitMessage + updatedAt + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + testPlan { + directory + } + testPlanReports { + id + markedFinalAt + at { + id + name + } + browser { + id + name + } + issues { + link + isOpen + feedbackType + } + } + } + } +`; + +export const UPDATE_TEST_PLAN_VERSION_PHASE = gql` + mutation UpdateTestPlanVersionPhase( + $testPlanVersionId: ID! + $phase: TestPlanVersionPhase! + $testPlanVersionDataToIncludeId: ID + ) { + testPlanVersion(id: $testPlanVersionId) { + updatePhase( + phase: $phase + testPlanVersionDataToIncludeId: $testPlanVersionDataToIncludeId + ) { + testPlanVersion { + id + title + phase + gitSha + gitMessage + updatedAt + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + testPlan { + directory + } + testPlanReports { + id + at { + id + name + } + browser { + id + name + } + issues { + link + isOpen + feedbackType + } + } + } + } + } + } +`; + +export const UPDATE_TEST_PLAN_VERSION_RECOMMENDED_TARGET_DATE = gql` + mutation UpdateTestPlanReportRecommendedTargetDate( + $testPlanVersionId: ID! + $recommendedPhaseTargetDate: Timestamp! + ) { + testPlanVersion(id: $testPlanVersionId) { + updateRecommendedPhaseTargetDate( + recommendedPhaseTargetDate: $recommendedPhaseTargetDate + ) { + testPlanVersion { + id + title + phase + gitSha + gitMessage + updatedAt + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + testPlan { + directory + } + testPlanReports { + id + at { + id + name + } + browser { + id + name + } + issues { + link + isOpen + feedbackType + } + } + } + } + } + } +`; diff --git a/client/components/DisclaimerInfo/index.jsx b/client/components/DisclaimerInfo/index.jsx index 8e04b54b8..e2c9abee3 100644 --- a/client/components/DisclaimerInfo/index.jsx +++ b/client/components/DisclaimerInfo/index.jsx @@ -81,11 +81,11 @@ const content = { } }; -const DisclaimerInfo = ({ reportStatus }) => { +const DisclaimerInfo = ({ phase }) => { const [expanded, setExpanded] = useState(false); - const title = content[reportStatus].title; - const messageContent = content[reportStatus].messageContent; + const title = content[phase].title; + const messageContent = content[phase].messageContent; return ( @@ -112,7 +112,7 @@ const DisclaimerInfo = ({ reportStatus }) => { }; DisclaimerInfo.propTypes = { - reportStatus: PropTypes.string + phase: PropTypes.string }; export default DisclaimerInfo; diff --git a/client/components/ManageTestQueue/index.jsx b/client/components/ManageTestQueue/index.jsx index 47e408738..63382bc0f 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -156,14 +156,17 @@ const ManageTestQueue = ({ .flat(); // to remove duplicate entries from different test plan versions of the same test plan being imported multiple times - const filteredTestPlanVersions = allTestPlanVersions.filter( - (v, i, a) => - a.findIndex( - t => - t.title === v.title && - t.testPlan.directory === v.testPlan.directory - ) === i - ); + const filteredTestPlanVersions = allTestPlanVersions + .filter( + (v, i, a) => + a.findIndex( + t => + t.title === v.title && + t.testPlan.directory === v.testPlan.directory + ) === i + ) + // sort by the testPlanVersion titles + .sort((a, b) => (a.title < b.title ? -1 : 1)); // mark the first testPlanVersion as selected if (filteredTestPlanVersions.length) { @@ -172,11 +175,7 @@ const ManageTestQueue = ({ } setAllTestPlanVersions(allTestPlanVersions); - setFilteredTestPlanVersions( - filteredTestPlanVersions.sort((a, b) => - a.title < b.title ? -1 : 1 - ) - ); + setFilteredTestPlanVersions(filteredTestPlanVersions); }, [testPlanVersions]); useEffect(() => { @@ -204,11 +203,16 @@ const ManageTestQueue = ({ ); // find the versions that apply and pre-set these - const matchingTestPlanVersions = allTestPlanVersions.filter( - item => - item.title === retrievedTestPlan.title && - item.testPlan.directory === retrievedTestPlan.testPlan.directory - ); + const matchingTestPlanVersions = allTestPlanVersions + .filter( + item => + item.title === retrievedTestPlan.title && + item.testPlan.directory === + retrievedTestPlan.testPlan.directory + ) + .sort((a, b) => + new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1 + ); setMatchingTestPlanVersions(matchingTestPlanVersions); setSelectedTestPlanVersionId(matchingTestPlanVersions[0].id); }; diff --git a/client/components/Reports/Reports.jsx b/client/components/Reports/Reports.jsx index 920638a3f..33a911310 100644 --- a/client/components/Reports/Reports.jsx +++ b/client/components/Reports/Reports.jsx @@ -33,7 +33,11 @@ const Reports = () => { if (!data) return null; return ( - + testPlanVersion.testPlanReports.length + )} + /> ); }; diff --git a/client/components/Reports/SummarizeTestPlanReport.jsx b/client/components/Reports/SummarizeTestPlanReport.jsx index 4c779f127..4a04bcc87 100644 --- a/client/components/Reports/SummarizeTestPlanReport.jsx +++ b/client/components/Reports/SummarizeTestPlanReport.jsx @@ -30,11 +30,7 @@ const getTestersRunHistory = ( const { testPlanReport, testResults, tester } = draftTestPlanRun; const testResult = testResults.find(item => item.test.id === testId); - if ( - testPlanReportId === testPlanReport.id && - testPlanReport.status === 'CANDIDATE' && - testResult?.completedAt - ) { + if (testPlanReportId === testPlanReport.id && testResult?.completedAt) { lines.push(
  • { Details for test: {test.title} - +
  • @@ -193,6 +189,7 @@ SummarizeTestPlanVersion.propTypes = { testPlanVersion: PropTypes.shape({ id: PropTypes.string.isRequired, title: PropTypes.string, + phase: PropTypes.string, metadata: PropTypes.shape({ exampleUrl: PropTypes.string.isRequired, designPatternUrl: PropTypes.string @@ -201,7 +198,6 @@ SummarizeTestPlanVersion.propTypes = { testPlanReports: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, runnableTests: PropTypes.arrayOf(PropTypes.object).isRequired, finalizedTestResults: PropTypes.arrayOf(PropTypes.object).isRequired }).isRequired diff --git a/client/components/Reports/queries.js b/client/components/Reports/queries.js index 01de5a695..5850fc545 100644 --- a/client/components/Reports/queries.js +++ b/client/components/Reports/queries.js @@ -12,7 +12,7 @@ export const REPORTS_PAGE_QUERY = gql` directory } metadata - testPlanReports(isCurrentPhase: true) { + testPlanReports(isFinal: true) { id metrics at { @@ -40,9 +40,8 @@ export const REPORT_PAGE_QUERY = gql` directory } metadata - testPlanReports(isCurrentPhase: true) { + testPlanReports(isFinal: true) { id - status metrics at { id @@ -113,7 +112,6 @@ export const REPORT_PAGE_QUERY = gql` } testPlanReport { id - status } testResults { test { diff --git a/client/components/TestManagement/StatusSummaryRow/index.jsx b/client/components/TestManagement/StatusSummaryRow/index.jsx deleted file mode 100644 index d7995576d..000000000 --- a/client/components/TestManagement/StatusSummaryRow/index.jsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useState, useRef } from 'react'; -import PropTypes from 'prop-types'; -import styled from '@emotion/styled'; -import { useMutation } from '@apollo/client'; -import { Dropdown } from 'react-bootstrap'; -import nextId from 'react-id-generator'; -import { BULK_UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION } from '../queries'; -import { LoadingStatus, useTriggerLoad } from '../../common/LoadingStatus'; -import BasicThemedModal from '../../common/BasicThemedModal'; - -const PhaseText = styled.span` - font-size: 14px; - margin-left: 8px; - padding: 4px 8px; - border-radius: 14px; - overflow: hidden; - white-space: nowrap; - color: white; - - &.draft { - background: #838f97; - } - - &.candidate { - background: #f87f1b; - } - - &.recommended { - background: #b253f8; - } -`; - -const PhaseDot = styled.span` - display: inline-block; - height: 10px; - width: 10px; - padding: 0; - margin-right: 8px; - border-radius: 50%; - - &.draft { - background: #838f97; - } - - &.candidate { - background: #f87f1b; - } - - &.recommended { - background: #b253f8; - } -`; - -const NoPhaseText = styled.span` - margin-left: 12px; - margin-right: 12px; -`; - -const StatusSummaryRow = ({ reportResult, testPlanVersion }) => { - const [bulkUpdateTestPlanReportStatusMutation] = useMutation( - BULK_UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION - ); - - const dropdownUpdateReportStatusButtonRef = useRef(); - const { triggerLoad, loadingMessage } = useTriggerLoad(); - - const [testPlanReports, setTestPlanReports] = useState( - Object.values(reportResult).filter(i => i !== null) - ); - const [showThemedModal, setShowThemedModal] = useState(false); - const [themedModalType, setThemedModalType] = useState('warning'); - const [themedModalTitle, setThemedModalTitle] = useState(''); - const [themedModalContent, setThemedModalContent] = useState(<>); - - const bulkUpdateReportStatus = async (testPlanReportIds, status) => { - try { - await triggerLoad(async () => { - const result = await bulkUpdateTestPlanReportStatusMutation({ - variables: { - testReportIds: testPlanReportIds, - status - } - }); - setTestPlanReports( - result.data.testPlanReport.bulkUpdateStatus.map( - i => i.testPlanReport - ) - ); - }, 'Updating Test Plan Status'); - } catch (e) { - showThemedMessage( - 'Error Updating Test Plan Status', - <>{e.message}, - 'warning' - ); - } - }; - - const showThemedMessage = (title, content, theme) => { - setThemedModalTitle(title); - setThemedModalContent(content); - setThemedModalType(theme); - setShowThemedModal(true); - }; - - const onThemedModalClose = () => { - setShowThemedModal(false); - dropdownUpdateReportStatusButtonRef.current.focus(); - }; - - let phase = 'Draft'; - if (testPlanReports.every(i => i.status === 'RECOMMENDED')) - phase = 'Recommended'; - else if (testPlanReports.every(i => i.status === 'CANDIDATE')) - phase = 'Candidate'; - - return ( - - - - {testPlanVersion?.title} - {Object.entries(reportResult).length > 0 && ( - - {phase} - - )} - - - {(Object.entries(reportResult).length <= 0 && ( - Not tested - )) || ( - - - - {phase} - - - { - await bulkUpdateReportStatus( - testPlanReports.map(i => i.id), - 'DRAFT' - ); - }} - > - - Draft - - { - await bulkUpdateReportStatus( - testPlanReports.map(i => i.id), - 'CANDIDATE' - ); - }} - > - - Candidate - - { - await bulkUpdateReportStatus( - testPlanReports.map(i => i.id), - 'RECOMMENDED' - ); - }} - > - - Recommended - - - - )} - - - - {showThemedModal && ( - - )} - - ); -}; - -StatusSummaryRow.propTypes = { - reportResult: PropTypes.object, - testPlanVersion: PropTypes.object -}; - -export default StatusSummaryRow; diff --git a/client/components/TestManagement/TestManagement.css b/client/components/TestManagement/TestManagement.css deleted file mode 100644 index 0aaf40174..000000000 --- a/client/components/TestManagement/TestManagement.css +++ /dev/null @@ -1,27 +0,0 @@ -.test-management.table { - padding: 0; - margin: 0; -} - -.test-management.table tbody tr th { - padding: 20px; - vertical-align: middle; -} - -.test-management.table tbody tr td { - vertical-align: middle; -} - -.test-management.table th.phase { - width: 225px; -} - -.change-phase { - margin-left: 12px; - margin-right: 12px; -} - -.change-phase .btn { - display: flex; - align-items: center; -} diff --git a/client/components/TestManagement/index.jsx b/client/components/TestManagement/index.jsx deleted file mode 100644 index 9365f205c..000000000 --- a/client/components/TestManagement/index.jsx +++ /dev/null @@ -1,315 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; -import { Container, Table, Alert } from 'react-bootstrap'; -import { useQuery } from '@apollo/client'; -import { TEST_MANAGEMENT_PAGE_QUERY } from './queries'; -import StatusSummaryRow from './StatusSummaryRow'; -import PageStatus from '../common/PageStatus'; -import DisclosureComponent from '../common/DisclosureComponent'; -import ManageTestQueue from '../ManageTestQueue'; -import alphabetizeObjectBy from '@client/utils/alphabetizeObjectBy'; -import { - getTestPlanTargetTitle, - getTestPlanVersionTitle -} from '@components/Reports/getTitles'; -import './TestManagement.css'; - -const TestManagement = () => { - const { loading, data, error, refetch } = useQuery( - TEST_MANAGEMENT_PAGE_QUERY, - { - fetchPolicy: 'cache-and-network' - } - ); - - const [pageReady, setPageReady] = useState(false); - const [ats, setAts] = useState([]); - const [browsers, setBrowsers] = useState([]); - const [testPlans, setTestPlans] = useState([]); - const [testPlanVersions, setTestPlanVersions] = useState([]); - const [testPlanReports, setTestPlanReports] = useState([]); - - useEffect(() => { - if (data) { - const { - ats = [], - browsers = [], - testPlanVersions = [], - testPlanReports = [], - testPlans = [] - } = data; - setAts(ats); - setTestPlanVersions(testPlanVersions); - setTestPlanReports(testPlanReports); - setTestPlans(testPlans); - setBrowsers(browsers); - setPageReady(true); - } - }, [data]); - - if (error) { - return ( - - ); - } - - if (loading || !pageReady) { - return ( - - ); - } - - const emptyTestPlans = !testPlanReports.length; - - const testPlanReportsById = {}; - let testPlanTargetsById = {}; - let testPlanVersionsById = {}; - testPlanReports.forEach(testPlanReport => { - const { testPlanVersion, at, browser } = testPlanReport; - - // Construct testPlanTarget - const testPlanTarget = { id: `${at.id}${browser.id}`, at, browser }; - testPlanReportsById[testPlanReport.id] = testPlanReport; - testPlanTargetsById[testPlanTarget.id] = testPlanTarget; - testPlanVersionsById[testPlanVersion.id] = testPlanVersion; - }); - testPlanTargetsById = alphabetizeObjectBy(testPlanTargetsById, keyValue => - getTestPlanTargetTitle(keyValue[1]) - ); - testPlanVersionsById = alphabetizeObjectBy(testPlanVersionsById, keyValue => - getTestPlanVersionTitle(keyValue[1]) - ); - - const tabularReports = {}; - const tabularReportsByDirectory = {}; - Object.keys(testPlanVersionsById).forEach(testPlanVersionId => { - const directory = - testPlanVersionsById[testPlanVersionId].testPlan.directory; - - tabularReports[testPlanVersionId] = {}; - if (!tabularReportsByDirectory[directory]) - tabularReportsByDirectory[directory] = {}; - tabularReportsByDirectory[directory][testPlanVersionId] = {}; - Object.keys(testPlanTargetsById).forEach(testPlanTargetId => { - tabularReports[testPlanVersionId][testPlanTargetId] = null; - tabularReportsByDirectory[directory][testPlanVersionId][ - testPlanTargetId - ] = null; - }); - }); - - testPlanReports.forEach(testPlanReport => { - const { testPlanVersion, at, browser } = testPlanReport; - const directory = testPlanVersion.testPlan.directory; - - // Construct testPlanTarget - const testPlanTarget = { id: `${at.id}${browser.id}`, at, browser }; - tabularReports[testPlanVersion.id][testPlanTarget.id] = testPlanReport; - tabularReportsByDirectory[directory][testPlanVersion.id][ - testPlanTarget.id - ] = testPlanReport; - tabularReportsByDirectory[directory][ - testPlanVersion.id - ].testPlanVersion = testPlanVersion; - }); - - testPlans.forEach(testPlan => { - if (!(testPlan.directory in tabularReportsByDirectory)) { - tabularReportsByDirectory[testPlan.directory] = testPlan; - } - }); - - const combineObject = originalObject => { - let combinedTestPlanVersionIdArray = []; - let resultTestPlanTargets = Object.values(originalObject)[0]; - combinedTestPlanVersionIdArray.push( - resultTestPlanTargets.testPlanVersion.id - ); - - for (let i = 1; i < Object.values(originalObject).length; i++) { - let testPlanTargets = Object.values(originalObject)[i]; - if ( - !combinedTestPlanVersionIdArray.includes( - testPlanTargets.testPlanVersion.id - ) - ) - combinedTestPlanVersionIdArray.push( - testPlanTargets.testPlanVersion.id - ); - - delete testPlanTargets.testPlanVersion; - - // Check if exists in newObject and add/update newObject based on criteria - Object.keys(testPlanTargets).forEach(testPlanTargetKey => { - if (!resultTestPlanTargets[testPlanTargetKey]) - resultTestPlanTargets[testPlanTargetKey] = - testPlanTargets[testPlanTargetKey]; - else { - const latestPrevDate = new Date( - testPlanTargets[ - testPlanTargetKey - ]?.latestAtVersionReleasedAt?.releasedAt - ); - - const latestCurrDate = new Date( - resultTestPlanTargets[ - testPlanTargetKey - ]?.latestAtVersionReleasedAt?.releasedAt - ); - - if (latestPrevDate >= latestCurrDate) - resultTestPlanTargets[testPlanTargetKey] = - testPlanTargets[testPlanTargetKey]; - } - }); - } - return { resultTestPlanTargets, combinedTestPlanVersionIdArray }; - }; - - return ( - - - Test Management | ARIA-AT - -

    Test Management

    - - {emptyTestPlans && ( -

    - There are no test plans available -

    - )} - - {emptyTestPlans && ( - - Add a Test Plan to the Queue - - )} - -

    - Manage test plans in the Test Queue (which are using the latest - Assistive Technology versions), and their test plan phases. -

    - - - -
    -
    - - - - - - - - - - {/* Sort the summary items by title */} - {Object.values(tabularReportsByDirectory) - .sort((a, b) => { - return ( - a.title || - Object.values(a)[0].testPlanVersion - .title - ).localeCompare( - b.title || - Object.values(b)[0] - .testPlanVersion.title - ); - }) - .map(tabularReport => { - let reportResult = null; - let testPlanVersionId = null; - - // Evaluate what is prioritised across the - // collection of testPlanVersions - if ( - typeof Object.values( - tabularReport - )[0] !== 'object' - ) { - return ( - - ); - } else if ( - Object.values(tabularReport) - .length > 1 - ) { - const { - resultTestPlanTargets, - combinedTestPlanVersionIdArray - } = combineObject(tabularReport); - reportResult = - resultTestPlanTargets; - testPlanVersionId = - combinedTestPlanVersionIdArray.join( - ',' - ); - } else { - reportResult = - Object.values(tabularReport)[0]; - testPlanVersionId = - reportResult.testPlanVersion.id; - } - - const testPlanVersion = - reportResult.testPlanVersion; - delete reportResult.testPlanVersion; - - return ( - - ); - })} - -
    Test PlansPhase
    - - } - /> -
    - ); -}; - -export default TestManagement; diff --git a/client/components/TestManagement/queries.js b/client/components/TestManagement/queries.js deleted file mode 100644 index 8a1d78e7c..000000000 --- a/client/components/TestManagement/queries.js +++ /dev/null @@ -1,99 +0,0 @@ -import { gql } from '@apollo/client'; - -export const TEST_MANAGEMENT_PAGE_QUERY = gql` - query TestManagementPage { - ats { - id - name - atVersions { - id - name - releasedAt - } - } - browsers { - id - name - } - testPlans { - directory - id - title - latestTestPlanVersion { - id - title - } - } - testPlanVersions { - id - title - gitSha - gitMessage - testPlan { - directory - } - updatedAt - } - testPlanReports(statuses: [DRAFT, CANDIDATE, RECOMMENDED]) { - id - status - at { - id - name - } - latestAtVersionReleasedAt { - id - name - releasedAt - } - browser { - id - name - } - testPlanVersion { - id - title - gitSha - gitMessage - testPlan { - directory - } - updatedAt - } - } - } -`; - -export const BULK_UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION = gql` - mutation BulkUpdateTestPlanReportStatus( - $testReportIds: [ID]! - $status: TestPlanReportStatus! - ) { - testPlanReport(ids: $testReportIds) { - bulkUpdateStatus(status: $status) { - testPlanReport { - id - status - at { - id - name - } - browser { - id - name - } - testPlanVersion { - id - title - gitSha - gitMessage - testPlan { - directory - } - updatedAt - } - } - } - } - } -`; diff --git a/client/components/TestQueue/queries.js b/client/components/TestQueue/queries.js index cc12bce53..c1f565c80 100644 --- a/client/components/TestQueue/queries.js +++ b/client/components/TestQueue/queries.js @@ -35,11 +35,11 @@ export const TEST_QUEUE_PAGE_QUERY = gql` } updatedAt } - testPlanReports(statuses: [DRAFT]) { + testPlanReports(isFinal: false) { id - status conflictsLength runnableTestsLength + markedFinalAt at { id name @@ -51,6 +51,7 @@ export const TEST_QUEUE_PAGE_QUERY = gql` testPlanVersion { id title + phase gitSha gitMessage testPlan { @@ -83,7 +84,6 @@ export const TEST_PLAN_REPORT_QUERY = gql` query TestPlanReport($testPlanReportId: ID!) { testPlanReport(id: $testPlanReportId) { id - status conflictsLength runnableTests { id @@ -201,7 +201,6 @@ export const ADD_TEST_QUEUE_MUTATION = gql` populatedData { testPlanReport { id - status at { id } @@ -237,15 +236,12 @@ export const ASSIGN_TESTER_MUTATION = gql` } `; -export const UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION = gql` - mutation UpdateTestPlanReportStatus( - $testReportId: ID! - $status: TestPlanReportStatus! - ) { +export const UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION = gql` + mutation UpdateTestPlanReportMarkedFinalAt($testReportId: ID!) { testPlanReport(id: $testReportId) { - updateStatus(status: $status) { + markAsFinal { testPlanReport { - status + markedFinalAt } } } diff --git a/client/components/TestQueueRow/index.jsx b/client/components/TestQueueRow/index.jsx index 48229773a..3e28a8f37 100644 --- a/client/components/TestQueueRow/index.jsx +++ b/client/components/TestQueueRow/index.jsx @@ -11,11 +11,10 @@ import nextId from 'react-id-generator'; import { Button, Dropdown } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import ATAlert from '../ATAlert'; -import { capitalizeEachWord } from '../../utils/formatter'; import { TEST_PLAN_REPORT_QUERY, ASSIGN_TESTER_MUTATION, - UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION, + UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION, REMOVE_TEST_PLAN_REPORT_MUTATION, REMOVE_TESTER_MUTATION, REMOVE_TESTER_RESULTS_MUTATION @@ -54,8 +53,8 @@ const TestQueueRow = ({ const [themedModalContent, setThemedModalContent] = useState(<>); const [assignTester] = useMutation(ASSIGN_TESTER_MUTATION); - const [updateTestPlanReportStatus] = useMutation( - UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION + const [updateTestPlanMarkedFinalAt] = useMutation( + UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION ); const [removeTestPlanReport] = useMutation( REMOVE_TEST_PLAN_REPORT_MUTATION @@ -308,19 +307,24 @@ const TestQueueRow = ({ Open run as... - {draftTestPlanRuns.map(({ tester }) => { - return ( - - {tester.username} - - ); - })} + {draftTestPlanRuns + .slice() // because array was frozen + .sort((a, b) => + a.tester.username < b.tester.username ? -1 : 1 + ) + .map(({ tester }) => { + return ( + + {tester.username} + + ); + })} ); @@ -376,28 +380,15 @@ const TestQueueRow = ({ } }; - const updateReportStatus = async status => { + const updateReportStatus = async () => { try { await triggerLoad(async () => { - if (status === 'CANDIDATE') { - await updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status: status - } - }); - await triggerPageUpdate(); - } else { - // Unnecessary unless we're introducing a new status which can also be viewed - // on the Test Queue - await updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status: status - } - }); - await triggerTestPlanReportUpdate(); - } + await updateTestPlanMarkedFinalAt({ + variables: { + testReportId: testPlanReport.id + } + }); + await triggerPageUpdate(); }, 'Updating Test Plan Status'); } catch (e) { showThemedMessage( @@ -408,46 +399,35 @@ const TestQueueRow = ({ } }; - const evaluateStatusAndResults = () => { - const { status: runStatus, conflictsLength } = testPlanReport; + const evaluateLabelStatus = () => { + const { conflictsLength } = testPlanReport; + const { phase } = testPlanVersion; - let status, results; + let labelStatus; if (isLoading) { - status = ( + labelStatus = ( Loading ... ); } else if (conflictsLength > 0) { let pluralizedStatus = `${conflictsLength} Conflict${ conflictsLength === 1 ? '' : 's' }`; - status = ( + labelStatus = ( {pluralizedStatus} ); - } else if (runStatus === 'DRAFT' || !runStatus) { - status = Draft; - } - - return { status, results }; - }; - - const evaluateNewReportStatus = () => { - const { conflictsLength } = testPlanReport; - - // If the results have been marked as draft and there is no conflict, - // they can be marked as "CANDIDATE" - - if (conflictsLength === 0 && testPlanRunsWithResults.length > 0) { - return 'CANDIDATE'; + } else if (phase === 'DRAFT' || !phase) { + labelStatus = ( + Draft + ); } - return null; + return labelStatus; }; - const { status, results } = evaluateStatusAndResults(); - const nextReportStatus = evaluateNewReportStatus(); + const labelStatus = evaluateLabelStatus(); const getRowId = tester => [ @@ -530,10 +510,10 @@ const TestQueueRow = ({
    -
    {status}
    +
    {labelStatus}
    {isSignedIn && isTester && (
    - {isAdmin && !isLoading && nextReportStatus && ( + {isAdmin && !isLoading && ( <> )} - {results}
    )} diff --git a/client/components/TestRun/queries.js b/client/components/TestRun/queries.js index 8e334021c..43f58277f 100644 --- a/client/components/TestRun/queries.js +++ b/client/components/TestRun/queries.js @@ -41,7 +41,6 @@ export const TEST_RUN_PAGE_QUERY = gql` } testPlanReport { id - status conflicts { source { test { @@ -99,6 +98,7 @@ export const TEST_RUN_PAGE_QUERY = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -150,7 +150,6 @@ export const TEST_RUN_PAGE_ANON_QUERY = gql` query TestPlanRunAnonPage($testPlanReportId: ID!) { testPlanReport(id: $testPlanReportId) { id - status conflicts { source { test { @@ -208,6 +207,7 @@ export const TEST_RUN_PAGE_ANON_QUERY = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -295,7 +295,6 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -353,6 +352,7 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -444,7 +444,6 @@ export const SAVE_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -502,6 +501,7 @@ export const SAVE_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -593,7 +593,6 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -651,6 +650,7 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { diff --git a/client/routes/index.js b/client/routes/index.js index 0da144954..1b6325980 100644 --- a/client/routes/index.js +++ b/client/routes/index.js @@ -5,13 +5,13 @@ import Home from '@components/Home'; import InvalidRequest from '@components/InvalidRequest'; import NotFound from '@components/NotFound'; import { Reports, Report } from '@components/Reports'; -import CandidateTests from '@components/CandidateTests'; +import CandidateReview from '@components/CandidateReview'; import SignupInstructions from '@components/SignupInstructions'; import TestQueue from '@components/TestQueue'; import TestRun from '@components/TestRun'; import UserSettings from '@components/UserSettings'; -import CandidateTestPlanRun from '@components/CandidateTests/CandidateTestPlanRun'; -import TestManagement from '@components/TestManagement'; +import CandidateTestPlanRun from '@components/CandidateReview/CandidateTestPlanRun'; +import DataManagement from 'client/components/DataManagement'; export default () => ( @@ -58,22 +58,14 @@ export default () => ( } /> - - - } - /> - - + } /> + } /> } /> } /> } /> diff --git a/client/stories/ProvideFeedbackModal.stories.jsx b/client/stories/ProvideFeedbackModal.stories.jsx index be403cfcc..a2ed6ea9f 100644 --- a/client/stories/ProvideFeedbackModal.stories.jsx +++ b/client/stories/ProvideFeedbackModal.stories.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ProvideFeedbackModal from '../components/CandidateTests/CandidateModals/ProvideFeedbackModal'; +import ProvideFeedbackModal from '../components/CandidateReview/CandidateModals/ProvideFeedbackModal'; export default { component: ProvideFeedbackModal, diff --git a/client/stories/ThankYouModal.stories.jsx b/client/stories/ThankYouModal.stories.jsx index 3226994ca..1c6828b62 100644 --- a/client/stories/ThankYouModal.stories.jsx +++ b/client/stories/ThankYouModal.stories.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ThankYouModal from '../components/CandidateTests/CandidateModals/ThankYouModal/index.jsx'; +import ThankYouModal from '../components/CandidateReview/CandidateModals/ThankYouModal/index.jsx'; export default { component: ThankYouModal, diff --git a/client/tests/DataManagement.test.jsx b/client/tests/DataManagement.test.jsx new file mode 100644 index 000000000..8b90d1235 --- /dev/null +++ b/client/tests/DataManagement.test.jsx @@ -0,0 +1,70 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { InMemoryCache } from '@apollo/client'; +import { MockedProvider } from '@apollo/client/testing'; +import { BrowserRouter } from 'react-router-dom'; +import '@testing-library/jest-dom/extend-expect'; + +import DataManagement from '../components/DataManagement'; + +// eslint-disable-next-line jest/no-mocks-import +import { DATA_MANAGEMENT_PAGE_POPULATED } from './__mocks__/GraphQLMocks'; + +const setup = (mocks = []) => { + return render( + + + + + + ); +}; + +describe('Data Management page', () => { + let wrapper; + + beforeEach(() => { + wrapper = setup(DATA_MANAGEMENT_PAGE_POPULATED); + }); + + it('renders loading state on initialization', async () => { + const { getByTestId } = wrapper; + const element = getByTestId('page-status'); + + expect(element).toBeTruthy(); + expect(element).toHaveTextContent('Loading'); + }); + + it('renders Status Summary component', async () => { + // allow page time to load + await waitFor(() => new Promise(res => setTimeout(res, 0))); + + const { queryAllByText } = wrapper; + const statusSummaryElement = queryAllByText( + /Test Plans Status Summary/i + ); + const testPlanElement = queryAllByText(/Test Plan/i); + const coveredAtElement = queryAllByText(/Covered AT/i); + const overallStatusElement = queryAllByText(/Overall Status/i); + const rdElement = queryAllByText(/R&D Version/i); + const draftElement = queryAllByText(/Draft Review/i); + const candidateElement = queryAllByText(/Candidate Review/i); + const recommendedElement = queryAllByText(/Recommended Version/i); + + expect(statusSummaryElement.length).toBeGreaterThanOrEqual(1); + expect(testPlanElement.length).toBeGreaterThanOrEqual(1); + expect(coveredAtElement.length).toBeGreaterThanOrEqual(1); + expect(overallStatusElement.length).toBeGreaterThanOrEqual(1); + expect(rdElement.length).toBeGreaterThanOrEqual(1); + expect(draftElement.length).toBeGreaterThanOrEqual(1); + expect(candidateElement.length).toBeGreaterThanOrEqual(1); + expect(recommendedElement.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/client/tests/TestManagement.test.jsx b/client/tests/TestManagement.test.jsx deleted file mode 100644 index 45fe5583a..000000000 --- a/client/tests/TestManagement.test.jsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import React from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { InMemoryCache } from '@apollo/client'; -import { MockedProvider } from '@apollo/client/testing'; -import { BrowserRouter } from 'react-router-dom'; -import '@testing-library/jest-dom/extend-expect'; - -import TestManagement from '../components/TestManagement'; - -// eslint-disable-next-line jest/no-mocks-import -import { TEST_MANAGEMENT_PAGE_POPULATED } from './__mocks__/GraphQLMocks'; - -const setup = (mocks = []) => { - return render( - - - - - - ); -}; - -describe('Test Management page', () => { - let wrapper; - - beforeEach(() => { - wrapper = setup(TEST_MANAGEMENT_PAGE_POPULATED); - }); - - it('renders loading state on initialization', async () => { - const { getByTestId } = wrapper; - const element = getByTestId('page-status'); - - expect(element).toBeTruthy(); - expect(element).toHaveTextContent('Loading'); - }); - - it('renders Status Summary component', async () => { - // allow page time to load - await waitFor(() => new Promise(res => setTimeout(res, 0))); - - const { queryAllByText } = wrapper; - const statusSummaryElement = queryAllByText(/Status Summary/i); - const testPlansElement = queryAllByText(/Test Plans/i); - const phaseElement = queryAllByText(/Phase/i); - const candidateElements = queryAllByText(/Candidate/i); - const notTestedElements = queryAllByText(/Not tested/i); - - expect(statusSummaryElement.length).toBeGreaterThanOrEqual(1); - expect(testPlansElement.length).toBeGreaterThanOrEqual(1); - expect(phaseElement.length).toBeGreaterThanOrEqual(1); - expect(candidateElements.length).toBeGreaterThanOrEqual(1); - expect(notTestedElements.length).toBeGreaterThanOrEqual(1); - }); -}); diff --git a/client/tests/__mocks__/GraphQLMocks.js b/client/tests/__mocks__/GraphQLMocks.js index 29fe9cff8..d4f7b4b2b 100644 --- a/client/tests/__mocks__/GraphQLMocks.js +++ b/client/tests/__mocks__/GraphQLMocks.js @@ -1,5 +1,5 @@ import { TEST_QUEUE_PAGE_QUERY } from '../../components/TestQueue/queries'; -import { TEST_MANAGEMENT_PAGE_QUERY } from '../../components/TestManagement/queries'; +import { DATA_MANAGEMENT_PAGE_QUERY } from '../../components/DataManagement/queries'; export const TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_ADMIN = [ { @@ -219,11 +219,13 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN = [ status: 'DRAFT', conflictsLength: 0, runnableTestsLength: 17, + markedFinalAt: null, at: { id: '1', name: 'JAWS' }, browser: { id: '2', name: 'Chrome' }, testPlanVersion: { id: '1', title: 'Checkbox Example (Two State)', + phase: 'DRAFT', gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', gitMessage: 'The message for this SHA', testPlan: { directory: 'checkbox' }, @@ -245,11 +247,13 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN = [ status: 'DRAFT', conflictsLength: 0, runnableTestsLength: 17, + markedFinalAt: null, at: { id: '3', name: 'VoiceOver for macOS' }, browser: { id: '3', name: 'Safari' }, testPlanVersion: { id: '1', title: 'Checkbox Example (Two State)', + phase: 'DRAFT', gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', gitMessage: 'The message for this SHA', testPlan: { directory: 'checkbox' }, @@ -271,11 +275,13 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN = [ status: 'DRAFT', conflictsLength: 3, runnableTestsLength: 17, + markedFinalAt: null, at: { id: '2', name: 'NVDA' }, browser: { id: '1', name: 'Firefox' }, testPlanVersion: { id: '1', title: 'Checkbox Example (Two State)', + phase: 'DRAFT', gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', gitMessage: 'The message for this SHA', testPlan: { directory: 'checkbox' }, @@ -449,6 +455,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ status: 'DRAFT', conflictsLength: 0, runnableTestsLength: 17, + markedFinalAt: null, at: { id: '2', name: 'NVDA' @@ -460,6 +467,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ testPlanVersion: { id: '65', title: 'Checkbox Example (Two State)', + phase: 'DRAFT', gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', gitMessage: 'The message for this SHA', testPlan: { @@ -491,6 +499,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ status: 'DRAFT', conflictsLength: 0, runnableTestsLength: 17, + markedFinalAt: null, at: { id: '2', name: 'JAWS' @@ -502,6 +511,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ testPlanVersion: { id: '65', title: 'Checkbox Example (Two State)', + phase: 'DRAFT', gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', gitMessage: 'The message for this SHA', testPlan: { @@ -525,6 +535,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ status: 'DRAFT', conflictsLength: 0, runnableTestsLength: 15, + markedFinalAt: null, at: { id: '3', name: 'VoiceOver for macOS' @@ -536,6 +547,7 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ testPlanVersion: { id: '74', title: 'Editor Menubar Example', + phase: 'DRAFT', gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', gitMessage: 'The message for this SHA', testPlan: { @@ -552,13 +564,18 @@ export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ } ]; -export const TEST_MANAGEMENT_PAGE_POPULATED = [ +export const DATA_MANAGEMENT_PAGE_POPULATED = [ { request: { - query: TEST_MANAGEMENT_PAGE_QUERY + query: DATA_MANAGEMENT_PAGE_QUERY }, result: { data: { + me: { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + }, ats: [ { id: '1', @@ -610,876 +627,871 @@ export const TEST_MANAGEMENT_PAGE_POPULATED = [ ], testPlans: [ { - directory: 'alert', - id: 'alert', - title: 'Alert Example', - latestTestPlanVersion: { - id: '1', - title: 'Alert Example' - } + id: '27', + directory: 'radiogroup-aria-activedescendant', + title: 'Radio Group Example Using aria-activedescendant' }, { - directory: 'banner', - id: 'banner', - title: 'Banner Landmark', - latestTestPlanVersion: { - id: '2', - title: 'Banner Landmark' - } + id: '28', + directory: 'radiogroup-roving-tabindex', + title: 'Radio Group Example Using Roving tabindex' }, { - directory: 'breadcrumb', - id: 'breadcrumb', - title: 'Breadcrumb Example', - latestTestPlanVersion: { - id: '3', - title: 'Breadcrumb Example' - } + id: '31', + directory: 'slider-multithumb', + title: 'Horizontal Multi-Thumb Slider' }, { - directory: 'checkbox', - id: 'checkbox', - title: 'Checkbox Example (Two State)', - latestTestPlanVersion: { - id: '4', - title: 'Checkbox Example (Two State)' - } + id: '16', + directory: 'link-css', + title: 'Link Example 3 (CSS :before content property on a span element)' }, { - directory: 'checkbox-tri-state', - id: 'checkbox-tri-state', - title: 'Checkbox Example (Mixed-State)', - latestTestPlanVersion: { - id: '5', - title: 'Checkbox Example (Mixed-State)' - } + id: '17', + directory: 'link-img-alt', + title: 'Link Example 2 (img element with alt attribute)' }, { - directory: 'combobox-autocomplete-both-updated', - id: 'combobox-autocomplete-both-updated', - title: 'Combobox with Both List and Inline Autocomplete Example', - latestTestPlanVersion: { - id: '6', - title: 'Combobox with Both List and Inline Autocomplete Example' - } + id: '1', + directory: 'alert', + title: 'Alert Example' }, { - directory: 'combobox-select-only', - id: 'combobox-select-only', - title: 'Select Only Combobox Example', - latestTestPlanVersion: { - id: '7', - title: 'Select Only Combobox Example' - } + id: '13', + directory: 'disclosure-navigation', + title: 'Disclosure Navigation Menu Example' }, { - directory: 'command-button', - id: 'command-button', - title: 'Command Button Example', - latestTestPlanVersion: { - id: '8', - title: 'Command Button Example' - } + id: '5', + directory: 'checkbox-tri-state', + title: 'Checkbox Example (Mixed-State)' }, { - directory: 'complementary', - id: 'complementary', - title: 'Complementary Landmark', - latestTestPlanVersion: { - id: '9', - title: 'Complementary Landmark' - } + id: '3', + directory: 'breadcrumb', + title: 'Breadcrumb Example' }, { - directory: 'contentinfo', - id: 'contentinfo', - title: 'Contentinfo Landmark', - latestTestPlanVersion: { - id: '10', - title: 'Contentinfo Landmark' - } + id: '19', + directory: 'main', + title: 'Main Landmark' }, { - directory: 'datepicker-spin-button', - id: 'datepicker-spin-button', - title: 'Date Picker Spin Button Example', - latestTestPlanVersion: { - id: '11', - title: 'Date Picker Spin Button Example' - } + id: '24', + directory: 'meter', + title: 'Meter' }, { - directory: 'disclosure-faq', - id: 'disclosure-faq', - title: 'Disclosure of Answers to Frequently Asked Questions Example', - latestTestPlanVersion: { - id: '12', - title: 'Disclosure of Answers to Frequently Asked Questions Example' - } + id: '32', + directory: 'switch', + title: 'Switch Example' }, { - directory: 'disclosure-navigation', - id: 'disclosure-navigation', - title: 'Disclosure Navigation Menu Example', - latestTestPlanVersion: { - id: '13', - title: 'Disclosure Navigation Menu Example' - } + id: '26', + directory: 'modal-dialog', + title: 'Modal Dialog Example' }, { - directory: 'form', - id: 'form', - title: 'Form Landmark', - latestTestPlanVersion: { - id: '14', - title: 'Form Landmark' - } + id: '22', + directory: 'menu-button-navigation', + title: 'Navigation Menu Button' }, { - directory: 'horizontal-slider', - id: 'horizontal-slider', - title: 'Color Viewer Slider', - latestTestPlanVersion: { - id: '15', - title: 'Color Viewer Slider' - } + id: '34', + directory: 'toggle-button', + title: 'Toggle Button' }, { - directory: 'link-css', - id: 'link-css', - title: 'Link Example 3 (CSS :before content property on a span element)', - latestTestPlanVersion: { - id: '16', - title: 'Link Example 3 (CSS :before content property on a span element)' - } + id: '18', + directory: 'link-span-text', + title: 'Link Example 1 (span element with text content)' }, { - directory: 'link-img-alt', - id: 'link-img-alt', - title: 'Link Example 2 (img element with alt attribute)', - latestTestPlanVersion: { - id: '17', - title: 'Link Example 2 (img element with alt attribute)' - } + id: '8', + directory: 'command-button', + title: 'Command Button Example' }, { - directory: 'link-span-text', - id: 'link-span-text', - title: 'Link Example 1 (span element with text content)', - latestTestPlanVersion: { - id: '18', - title: 'Link Example 1 (span element with text content)' - } + id: '15', + directory: 'horizontal-slider', + title: 'Color Viewer Slider' }, { - directory: 'main', - id: 'main', - title: 'Main Landmark', - latestTestPlanVersion: { - id: '19', - title: 'Main Landmark' - } + id: '6', + directory: 'combobox-autocomplete-both-updated', + title: 'Combobox with Both List and Inline Autocomplete Example' }, { - directory: 'menu-button-actions', - id: 'menu-button-actions', - title: 'Action Menu Button Example Using element.focus()', - latestTestPlanVersion: { - id: '20', - title: 'Action Menu Button Example Using element.focus()' - } + id: '7', + directory: 'combobox-select-only', + title: 'Select Only Combobox Example' }, { - directory: 'menu-button-actions-active-descendant', - id: 'menu-button-actions-active-descendant', - title: 'Action Menu Button Example Using aria-activedescendant', - latestTestPlanVersion: { - id: '21', - title: 'Action Menu Button Example Using aria-activedescendant' - } + id: '4', + directory: 'checkbox', + title: 'Checkbox Example (Two State)' }, { - directory: 'menu-button-navigation', - id: 'menu-button-navigation', - title: 'Navigation Menu Button', - latestTestPlanVersion: { - id: '22', - title: 'Navigation Menu Button' - } + id: '9', + directory: 'complementary', + title: 'Complementary Landmark' }, { - directory: 'menubar-editor', - id: 'menubar-editor', - title: 'Editor Menubar Example', - latestTestPlanVersion: { - id: '23', - title: 'Editor Menubar Example' - } + id: '10', + directory: 'contentinfo', + title: 'Contentinfo Landmark' }, { - directory: 'meter', - id: 'meter', - title: 'Meter', - latestTestPlanVersion: { - id: '24', - title: 'Meter' - } + id: '11', + directory: 'datepicker-spin-button', + title: 'Date Picker Spin Button Example' }, { - directory: 'minimal-data-grid', - id: 'minimal-data-grid', - title: 'Data Grid Example 1: Minimal Data Grid', - latestTestPlanVersion: { - id: '25', - title: 'Data Grid Example 1: Minimal Data Grid' - } + id: '12', + directory: 'disclosure-faq', + title: 'Disclosure of Answers to Frequently Asked Questions Example' }, { - directory: 'modal-dialog', - id: 'modal-dialog', - title: 'Modal Dialog Example', - latestTestPlanVersion: { - id: '26', - title: 'Modal Dialog Example' - } + id: '14', + directory: 'form', + title: 'Form Landmark' }, { - directory: 'radiogroup-aria-activedescendant', - id: 'radiogroup-aria-activedescendant', - title: 'Radio Group Example Using aria-activedescendant', - latestTestPlanVersion: { - id: '27', - title: 'Radio Group Example Using aria-activedescendant' - } + id: '20', + directory: 'menu-button-actions', + title: 'Action Menu Button Example Using element.focus()' }, { - directory: 'radiogroup-roving-tabindex', - id: 'radiogroup-roving-tabindex', - title: 'Radio Group Example Using Roving tabindex', - latestTestPlanVersion: { - id: '28', - title: 'Radio Group Example Using Roving tabindex' - } + id: '21', + directory: 'menu-button-actions-active-descendant', + title: 'Action Menu Button Example Using aria-activedescendant' }, { - directory: 'rating-slider', - id: 'rating-slider', - title: 'Rating Slider', - latestTestPlanVersion: { - id: '29', - title: 'Rating Slider' - } + id: '23', + directory: 'menubar-editor', + title: 'Editor Menubar Example' }, { - directory: 'seek-slider', - id: 'seek-slider', - title: 'Media Seek Slider', - latestTestPlanVersion: { - id: '30', - title: 'Media Seek Slider' - } + id: '25', + directory: 'minimal-data-grid', + title: 'Data Grid Example 1: Minimal Data Grid' }, { - directory: 'slider-multithumb', - id: 'slider-multithumb', - title: 'Horizontal Multi-Thumb Slider', - latestTestPlanVersion: { - id: '31', - title: 'Horizontal Multi-Thumb Slider' - } + id: '29', + directory: 'rating-slider', + title: 'Rating Slider' }, { - directory: 'switch', - id: 'switch', - title: 'Switch Example', - latestTestPlanVersion: { - id: '32', - title: 'Switch Example' - } + id: '30', + directory: 'seek-slider', + title: 'Media Seek Slider' }, { + id: '33', directory: 'tabs-manual-activation', - id: 'tabs-manual-activation', - title: 'Tabs with Manual Activation', - latestTestPlanVersion: { - id: '33', - title: 'Tabs with Manual Activation' - } + title: 'Tabs with Manual Activation' }, { - directory: 'toggle-button', - id: 'toggle-button', - title: 'Toggle Button', - latestTestPlanVersion: { - id: '34', - title: 'Toggle Button' - } + id: '35', + directory: 'vertical-temperature-slider', + title: 'Vertical Temperature Slider' }, { - directory: 'vertical-temperature-slider', - id: 'vertical-temperature-slider', - title: 'Vertical Temperature Slider', - latestTestPlanVersion: { - id: '35', - title: 'Vertical Temperature Slider' - } + id: '2', + directory: 'banner', + title: 'Banner Landmark' } ], testPlanVersions: [ { - id: '21', - title: 'Action Menu Button Example Using aria-activedescendant', + id: '28', + title: 'Radio Group Example Using Roving tabindex', + phase: 'RD', gitSha: '1768070bd68beefef29284b568d2da910b449c14', gitMessage: 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + updatedAt: '2023-04-10T18:22:22.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'menu-button-actions-active-descendant' + directory: 'radiogroup-roving-tabindex' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '20', - title: 'Action Menu Button Example Using element.focus()', + id: '27', + title: 'Radio Group Example Using aria-activedescendant', + phase: 'RD', gitSha: '1768070bd68beefef29284b568d2da910b449c14', gitMessage: 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + updatedAt: '2023-04-10T18:22:22.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'menu-button-actions' + directory: 'radiogroup-aria-activedescendant' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '1', - title: 'Alert Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '31', + title: 'Horizontal Multi-Thumb Slider', + phase: 'RD', + gitSha: 'b5fe3efd569518e449ef9a0978b0dec1f2a08bd6', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create tests for APG design pattern example: Horizontal Multi-Thumb Slider (#511)', + updatedAt: '2023-03-20T21:24:41.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'alert' + directory: 'slider-multithumb' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '2', - title: 'Banner Landmark', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '16', + title: 'Link Example 3 (CSS :before content property on a span element)', + phase: 'RD', + gitSha: '7a8454bca6de980199868101431817cea03cce35', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create tests for APG design pattern example: Link Example 3 (CSS :before content property on a span element) (#518)', + updatedAt: '2023-03-13T22:10:13.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'banner' + directory: 'link-css' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '3', - title: 'Breadcrumb Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '17', + title: 'Link Example 2 (img element with alt attribute)', + phase: 'RD', + gitSha: 'dc637636cff74b51f5c468ff3b81bd1f38aefbb2', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create tests for APG design pattern example: Link Example 2 (img element with alt attribute) (#516)', + updatedAt: '2023-03-13T19:51:48.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'breadcrumb' + directory: 'link-img-alt' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '5', - title: 'Checkbox Example (Mixed-State)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '1', + title: 'Alert Example', + phase: 'DRAFT', + gitSha: '0928bcf530efcf4faa677285439701537674e014', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Alert and Radiogroup/activedescendent updates (#865)', + updatedAt: '2022-12-08T21:47:42.000Z', + draftPhaseReachedAt: '2022-07-06T00:00:00.000Z', + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'checkbox-tri-state' + directory: 'alert' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [ + { + id: '7', + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [] + } + ] }, { - id: '4', - title: 'Checkbox Example (Two State)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '13', + title: 'Disclosure Navigation Menu Example', + phase: 'RD', + gitSha: '179ba0f438aaa5781b3ec8a4033d6bf9f757360b', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Delete up arrow command for VoiceOver when navigating backwards to a disclosure button (#845)', + updatedAt: '2022-10-31T19:29:17.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'checkbox' + directory: 'disclosure-navigation' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '15', - title: 'Color Viewer Slider', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '3', + title: 'Breadcrumb Example', + phase: 'RD', + gitSha: '1aa3b74d24d340362e9f511eae33788d55487d12', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Add down arrow command to the Navigate forwards out of the Breadcrumb navigation landmark task for JAWS (#803)', + updatedAt: '2022-08-10T18:44:16.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'horizontal-slider' + directory: 'breadcrumb' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '6', - title: 'Combobox with Both List and Inline Autocomplete Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '19', + title: 'Main Landmark', + phase: 'RD', + gitSha: 'c87a66ea13a2b6fac6d79fe1fb0b7a2f721dcd22', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create updated tests for APG design pattern example: Main landmark (#707)', + updatedAt: '2022-08-05T17:46:37.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'combobox-autocomplete-both-updated' + directory: 'main' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '8', - title: 'Command Button Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '24', + title: 'Meter', + phase: 'RD', + gitSha: '32d2d9db48becfc008fc566b569ac1563576ceb9', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create updated tests for APG design pattern example: Meter (#692)', + updatedAt: '2022-08-05T17:02:59.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'command-button' + directory: 'meter' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '9', - title: 'Complementary Landmark', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '32', + title: 'Switch Example', + phase: 'RD', + gitSha: '9d0e4e3d1040d64d9db69647e615c4ec0be723c2', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create updated tests for APG design pattern example: Switch (#691)', + updatedAt: '2022-08-05T16:13:44.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'complementary' + directory: 'switch' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '10', - title: 'Contentinfo Landmark', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '22', + title: 'Navigation Menu Button', + phase: 'RD', + gitSha: 'ecf05f484292189789f4db8b1ec41b19db38e567', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Tasks 4, 5 and 6: corrected link name "Navigate backwards from here" (#734)', + updatedAt: '2022-05-26T16:14:17.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'contentinfo' + directory: 'menu-button-navigation' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '25', - title: 'Data Grid Example 1: Minimal Data Grid', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '34', + title: 'Toggle Button', + phase: 'DRAFT', + gitSha: '022340081280b8cafb8ae0716a5b67e9ab942ef4', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Delete duplicated assertion for operating a not pressed togle button (VoiceOver) (#716)', + updatedAt: '2022-05-18T20:51:40.000Z', + draftPhaseReachedAt: '2022-07-06T00:00:00.000Z', + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'minimal-data-grid' + directory: 'toggle-button' }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '11', - title: 'Date Picker Spin Button Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + testPlanReports: [ + { + id: '1', + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [] + } + ] + }, + { + id: '8', + title: 'Command Button Example', + phase: 'RD', + gitSha: '0c466eec96c8cafc9961232c85e14758c4589525', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Fix navigation link positions in three test plans: link, command button and toggle button (#709)', + updatedAt: '2022-05-04T21:33:31.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'datepicker-spin-button' + directory: 'command-button' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '13', - title: 'Disclosure Navigation Menu Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '18', + title: 'Link Example 1 (span element with text content)', + phase: 'RD', + gitSha: '0c466eec96c8cafc9961232c85e14758c4589525', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Fix navigation link positions in three test plans: link, command button and toggle button (#709)', + updatedAt: '2022-05-04T21:33:31.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'disclosure-navigation' + directory: 'link-span-text' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '12', - title: 'Disclosure of Answers to Frequently Asked Questions Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '15', + title: 'Color Viewer Slider', + phase: 'RD', + gitSha: '1c6ef2fbef5fc056c622c802bebedaa14f2c8d40', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Create updated tests for APG design pattern example: Color Viewer Slider (#686)', + updatedAt: '2022-04-14T18:06:40.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'disclosure-faq' + directory: 'horizontal-slider' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '23', - title: 'Editor Menubar Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + id: '6', + title: 'Combobox with Both List and Inline Autocomplete Example', + phase: 'RD', + gitSha: '6b2cbcbdbd5f6867cd3c9e96362817c353335187', + gitMessage: "typo: double word 'the' (#595)", + updatedAt: '2022-03-29T16:02:56.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'menubar-editor' + directory: 'combobox-autocomplete-both-updated' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '14', - title: 'Form Landmark', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '21', + title: 'Action Menu Button Example Using aria-activedescendant', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'form' + directory: 'menu-button-actions-active-descendant' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '31', - title: 'Horizontal Multi-Thumb Slider', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '20', + title: 'Action Menu Button Example Using element.focus()', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'slider-multithumb' + directory: 'menu-button-actions' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '18', - title: 'Link Example 1 (span element with text content)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '2', + title: 'Banner Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'link-span-text' + directory: 'banner' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '17', - title: 'Link Example 2 (img element with alt attribute)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '4', + title: 'Checkbox Example (Two State)', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'link-img-alt' + directory: 'checkbox' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '16', - title: 'Link Example 3 (CSS :before content property on a span element)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '9', + title: 'Complementary Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'link-css' + directory: 'complementary' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '19', - title: 'Main Landmark', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '10', + title: 'Contentinfo Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'main' + directory: 'contentinfo' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '30', - title: 'Media Seek Slider', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '25', + title: 'Data Grid Example 1: Minimal Data Grid', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'seek-slider' + directory: 'minimal-data-grid' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '24', - title: 'Meter', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '11', + title: 'Date Picker Spin Button Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'meter' + directory: 'datepicker-spin-button' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '26', - title: 'Modal Dialog Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '12', + title: 'Disclosure of Answers to Frequently Asked Questions Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'modal-dialog' + directory: 'disclosure-faq' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '22', - title: 'Navigation Menu Button', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '23', + title: 'Editor Menubar Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'menu-button-navigation' + directory: 'menubar-editor' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '28', - title: 'Radio Group Example Using Roving tabindex', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '14', + title: 'Form Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'radiogroup-roving-tabindex' + directory: 'form' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { - id: '27', - title: 'Radio Group Example Using aria-activedescendant', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + id: '30', + title: 'Media Seek Slider', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { - directory: 'radiogroup-aria-activedescendant' + directory: 'seek-slider' }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { id: '29', title: 'Rating Slider', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { directory: 'rating-slider' }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '7', - title: 'Select Only Combobox Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'combobox-select-only' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '32', - title: 'Switch Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'switch' - }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { id: '33', title: 'Tabs with Manual Activation', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { directory: 'tabs-manual-activation' }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '34', - title: 'Toggle Button', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'toggle-button' - }, - updatedAt: '2023-04-10T18:22:22.000Z' + testPlanReports: [] }, { id: '35', title: 'Vertical Temperature Slider', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, testPlan: { directory: 'vertical-temperature-slider' }, - updatedAt: '2023-04-10T18:22:22.000Z' - } - ], - testPlanReports: [ - { - id: '2', - status: 'DRAFT', - at: { - id: '2', - name: 'NVDA' - }, - latestAtVersionReleasedAt: { - id: '2', - name: '2020.4', - releasedAt: '2021-02-19T05:00:00.000Z' - }, - browser: { - id: '1', - name: 'Firefox' - }, - testPlanVersion: { - id: '7', - title: 'Select Only Combobox Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'combobox-select-only' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - } - }, - { - id: '4', - status: 'CANDIDATE', - at: { - id: '2', - name: 'NVDA' - }, - latestAtVersionReleasedAt: { - id: '2', - name: '2020.4', - releasedAt: '2021-02-19T05:00:00.000Z' - }, - browser: { - id: '1', - name: 'Firefox' - }, - testPlanVersion: { - id: '26', - title: 'Modal Dialog Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'modal-dialog' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - } + testPlanReports: [] }, { - id: '3', - status: 'CANDIDATE', - at: { - id: '1', - name: 'JAWS' - }, - latestAtVersionReleasedAt: { - id: '1', - name: '2021.2111.13', - releasedAt: '2021-11-01T04:00:00.000Z' - }, - browser: { - id: '2', - name: 'Chrome' + id: '7', + title: 'Select Only Combobox Example', + phase: 'CANDIDATE', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + 'Generate html source script to support aria-at-app (#646)', + updatedAt: '2022-03-17T18:34:51.000Z', + draftPhaseReachedAt: '2022-07-06T00:00:00.000Z', + candidatePhaseReachedAt: '2023-08-03T20:22:51.263Z', + recommendedPhaseTargetDate: '2024-01-30T21:22:51.263Z', + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'combobox-select-only' }, - testPlanVersion: { - id: '26', - title: 'Modal Dialog Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'modal-dialog' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - } + testPlanReports: [ + { + id: '2', + markedFinalAt: '2023-08-03T20:20:48.535Z', + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [] + } + ] }, { - id: '1', - status: 'DRAFT', - at: { - id: '1', - name: 'JAWS' - }, - latestAtVersionReleasedAt: { - id: '1', - name: '2021.2111.13', - releasedAt: '2021-11-01T04:00:00.000Z' - }, - browser: { - id: '2', - name: 'Chrome' + id: '5', + title: 'Checkbox Example (Mixed-State)', + phase: 'RECOMMENDED', + gitSha: 'b3d0576a2901ea7f100f49a994b64edbecf81cff', + gitMessage: + 'Modify VoiceOver commands for task 7 (#842)', + updatedAt: '2022-10-24T21:33:12.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: '2022-07-06T00:00:00.000Z', + recommendedPhaseTargetDate: '2023-01-02T00:00:00.000Z', + recommendedPhaseReachedAt: '2023-01-03T00:00:00.000Z', + testPlan: { + directory: 'checkbox-tri-state' }, - testPlanVersion: { - id: '34', - title: 'Toggle Button', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'toggle-button' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - } + testPlanReports: [ + { + id: '6', + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [] + } + ] }, { - id: '6', - status: 'CANDIDATE', - at: { - id: '3', - name: 'VoiceOver for macOS' - }, - latestAtVersionReleasedAt: { - id: '3', - name: '11.6 (20G165)', - releasedAt: '2019-09-01T04:00:00.000Z' - }, - browser: { - id: '3', - name: 'Safari' + id: '26', + title: 'Modal Dialog Example', + phase: 'CANDIDATE', + gitSha: 'd0e16b42179de6f6c070da2310e99de837c71215', + gitMessage: + 'Delete down arrow command for navigating to the beginning of a dialog with JAWS and add the ESC command to exit forms or focus mode (#759)', + updatedAt: '2022-06-22T17:56:16.000Z', + draftPhaseReachedAt: null, + candidatePhaseReachedAt: '2022-07-06T00:00:00.000Z', + recommendedPhaseTargetDate: '2023-01-02T00:00:00.000Z', + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'modal-dialog' }, - testPlanVersion: { - id: '5', - title: 'Checkbox Example (Mixed-State)', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'checkbox-tri-state' + testPlanReports: [ + { + id: '5', + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [] }, - updatedAt: '2023-04-10T18:22:22.000Z' - } - }, - { - id: '5', - status: 'CANDIDATE', - at: { - id: '3', - name: 'VoiceOver for macOS' - }, - latestAtVersionReleasedAt: { - id: '3', - name: '11.6 (20G165)', - releasedAt: '2019-09-01T04:00:00.000Z' - }, - browser: { - id: '3', - name: 'Safari' - }, - testPlanVersion: { - id: '26', - title: 'Modal Dialog Example', - gitSha: '1768070bd68beefef29284b568d2da910b449c14', - gitMessage: - 'Remove Tab and Shift+Tab from radiogroup tests when navigating out of the start and end of a radio group (reading mode and VoiceOver only) (#928)', - testPlan: { - directory: 'modal-dialog' + { + id: '4', + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [] }, - updatedAt: '2023-04-10T18:22:22.000Z' - } + { + id: '3', + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [] + } + ] } ] } diff --git a/client/utils/aria.js b/client/utils/aria.js index eaf6450de..944ef376f 100644 --- a/client/utils/aria.js +++ b/client/utils/aria.js @@ -8,3 +8,16 @@ export const buildTestPageUrl = (gitSha, directory, testReferencePath) => { const BASE_PATH = '/aria-at'; return `${BASE_PATH}/${gitSha}/build/tests/${directory}/${testReferencePath}`; }; + +export const derivePhaseName = name => { + switch (name) { + case 'RD': + return 'R&D'; + case 'DRAFT': + return 'Draft'; + case 'CANDIDATE': + return 'Candidate'; + case 'RECOMMENDED': + return 'Recommended'; + } +}; diff --git a/client/utils/formatter.js b/client/utils/formatter.js index e53d59fd8..6be2f3c2d 100644 --- a/client/utils/formatter.js +++ b/client/utils/formatter.js @@ -42,3 +42,10 @@ export const convertStringFormatToAnotherFormat = ( export const isValidDate = (date, format = 'DD-MM-YYYY') => { return moment(date, format).isValid(); }; + +export const checkTimeBetweenDates = (date, otherDate, unit = 'days') => { + const _date = moment(date); + const _otherDate = moment(otherDate); + + return _date.diff(_otherDate, unit); +}; diff --git a/client/webpack.config.js b/client/webpack.config.js index b3b8fc590..68a8815e7 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -70,7 +70,7 @@ module.exports = { hot: 'only', proxy: [ { - context: ['/aria-at', '/api', '/embed'], + context: ['/aria-at', '/api', '/embed', '/test-review'], target: 'http://localhost:8000' } ] diff --git a/docs/database.md b/docs/database.md index 2804e1e00..0be2c999b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -73,6 +73,11 @@ The prerequisite for the following steps is SSH access to the production server. ``` psql -d aria_at_report -f _dump_.sql ``` +5. Run the migrations and seeders: + ``` + yarn sequelize db:migrate; + yarn sequelize db:seed:all; + ``` ## Test Database diff --git a/server/app.js b/server/app.js index 719a74662..7c4976bf0 100644 --- a/server/app.js +++ b/server/app.js @@ -4,6 +4,7 @@ const cacheMiddleware = require('apicache').middleware; const proxyMiddleware = require('rawgit/lib/middleware'); const { session } = require('./middleware/session'); const embedApp = require('./apps/embed'); +const testReviewApp = require('./apps/test-review'); const authRoutes = require('./routes/auth'); const testRoutes = require('./routes/tests'); const path = require('path'); @@ -21,7 +22,10 @@ apolloServer.start().then(() => { }); const listener = express(); -listener.use('/api', app).use('/embed', embedApp); +listener + .use('/api', app) + .use('/embed', embedApp) + .use('/test-review', testReviewApp); const baseUrl = 'https://raw.githubusercontent.com'; const onlyStatus200 = (req, res) => res.statusCode === 200; diff --git a/server/apps/embed.js b/server/apps/embed.js index 00091aff5..4531b58d7 100644 --- a/server/apps/embed.js +++ b/server/apps/embed.js @@ -9,8 +9,8 @@ const hash = require('object-hash'); const app = express(); const handlebarsPath = process.env.ENVIRONMENT === 'dev' || process.env.ENVIRONMENT === 'test' - ? 'handlebars' - : 'server/handlebars'; + ? 'handlebars/embed' + : 'server/handlebars/embed'; // handlebars const hbs = create({ @@ -44,10 +44,11 @@ const queryReports = async () => { name } } - testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) { + testPlanReports( + testPlanVersionPhases: [CANDIDATE, RECOMMENDED] + ) { id metrics - status at { id name @@ -64,6 +65,7 @@ const queryReports = async () => { testPlanVersion { id title + phase updatedAt testPlan { id @@ -185,16 +187,16 @@ const getLatestReportsForPattern = ({ allTestPlanReports, pattern }) => { }); const hasAnyCandidateReports = Object.values(reportsByAt).find(atReports => - atReports.find(report => report.status === 'CANDIDATE') + atReports.some(report => report.testPlanVersion.phase === 'CANDIDATE') ); - let status = hasAnyCandidateReports ? 'CANDIDATE' : 'RECOMMENDED'; + let phase = hasAnyCandidateReports ? 'CANDIDATE' : 'RECOMMENDED'; return { title, allBrowsers, allAtVersionsByAt, testPlanVersionIds, - status, + phase, reportsByAt }; }; @@ -212,7 +214,7 @@ const renderEmbed = ({ allBrowsers, allAtVersionsByAt, testPlanVersionIds, - status, + phase, reportsByAt } = getLatestReportsForPattern({ pattern, allTestPlanReports }); const allAtBrowserCombinations = Object.fromEntries( @@ -232,7 +234,7 @@ const renderEmbed = ({ allAtBrowserCombinations, title: queryTitle || title || 'Pattern Not Found', pattern, - status, + phase, allBrowsers, allAtVersionsByAt, reportsByAt, diff --git a/server/apps/test-review.js b/server/apps/test-review.js new file mode 100644 index 000000000..f6f59c442 --- /dev/null +++ b/server/apps/test-review.js @@ -0,0 +1,372 @@ +const express = require('express'); +const fs = require('fs'); +const fetch = require('node-fetch'); +const { resolve } = require('path'); +const moment = require('moment'); +const { create } = require('express-handlebars'); +const CommandsAPI = require('../handlebars/test-review/scripts/at-commands'); +const { + parseCommandCSVRow +} = require('../handlebars/test-review/scripts/parse-command-csv-row'); +const { + createCommandTuplesATModeTaskLookup +} = require('../handlebars/test-review/scripts/command-tuples-at-mode-task-lookup'); + +const app = express(); + +// handlebars setup +const handlebarsPath = + process.env.ENVIRONMENT === 'dev' || process.env.ENVIRONMENT === 'test' + ? 'handlebars/test-review' + : 'server/handlebars/test-review'; + +const hbs = create({ + layoutsDir: resolve(handlebarsPath, 'views/layouts'), + extname: 'hbs', + defaultLayout: 'index', + helpers: require(resolve(handlebarsPath, 'helpers')) +}); +app.engine('hbs', hbs.engine); +app.set('view engine', 'hbs'); +app.set('views', resolve(handlebarsPath, 'views')); + +// Prepare applicable format for ats.json +const atsJSONFilePath = + process.env.ENVIRONMENT === 'dev' || process.env.ENVIRONMENT === 'test' + ? 'resources/ats.json' + : 'server/resources/ats.json'; + +const ats = JSON.parse(fs.readFileSync(atsJSONFilePath, 'utf-8')); +for (const at of ats) { + if (at.name.includes('VoiceOver')) at.key = 'voiceover_macos'; + else at.key = at.name.toLowerCase(); +} + +const getCommitInfo = async commit => { + const OWNER = 'w3c'; + const REPO = 'aria-at'; + + let commitDate; + let commitMessage; + + try { + const response = await fetch( + `https://api.github.com/repos/${OWNER}/${REPO}/commits/${commit}` + ); + const data = await response.json(); + + if (data.commit) { + commitDate = data.commit.author.date; + commitMessage = data.commit.message; + } + } catch (error) { + console.error('Error:', error.message); + } + + return { commitDate, commitMessage }; +}; + +const csvToJSON = csvString => { + const lines = csvString.trim().split('\n'); + const headers = lines.shift().split(','); + const result = []; + + lines.forEach(line => { + const values = line.split(','); + const obj = {}; + let valueIndex = 0; + + headers.forEach(header => { + let value = values[valueIndex].trim(); + + if (value.startsWith('"') && !value.endsWith('"')) { + // If the value starts with a double quote but does not end with it, + // continue to concatenate values until the ending quote is found + while ( + valueIndex < values.length && + !values[valueIndex].endsWith('"') + ) { + valueIndex++; + value += `,${values[valueIndex].trim()}`; + } + } + + if (value.startsWith('"') && value.endsWith('"')) { + // If the value is enclosed in double quotes, remove the quotes + value = value.slice(1, -1); + } + + obj[header] = value; + valueIndex++; + }); + + result.push(obj); + }); + + return result; +}; + +const convertTextToHTML = text => { + const blocks = text.split('\n\n'); + let html = ''; + + for (let block of blocks) { + const lines = block.split('\n'); + if (lines[0].startsWith('*')) { + html += '
      '; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('*')) { + html += '
    • ' + line.substring(1).trim() + '
    • '; + } + } + html += '
    '; + } else { + html += '

    ' + lines.join('
    ') + '

    '; + } + } + + return html; +}; + +const generateTests = async (pattern, commit, commitDate) => { + const commandsCSVData = await fetch( + `https://raw.githubusercontent.com/w3c/aria-at/${commit}/tests/${pattern}/data/commands.csv` + ); + const commandsCSVText = await commandsCSVData.text(); + const commandsJSON = csvToJSON(commandsCSVText); + + const testsCSVData = await fetch( + `https://raw.githubusercontent.com/w3c/aria-at/${commit}/tests/${pattern}/data/tests.csv` + ); + const testsCSVText = await testsCSVData.text(); + const testsJSON = csvToJSON(testsCSVText); + + const referencesCSVData = await fetch( + `https://raw.githubusercontent.com/w3c/aria-at/${commit}/tests/${pattern}/data/references.csv` + ); + const referencesCSVText = await referencesCSVData.text(); + const referencesJSON = csvToJSON(referencesCSVText); + + const commandsParsed = commandsJSON.map(parseCommandCSVRow); + const commands = createCommandTuplesATModeTaskLookup(commandsParsed); + const commandsAPI = new CommandsAPI(commands, ats); + + const tests = []; + const scripts = []; + + let scriptNames = []; + testsJSON.forEach(testJSON => scriptNames.push(testJSON.setupScript)); + + scriptNames = scriptNames.filter( + (item, index) => scriptNames.indexOf(item) === index + ); + + for (const scriptName of scriptNames) { + let script = ''; + try { + const scriptData = await fetch( + `https://raw.githubusercontent.com/w3c/aria-at/${commit}/tests/${pattern}/data/js/${scriptName}.js` + ); + const scriptText = await scriptData.text(); + + const lines = scriptText.split(/\r?\n/); + lines.forEach(line => { + if (line.trim().length) script += '\t' + line.trim() + '\n'; + }); + } catch (err) { + console.error(err); + } + scripts.push( + `\t${scriptName}: function(testPageDocument){\n${script}}` + ); + } + + const allATKeys = ats.map(({ key }) => key); + const validAppliesTo = ['Screen Readers', 'Desktop Screen Readers'].concat( + allATKeys + ); + + const getAppliesToValues = values => { + const checkValue = value => { + let v1 = value.trim().toLowerCase(); + for (let i = 0; i < validAppliesTo.length; i++) { + let v2 = validAppliesTo[i]; + if (v1 === v2.toLowerCase()) { + return v2; + } + } + return false; + }; + + // check for individual assistive technologies + const items = values.split(','); + const newValues = []; + items.filter(item => { + const value = checkValue(item); + if (value) newValues.push(value); + }); + + return newValues; + }; + + const getFormattedAssertion = assertion => { + let level = '1'; + let str = assertion; + assertion = assertion.trim(); + + // matches a 'colon' when preceded by either of the digits 1 OR 2 (SINGLE CHARACTER), at the start of the string + let parts = assertion.split(/(?<=^[1-2]):/g); + + if (parts.length === 2) { + level = parts[0]; + str = parts[1].substring(0); + if (level !== '1' && level !== '2') level = '2'; + } + + if (assertion.length) return [level, str]; + }; + + const getPriorityString = function (priority) { + priority = parseInt(priority); + if (priority === 1) return 'required'; + else if (priority === 2) return 'optional'; + return ''; + }; + + // https://github.com/w3c/aria-at/commit/9d73d6bb274b3fe75b9a8825e020c0546a33a162 + // This is the date of the last commit before the build folder removal. + // Meant to support backward compatability until the existing tests can + // be updated to the current structure + const buildRemovalDate = new Date('2022-03-10 18:08:36.000000 +00:00'); + const useBuildInReferencePath = + new Date(commitDate).getTime() <= buildRemovalDate.getTime(); + + for (const testJSON of testsJSON) { + const { + task, + mode, + instructions, + setupScript, + setupScriptDescription + } = testJSON; + + const atTests = []; + + const appliesTo = getAppliesToValues(testJSON.appliesTo); + const allRelevantAts = + appliesTo[0].toLowerCase() === 'desktop screen readers' || + appliesTo[0].toLowerCase() === 'screen readers' + ? allATKeys + : appliesTo; + + const assertions = []; + + for (const key of Object.keys(testJSON)) { + if (key.includes('assertion')) { + if (testJSON[key]) + assertions.push(getFormattedAssertion(testJSON[key])); + } + } + + for (const atKey of allRelevantAts.map(a => a.toLowerCase())) { + let commands; + let at = commandsAPI.isKnownAT(atKey); + + try { + commands = commandsAPI.getATCommands(mode, task, at); + } catch (error) { + // An error will occur if there is no data for a screen reader, ignore it + // console.error('commandsAPI.getATCommands.error', error); + } + + atTests.push({ + atName: at.name, + atKey: at.key, + commands: commands && commands.length ? commands : undefined, + assertions: + assertions && assertions.length + ? assertions.map(a => ({ + priority: getPriorityString(a[0]), + description: a[1] + })) + : undefined, + userInstruction: instructions, + modeInstruction: commandsAPI.getModeInstructions(mode, at), + setupScriptDescription: setupScriptDescription + }); + } + + let helpLinks = []; + const getRef = refId => referencesJSON.find(ref => ref.refId === refId); + + helpLinks.push({ + link: getRef('example').value, + text: `APG example: ${pattern}.html` + }); + + for (const ref of testJSON.refs.split(' ')) { + if (ref) + helpLinks.push({ + link: getRef(ref).value, + text: `ARIA specification: ${ref}` + }); + } + + const referenceValue = getRef('reference').value; + + const reference = `/aria-at/${commit}/${ + useBuildInReferencePath ? 'build/tests/' : 'tests/' + }${pattern}/${ + setupScript + ? referenceValue.replace(/\.html$/, `.${setupScript}.html`) + : referenceValue + }`; + + tests.push({ + testNumber: tests.length + 1, + name: testJSON.title, + setupScriptName: setupScript, + allRelevantAts, + reference, + task, + mode, + atTests, + helpLinks + }); + } + + return { tests, scripts }; +}; + +app.get('/:commit/:pattern', async (req, res) => { + const commit = req.params.commit; + const pattern = req.params.pattern; + + const { commitDate, commitMessage } = await getCommitInfo(commit); + const { tests, scripts: setupScripts } = await generateTests( + pattern, + commit, + commitDate + ); + + const rendered = await hbs.renderView( + resolve(handlebarsPath, 'views/main.hbs'), + { + ats, + tests, + pattern, + setupScripts, + commitMessage: convertTextToHTML(commitMessage), + commitDate: moment(commitDate).format('YYYY.MM.DD') + } + ); + + // Disable browser-based caching which could potentially make the embed + // contents appear stale even after being refreshed + res.set('cache-control', 'must-revalidate').send(rendered); +}); + +app.use(express.static(resolve(`${handlebarsPath}/public`))); + +module.exports = app; diff --git a/server/graphql-schema.js b/server/graphql-schema.js index 1de34c296..d5dcf1779 100644 --- a/server/graphql-schema.js +++ b/server/graphql-schema.js @@ -87,6 +87,14 @@ const graphqlSchema = gql` The Ats which can be run with the specific browser, for example, Jaws can be run with Chrome but not Safari, and Safari works with VoiceOver only. """ ats: [At]! + """ + The Ats which are required to move a TestPlanVersion to CANDIDATE phase. + """ + candidateAts: [At]! + """ + The Ats which are required to move a TestPlanVersion to RECOMMENDED phase. + """ + recommendedAts: [At]! } """ @@ -153,6 +161,14 @@ const graphqlSchema = gql` The browsers which can run the At, for example, Safari can run VoiceOver but not Jaws because Jaws is Windows only. """ browsers: [Browser]! + """ + The browsers which are required to move a TestPlanVersion to CANDIDATE phase. + """ + candidateBrowsers: [Browser]! + """ + The browsers which are required to move a TestPlanVersion to RECOMMENDED phase. + """ + recommendedBrowsers: [Browser]! } """ @@ -188,28 +204,6 @@ const graphqlSchema = gql` releasedAt: Timestamp } - # TODO: Determine if needed following 2021 Working Mode changes - # https://github.com/w3c/aria-at/wiki/Working-Mode - # """ - # To make sure Test Plans are relevant, they need to be reviewed by community - # stakeholders such as AT vendors. - # """ - # enum TestPlanVersionStatus { - # """ - # Default value meaning the source has been imported and the test plan - # can be reviewed internally. - # """ - # DRAFT - # """ - # Receiving review from stakeholders. - # """ - # CANDIDATE - # """ - # Wide review phase complete and ready to record test results. - # """ - # RECOMMENDED - # } - """ A suite of tests which keeps its identity as it evolves over time. """ @@ -236,25 +230,11 @@ const graphqlSchema = gql` Gets the most recent version imported from the test plan's directory. """ latestTestPlanVersion: TestPlanVersion - # latestTestPlanVersion( - # # TODO: Waiting for TestPlanVersionStatus to be implemented - # status: TestPlanVersionStatus - # - # # TODO: determine if we need to filter test plans with no results - # testPlanReportStatuses: [TestPlanReportStatus] - # ): TestPlanVersion """ Gets all historic versions of the test plan. """ testPlanVersions: [TestPlanVersion]! - # testPlanVersions( - # # TODO: Waiting for TestPlanVersionStatus to be implemented - # status: TestPlanVersionStatus - # - # # TODO: determine if we need to filter test plans with no results - # testPlanReportStatuses: [TestPlanReportStatus] - # ): [TestPlanVersion]! } """ @@ -296,21 +276,16 @@ const graphqlSchema = gql` The title of the TestPlan at this point in time. """ title: String - # TODO: Waiting for TestPlanVersionStatus to be needed - # status: TestPlanVersionStatus! - - # TODO: decide whether to use this approach, since we think gitShas are - # a bit intimidating and hard to use. - # """ - # A version label set in the ARIA-AT repo. The app will only import new - # versions when that label has changed. - # """ - # label: String! """ - See TestPlanVersionStatus type for more information. + See TestPlanVersionPhase type for more information. """ phase: TestPlanVersionPhase! """ + Date of when the TestPlanVersion last updated to the 'Draft' + phase. + """ + draftPhaseReachedAt: Timestamp + """ Date of when the TestPlanVersion was last updated to the 'Candidate' phase. """ @@ -327,6 +302,10 @@ const graphqlSchema = gql` """ recommendedPhaseTargetDate: Timestamp """ + The date when the TestPlanVersion was deprecated. + """ + deprecatedAt: Timestamp + """ The TestPlan this TestPlanVersion is a snapshot of. """ testPlan: TestPlan! @@ -342,6 +321,7 @@ const graphqlSchema = gql` gitMessage: String! # TODO: remove if using version labels """ The date (originating in Git) corresponding to the Git sha's commit. + This can also be considered as the time for when R & D was complete """ updatedAt: Timestamp! # TODO: consider moving to the Scenario type if we support multiple @@ -362,9 +342,15 @@ const graphqlSchema = gql` tests: [Test]! """ The TestPlanReports attached to the TestPlanVersion. There will always - be a unique combination of AT + Browser + TestPlanVersion + be a unique combination of AT + Browser + TestPlanVersion. + + isFinal is used to check if a TestPlanReport has been "Marked as Final", + indicated by TestPlanReport.markedFinalAt existence. + None value indicates to return all. + True value indicates to return the reports which only have an markedFinalAt date. + False value indicates to return the reports which have no markedFinalAt date. """ - testPlanReports(isCurrentPhase: Boolean): [TestPlanReport]! + testPlanReports(isFinal: Boolean): [TestPlanReport]! } """ @@ -764,27 +750,6 @@ const graphqlSchema = gql` testResultsLength: Int! } - """ - The life-cycle of a TestPlanReport from the point it is created by an admin - until it is saved an available to the public on the reports page. - """ - enum TestPlanReportStatus { - """ - Accepting new TestPlanRuns from testers. - """ - DRAFT - """ - Testing is complete and consistent, and ready to be displayed in the - Candidate Tests and Reports section of the app. - """ - CANDIDATE - """ - Testing is complete and consistent, and ready to be displayed in the - Reports section of the app as being recommended. - """ - RECOMMENDED - } - """ Tests, as we envision them, should not leave any room for interpretation. If a conflict between results is found, the report cannot be published until @@ -850,10 +815,6 @@ const graphqlSchema = gql` """ id: ID! """ - See TestPlanReportStatus type for more information. - """ - status: TestPlanReportStatus! - """ The snapshot of a TestPlan to use. """ testPlanVersion: TestPlanVersion! @@ -889,7 +850,7 @@ const graphqlSchema = gql` Scenario if the output or unexpected behaviors do not match, or even at the level of an Assertion, if the result of an assertion does not match. - These conflicts must be resolved before the status can change from + These conflicts must be resolved before the TestPlanVersion phase can change from DRAFT to CANDIDATE. """ conflicts: [TestPlanReportConflict]! @@ -928,6 +889,16 @@ const graphqlSchema = gql` The point at which an admin created the TestPlanReport. """ createdAt: Timestamp! + """ + This is marked with the date when an admin has determined that all conflicts on the + TestPlanReport have been resolved and indicates that the TestPlanReport is ready + to be included when the entire TestPlanVersion is advanced to the "CANDIDATE" phase. + """ + markedFinalAt: Timestamp + """ + Indicated by TestPlanReport.markedFinalAt existence, after a report has been "marked as final". + """ + isFinal: Boolean! } """ @@ -1029,14 +1000,15 @@ const graphqlSchema = gql` testPlanVersion(id: ID): TestPlanVersion """ Load multiple TestPlanReports, with the optional ability to filter by - status, atId and testPlanVersionId. + TestPlanVersionPhase, atId, testPlanVersionId and if the report is marked as final. See TestPlanReport type for more information. """ testPlanReports( - statuses: [TestPlanReportStatus] + testPlanVersionPhases: [TestPlanVersionPhase] testPlanVersionId: ID testPlanVersionIds: [ID] atId: ID + isFinal: Boolean ): [TestPlanReport]! """ Get a TestPlanReport by ID. @@ -1122,10 +1094,16 @@ const graphqlSchema = gql` """ deleteTestPlanRun(userId: ID!): PopulatedData! """ - Update the report status. Remember that all conflicts must be resolved - when setting the status to CANDIDATE. Only available to admins. + Updates the markedFinalAt date. This must be set before a TestPlanReport can + be advanced to CANDIDATE. All conflicts must also be resolved. + Only available to admins. + """ + markAsFinal: PopulatedData! + """ + Remove the TestPlanReport's markedFinalAt date. This allows the TestPlanReport + to be worked on in the Test Queue page again if was previously marked as final. """ - updateStatus(status: TestPlanReportStatus!): PopulatedData! + unmarkAsFinal: PopulatedData! """ Update the report to a specific TestPlanVersion id. """ @@ -1136,12 +1114,6 @@ const graphqlSchema = gql` input: TestPlanReportInput! ): PopulatedData! """ - Update the report status for multiple TestPlanReports. Remember that all - conflicts must be resolved when setting the status to CANDIDATE. Only - available to admins. - """ - bulkUpdateStatus(status: TestPlanReportStatus!): [PopulatedData]! - """ Move the vendor review status from READY to IN PROGRESS or IN PROGRESS to APPROVED """ @@ -1159,12 +1131,13 @@ const graphqlSchema = gql` type TestPlanVersionOperations { """ Update the test plan version phase. Remember that all conflicts must be resolved - when setting the status to CANDIDATE. Only available to admins. + when setting the phase to CANDIDATE. Only available to admins. """ updatePhase( phase: TestPlanVersionPhase! candidatePhaseReachedAt: Timestamp recommendedPhaseTargetDate: Timestamp + testPlanVersionDataToIncludeId: ID ): PopulatedData! """ Update the test plan version recommended phase target date. @@ -1256,10 +1229,7 @@ const graphqlSchema = gql` Adds a report with the given TestPlanVersion, AT and Browser, and a state of "DRAFT", resulting in the report appearing in the Test Queue. In the case an identical report already exists, it will be returned - without changes and without affecting existing results. In the case an - identical report exists but with a status of "CANDIDATE" or "RECOMMENDED", - it will be given a status of "DRAFT" and will therefore be pulled back - into the queue with its results unaffected. + without changes and without affecting existing results. """ findOrCreateTestPlanReport( """ diff --git a/server/handlebars/helpers/index.js b/server/handlebars/embed/helpers/index.js similarity index 100% rename from server/handlebars/helpers/index.js rename to server/handlebars/embed/helpers/index.js diff --git a/server/handlebars/public/script.js b/server/handlebars/embed/public/script.js similarity index 100% rename from server/handlebars/public/script.js rename to server/handlebars/embed/public/script.js diff --git a/server/handlebars/public/style.css b/server/handlebars/embed/public/style.css similarity index 97% rename from server/handlebars/public/style.css rename to server/handlebars/embed/public/style.css index 113468f2d..9778cd559 100644 --- a/server/handlebars/public/style.css +++ b/server/handlebars/embed/public/style.css @@ -43,10 +43,10 @@ h3#report-title { text-align: center; } -#embed-report-status-container { +#embed-report-phase-container { margin-bottom: 1em; } -#embed-report-status-container summary:focus-visible { +#embed-report-phase-container summary:focus-visible { outline-offset: -2px; outline: 2px solid #3a86d1; } diff --git a/server/handlebars/views/layouts/index.hbs b/server/handlebars/embed/views/layouts/index.hbs similarity index 100% rename from server/handlebars/views/layouts/index.hbs rename to server/handlebars/embed/views/layouts/index.hbs diff --git a/server/handlebars/views/main.hbs b/server/handlebars/embed/views/main.hbs similarity index 98% rename from server/handlebars/views/main.hbs rename to server/handlebars/embed/views/main.hbs index 98e127d80..ca8459d4c 100644 --- a/server/handlebars/views/main.hbs +++ b/server/handlebars/embed/views/main.hbs @@ -7,8 +7,8 @@ {{/if}} {{#unless dataEmpty}} - {{#if (isCandidate status)}} -
    + {{#if (isCandidate phase)}} +
    Warning! Unapproved Report
    The information in this report is generated from candidate tests developed and run by the ARIA-AT Project. @@ -20,7 +20,7 @@
    {{else}} -
    +
    Recommended Report
    The information in this report is generated from recommended tests. diff --git a/server/handlebars/test-review/helpers/index.js b/server/handlebars/test-review/helpers/index.js new file mode 100644 index 000000000..5d5097338 --- /dev/null +++ b/server/handlebars/test-review/helpers/index.js @@ -0,0 +1,12 @@ +module.exports = { + arrayLength: function (array) { + if (!array) return 0; + if (!array.length) return 0; + return array.length; + }, + formatArrayJoinSeparator: function (array, separator) { + if (!array) return ''; + if (!array.length) return ''; + return array.join(separator); + } +}; diff --git a/server/handlebars/test-review/public/script.js b/server/handlebars/test-review/public/script.js new file mode 100644 index 000000000..ef457f607 --- /dev/null +++ b/server/handlebars/test-review/public/script.js @@ -0,0 +1,69 @@ +// eslint-disable-next-line no-unused-vars +class TestWindow { + /** + * Based on https://github.com/w3c/aria-at/blob/master/tests/resources/aria-at-test-window.mjs + * @param {object} options + * @param {Window | null} [options.window] + * @param {string} options.pageUri + * @param {TestWindowHooks} [options.hooks] + */ + constructor({ window = null, pageUri, hooks }) { + /** @type {Window | null} */ + this.window = window; + + /** @type {string} */ + this.pageUri = pageUri; + + /** @type {TestWindowHooks} */ + this.hooks = { + windowOpened: () => {}, + windowClosed: () => {}, + ...hooks + }; + } + + open() { + this.window = window.open( + this.pageUri, + '_blank', + 'toolbar=0,location=0,menubar=0,width=800,height=800' + ); + + this.hooks.windowOpened(); + + this.prepare(); + } + + prepare() { + if (!this.window) { + return; + } + + if (this.window.closed) { + this.window = undefined; + this.hooks.windowClosed(); + return; + } + + if ( + this.window.location.origin !== window.location.origin || // make sure the origin is the same, and prevent this from firing on an 'about' page + this.window.document.readyState !== 'complete' + ) { + window.setTimeout(() => { + this.prepare(); + }, 100); + return; + } + + // If the window is closed, re-enable open popup button + this.window.onunload = () => { + window.setTimeout(() => this.prepare(), 100); + }; + } + + close() { + if (this.window) { + this.window.close(); + } + } +} diff --git a/server/handlebars/test-review/public/style.css b/server/handlebars/test-review/public/style.css new file mode 100644 index 000000000..098879de2 --- /dev/null +++ b/server/handlebars/test-review/public/style.css @@ -0,0 +1,3 @@ +.commit-message { + margin: 0; +} diff --git a/server/handlebars/test-review/scripts/at-commands.js b/server/handlebars/test-review/scripts/at-commands.js new file mode 100644 index 000000000..5b1e62ad3 --- /dev/null +++ b/server/handlebars/test-review/scripts/at-commands.js @@ -0,0 +1,144 @@ +/** @deprecated See aria-at-test-io-format.mjs */ + +const commandsMap = require('../../../resources/commands.json'); + +/** + * Class for getting AT-specific instructions for a test against a design pattern. + * @deprecated See aria-at-test-io-format.mjs:CommandsInput + */ +module.exports = class CommandsAPI { + /** + * Creates an API to get AT-specific instructions for a design pattern. + * @param {object} commands - A data structure which is a nested object with the following format: + * { + * task: { + * mode: { + * at: [ + * key-command (string corresponding to export in keys.mjs), + * optional additional instructions to list after key command (string), + * ] + * } + * } + * } + */ + constructor(commands, ats) { + if (!commands) { + throw new Error( + 'You must initialize commandsAPI with a commands data object' + ); + } + + if (!ats) { + throw new Error( + 'You must initialize commandsAPI with a ats data object' + ); + } + + this.AT_COMMAND_MAP = commands; + + this.MODE_INSTRUCTIONS = { + reading: { + jaws: `Verify the Virtual Cursor is active by pressing ${this.commandString( + 'ALT_DELETE' + )}. If it is not, exit Forms Mode to activate the Virtual Cursor by pressing ${this.commandString( + 'ESC' + )}.`, + nvda: `Ensure NVDA is in browse mode by pressing ${this.commandString( + 'ESC' + )}. Note: This command has no effect if NVDA is already in browse mode.`, + voiceover_macos: `Toggle Quick Nav ON by pressing the ${this.commandString( + 'LEFT' + )} and ${this.commandString('RIGHT')} keys at the same time.` + }, + interaction: { + jaws: `Verify the PC Cursor is active by pressing ${this.commandString( + 'ALT_DELETE' + )}. If it is not, turn off the Virtual Cursor by pressing ${this.commandString( + 'INS_Z' + )}.`, + nvda: `If NVDA did not make the focus mode sound when the test page loaded, press ${this.commandString( + 'INS_SPACE' + )} to turn focus mode on.`, + voiceover_macos: `Toggle Quick Nav OFF by pressing the ${this.commandString( + 'LEFT' + )} and ${this.commandString('RIGHT')} keys at the same time.` + } + }; + + this.ats = ats; + } + + commandString(keyId) { + return commandsMap.find(command => command.id === keyId).text; + } + + /** + * Get AT-specific instruction + * @param {string} mode - The mode of the screen reader, "reading" or "interaction" + * @param {string} task - The task of the test. + * @param {string} assitiveTech - The assistive technology. + * @return {Array} - A list of commands (strings) + */ + getATCommands(mode, task, assistiveTech) { + if (!this.AT_COMMAND_MAP[task]) { + throw new Error( + `Task "${task}" does not exist, please add to at-commands or correct your spelling.` + ); + } else if (!this.AT_COMMAND_MAP[task][mode]) { + throw new Error( + `Mode "${mode}" instructions for task "${task}" does not exist, please add to at-commands or correct your spelling.` + ); + } + + let commandsData = + this.AT_COMMAND_MAP[task][mode][assistiveTech.key] || []; + let commands = []; + + for (let c of commandsData) { + let innerCommands = []; + let commandSequence = c[0].split(','); + for (let command of commandSequence) { + command = this.commandString(command); + if (typeof command === 'undefined') { + throw new Error( + `Key instruction identifier "${c}" for AT "${assistiveTech.name}", mode "${mode}", task "${task}" is not an available identified. Update you commands.json file to the correct identifier or add your identifier to resources/keys.mjs.` + ); + } + + let furtherInstruction = c[1]; + command = furtherInstruction + ? `${command} ${furtherInstruction}` + : command; + innerCommands.push(command); + } + commands.push(innerCommands.join(', then ')); + } + + return commands; + } + + /** + * Get AT-specific mode switching instructions + * @param {string} mode - The mode of the screen reader, "reading" or "interaction" + * @param {string} assistiveTech - The assistive technology. + * @return {string} - Instructions for switching into the correct mode. + */ + getModeInstructions(mode, assistiveTech) { + if ( + this.MODE_INSTRUCTIONS[mode] && + this.MODE_INSTRUCTIONS[mode][assistiveTech.key] + ) { + return this.MODE_INSTRUCTIONS[mode][assistiveTech.key]; + } + return ''; + } + + /** + * Get AT-specific instruction + * @param {string} at - an assitve technology with any capitalization + * @return {string} - if this API knows instructions for `at`, it will return the `at` with proper capitalization + */ + isKnownAT(at) { + return this.ats.find(o => o.key === at.toLowerCase()); + } +}; diff --git a/server/handlebars/test-review/scripts/command-tuples-at-mode-task-lookup.js b/server/handlebars/test-review/scripts/command-tuples-at-mode-task-lookup.js new file mode 100644 index 000000000..9b5d42329 --- /dev/null +++ b/server/handlebars/test-review/scripts/command-tuples-at-mode-task-lookup.js @@ -0,0 +1,38 @@ +/// +/// + +'use strict'; + +/** + * Create command lookup object and file. + * @param {AriaATValidated.Command[]} commands + * @returns {AriaATFile.CommandTuplesATModeTaskLookup} + */ +function createCommandTuplesATModeTaskLookup(commands) { + const data = commands.reduce((carry, command) => { + const commandTask = carry[command.task] || {}; + const commandTaskMode = commandTask[command.target.mode] || {}; + const commandTaskModeAT = commandTaskMode[command.target.at.key] || []; + const commandTuples = command.commands.map(({ id, extraInstruction }) => + extraInstruction ? [id, extraInstruction] : [id] + ); + return { + ...carry, + [command.task]: { + ...commandTask, + [command.target.mode]: { + ...commandTaskMode, + [command.target.at.key]: [ + ...commandTaskModeAT, + ...commandTuples + ] + } + } + }; + }, {}); + + return data; +} + +exports.createCommandTuplesATModeTaskLookup = + createCommandTuplesATModeTaskLookup; diff --git a/server/handlebars/test-review/scripts/parse-command-csv-row.js b/server/handlebars/test-review/scripts/parse-command-csv-row.js new file mode 100644 index 000000000..5a071af4e --- /dev/null +++ b/server/handlebars/test-review/scripts/parse-command-csv-row.js @@ -0,0 +1,70 @@ +/// +/// + +'use strict'; + +/** + * @param {AriaATCSV.Command} commandRow + * @returns {AriaATParsed.Command} + */ +function parseCommandCSVRow(commandRow) { + return { + testId: Number(commandRow.testId), + task: commandRow.task.replace(/[';]/g, '').trim().toLowerCase(), + target: { + at: { + key: commandRow.at.trim().toLowerCase(), + raw: commandRow.at + }, + mode: commandRow.mode.trim().toLowerCase() + }, + commands: [ + commandRow.commandA, + commandRow.commandB, + commandRow.commandC, + commandRow.commandD, + commandRow.commandE, + commandRow.commandF, + commandRow.commandG, + commandRow.commandH, + commandRow.commandI, + commandRow.commandJ, + commandRow.commandK, + commandRow.commandL, + commandRow.commandM, + commandRow.commandN, + commandRow.commandO, + commandRow.commandP, + commandRow.commandQ, + commandRow.commandR, + commandRow.commandS, + commandRow.commandT, + commandRow.commandU, + commandRow.commandV, + commandRow.commandW, + commandRow.commandX, + commandRow.commandY, + commandRow.commandZ + ] + .filter(Boolean) + .map(command => { + const paranIndex = command.indexOf('('); + if (paranIndex >= 0) { + return { + id: command.substring(0, paranIndex).trim(), + extraInstruction: command.substring(paranIndex).trim() + }; + } + return { + id: command.trim() + }; + }) + .map(({ id, ...rest }) => ({ + id, + keypresses: id.split(',').map(id => ({ id })), + ...rest + })) + }; +} + +exports.parseCommandCSVRow = parseCommandCSVRow; diff --git a/server/handlebars/test-review/views/layouts/index.hbs b/server/handlebars/test-review/views/layouts/index.hbs new file mode 100644 index 000000000..fc886f6bf --- /dev/null +++ b/server/handlebars/test-review/views/layouts/index.hbs @@ -0,0 +1,14 @@ + + + + + + Test plan review for pattern: {{pattern}} + + + + +{{{body}}} + + + diff --git a/server/handlebars/test-review/views/main.hbs b/server/handlebars/test-review/views/main.hbs new file mode 100644 index 000000000..3504edf0f --- /dev/null +++ b/server/handlebars/test-review/views/main.hbs @@ -0,0 +1,192 @@ + + + + + + Test plan review for pattern: {{pattern}} + + + + + +

    Test plan review for pattern: {{pattern}} ({{arrayLength tests}} tests)

    +

    Version created on: {{commitDate}}

    +

    Summary of commit:

    +{{{commitMessage}}} + +
    + Filter tests by assistive technology + + {{#each ats}} + + {{/each}} +
    + +{{#each tests}} +

    Test {{testNumber}}: {{{name}}}

    +
      +
    • Mode: {{mode}}
    • +
    • Applies to: {{formatArrayJoinSeparator allRelevantAts ", "}}
    • + + + + + + + + +
    • Relevant Specifications: +
        + {{#each helpLinks}} +
      • {{text}}
      • + {{/each}} +
      +
    • +
    + + + + {{#each atTests}} +
    +

    {{atName}}

    + {{#if setupScriptDescription}} +

    Scripted Instructions

    + The following instructions are executed by a script in the test page to initialize the widget: +
      +
    1. {{setupScriptDescription}}
    2. +
    + {{/if}} +

    Tester Instructions

    +
      +
    1. {{modeInstruction}}
    2. +
    3. {{userInstruction}} using the following commands: +
        + {{#each commands}} +
      • {{this}}
      • + {{/each}} + {{#unless commands}} +
      • No commands have been added for this test for {{atName}}. Add commands, or, update the "applies_to" to list only the relevant assistive technologies.
      • + {{/unless}} +
      +
    4. +
    +

    Assertions

    + + + {{#each assertions}} + + {{/each}} + {{#unless assertions}} + + {{/unless}} +
    PriorityAssertion
    {{priority}}{{description}}
    No assertions have been listed for {{atName}}. Add assertion or update the "applies_to" to list only the relevant assistive technologies.
    +
    + {{/each}} +{{/each}} + + + diff --git a/server/migrations/20230523163855-addColumnsToAtBrowsers.js b/server/migrations/20230523163855-addColumnsToAtBrowsers.js new file mode 100644 index 000000000..488fe7367 --- /dev/null +++ b/server/migrations/20230523163855-addColumnsToAtBrowsers.js @@ -0,0 +1,39 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'AtBrowsers', + 'isCandidate', + { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: false + }, + { transaction } + ); + + await queryInterface.addColumn( + 'AtBrowsers', + 'isRecommended', + { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: false + }, + { transaction } + ); + }); + }, + + async down(queryInterface /* , Sequelize */) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn('AtBrowsers', 'isCandidate', { + transaction + }); + await queryInterface.removeColumn('AtBrowsers', 'isRecommended', { + transaction + }); + }); + } +}; diff --git a/server/migrations/20230523163856-remove-OtherUnexpectedBehaviorText.js b/server/migrations/20230523163856-remove-OtherUnexpectedBehaviorText.js index 7ca15da17..1946ae140 100644 --- a/server/migrations/20230523163856-remove-OtherUnexpectedBehaviorText.js +++ b/server/migrations/20230523163856-remove-OtherUnexpectedBehaviorText.js @@ -7,6 +7,7 @@ const { const conflictsResolver = require('../resolvers/TestPlanReport/conflictsResolver'); const BrowserLoader = require('../models/loaders/BrowserLoader'); const AtLoader = require('../models/loaders/AtLoader'); +const { TEST_PLAN_REPORT_ATTRIBUTES } = require('../models/services/helpers'); module.exports = { up: queryInterface => { @@ -138,6 +139,12 @@ module.exports = { } } + // Exclude certain attributes called in testPlanReport query; + // needed to support future migrations + const testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES.filter( + e => !['markedFinalAt'].includes(e) + ); + for (let i = 0; i < testPlanReportsData.length; i++) { const testPlanReportId = testPlanReportsData[i].id; const status = testPlanReportsData[i].status; @@ -145,7 +152,7 @@ module.exports = { let updateParams = {}; const testPlanReport = await getTestPlanReportById( testPlanReportId, - null, + testPlanReportAttributes, null, ['id', 'tests'] ); diff --git a/server/migrations/20230608170853-addDateColumnsToTestPlanVersion.js b/server/migrations/20230608170853-addDateColumnsToTestPlanVersion.js index 72e34e4b9..7b1fa8d88 100644 --- a/server/migrations/20230608170853-addDateColumnsToTestPlanVersion.js +++ b/server/migrations/20230608170853-addDateColumnsToTestPlanVersion.js @@ -61,7 +61,7 @@ module.exports = { await queryInterface.addColumn( 'TestPlanVersion', - 'archivedAtDate', + 'deprecatedAt', { type: Sequelize.DataTypes.DATE, defaultValue: null, @@ -107,7 +107,7 @@ module.exports = { ); await queryInterface.removeColumn( 'TestPlanVersion', - 'archivedAtDate', + 'deprecatedAt', { transaction } diff --git a/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js b/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js index e24eed3d1..34a58ebd2 100644 --- a/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js +++ b/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js @@ -1,14 +1,93 @@ 'use strict'; -const { - updateTestPlanReportTestPlanVersion -} = require('../resolvers/TestPlanReportOperations'); const BrowserLoader = require('../models/loaders/BrowserLoader'); const AtLoader = require('../models/loaders/AtLoader'); +const { TEST_PLAN_REPORT_ATTRIBUTES } = require('../models/services/helpers'); +const scenariosResolver = require('../resolvers/Test/scenariosResolver'); +const { + getTestPlanReportById, + getOrCreateTestPlanReport, + updateTestPlanReport +} = require('../models/services/TestPlanReportService'); +const populateData = require('../services/PopulatedData/populateData'); +const { testResults } = require('../resolvers/TestPlanRun'); +const { + createTestPlanRun, + getTestPlanRunById +} = require('../models/services/TestPlanRunService'); +const { + findOrCreateTestResult +} = require('../resolvers/TestPlanRunOperations'); +const { + submitTestResult, + saveTestResult +} = require('../resolvers/TestResultOperations'); +const { hashTests } = require('../util/aria'); /** @type {import('sequelize-cli').Migration} */ module.exports = { - async up(queryInterface) { + async up(queryInterface, Sequelize) { + const atLoader = AtLoader(); + const browserLoader = BrowserLoader(); + + const compareTestContent = (currentTests, newTests) => { + const currentTestsByHash = hashTests(currentTests); + const newTestsByHash = hashTests(newTests); + + const testsToDelete = []; + const currentTestIdsToNewTestIds = {}; + Object.entries(currentTestsByHash).forEach( + ([hash, currentTest]) => { + const newTest = newTestsByHash[hash]; + if (!newTest) { + testsToDelete.push(currentTest); + return; + } + currentTestIdsToNewTestIds[currentTest.id] = newTest.id; + } + ); + + return { testsToDelete, currentTestIdsToNewTestIds }; + }; + + const copyTestResult = (testResultSkeleton, testResult) => { + return { + id: testResultSkeleton.id, + atVersionId: testResultSkeleton.atVersion.id, + browserVersionId: testResultSkeleton.browserVersion.id, + scenarioResults: testResultSkeleton.scenarioResults.map( + (scenarioResultSkeleton, index) => { + const scenarioResult = + testResult.scenarioResults[index]; + return { + id: scenarioResultSkeleton.id, + output: scenarioResult.output, + assertionResults: + scenarioResultSkeleton.assertionResults.map( + ( + assertionResultSkeleton, + assertionResultIndex + ) => { + const assertionResult = + scenarioResult.assertionResults[ + assertionResultIndex + ]; + return { + id: assertionResultSkeleton.id, + passed: assertionResult.passed, + failedReason: + assertionResult.failedReason + }; + } + ), + unexpectedBehaviors: + scenarioResult.unexpectedBehaviors + }; + } + ) + }; + }; + return queryInterface.sequelize.transaction(async transaction => { const testPlanReportsQuery = await queryInterface.sequelize.query( `select "TestPlanReport".id as "testPlanReportId", @@ -160,8 +239,346 @@ module.exports = { testPlanReportsByDirectory ); - const atLoader = AtLoader(); - const browserLoader = BrowserLoader(); + const updateTestPlanReportTestPlanVersion = async ({ + atId, + browserId, + testPlanReportId, + newTestPlanVersionId, + testPlanReportAttributes + }) => { + const context = { + atLoader, + browserLoader, + user: { + roles: [{ name: 'ADMIN' }] + } + }; + + // [SECTION START]: Preparing data to be worked with in a similar way to TestPlanUpdaterModal + const newTestPlanVersionQuery = + await queryInterface.sequelize.query( + `SELECT + * + FROM "TestPlanVersion" + WHERE id = ?;`, + { + replacements: [newTestPlanVersionId], + type: Sequelize.QueryTypes.SELECT, + transaction + } + ); + + const newTestPlanVersionData = newTestPlanVersionQuery[0]; + const newTestPlanVersion = { + id: newTestPlanVersionData.id, + tests: newTestPlanVersionData.tests.map( + ({ + assertions, + atMode, + atIds, + id, + scenarios, + title + }) => { + return { + id, + title, + ats: atIds.map(atId => ({ + id: atId + })), + atMode, + scenarios: scenariosResolver( + { scenarios }, + { atId } + ).map(({ commandIds }) => { + return { + commands: commandIds.map(commandId => ({ + text: commandId + })) + }; + }), + assertions: assertions.map( + ({ priority, text }) => ({ + priority, + text + }) + ) + }; + } + ) + }; + + const currentTestPlanReport = await getTestPlanReportById( + testPlanReportId, + testPlanReportAttributes + ); + + for ( + let i = 0; + i < currentTestPlanReport.testPlanRuns.length; + i++ + ) { + const testPlanRunId = + currentTestPlanReport.testPlanRuns[i].id; + const testPlanRun = await getTestPlanRunById( + testPlanRunId, + null, + null, + testPlanReportAttributes + ); + // testPlanReport = testPlanRun?.testPlanReport; + + testPlanRun.testResults = await testResults( + testPlanRun, + null, + context + ); + + if (!currentTestPlanReport.draftTestPlanRuns) + currentTestPlanReport.draftTestPlanRuns = []; + currentTestPlanReport.draftTestPlanRuns[i] = testPlanRun; + } + + const skeletonTestPlanReport = { + id: currentTestPlanReport.id, + draftTestPlanRuns: + currentTestPlanReport.draftTestPlanRuns.map( + ({ testResults, tester }) => ({ + tester: { + id: tester.id, + username: tester.username + }, + testResults: testResults.map( + ({ + atVersion, + browserVersion, + completedAt, + scenarioResults, + test + }) => { + return { + test: { + id: test.id, + title: test.title, + ats: test.ats.map(({ id }) => ({ + id + })), + atMode: test.atMode, + scenarios: scenariosResolver( + { + scenarios: + test.scenarios + }, + { atId } + ).map(({ commandIds }) => { + return { + commands: + commandIds.map( + commandId => ({ + text: commandId + }) + ) + }; + }), + assertions: test.assertions.map( + ({ priority, text }) => ({ + priority, + text + }) + ) + }, + atVersion: { id: atVersion.id }, + browserVersion: { + id: browserVersion.id + }, + completedAt, + scenarioResults: + scenarioResults.map( + ({ + output, + assertionResults, + unexpectedBehaviors + }) => ({ + output, + assertionResults: + assertionResults.map( + ({ + failedReason, + passed + }) => ({ + passed, + failedReason + }) + ), + unexpectedBehaviors: + unexpectedBehaviors?.map( + ({ + id, + otherUnexpectedBehaviorText + }) => ({ + id, + otherUnexpectedBehaviorText + }) + ) + }) + ) + }; + } + ) + }) + ) + }; + + let runsWithResults, + allTestResults, + copyableTestResults, + testsToDelete, + currentTestIdsToNewTestIds; + + runsWithResults = + skeletonTestPlanReport.draftTestPlanRuns.filter( + testPlanRun => testPlanRun.testResults.length + ); + + allTestResults = runsWithResults.flatMap( + testPlanRun => testPlanRun.testResults + ); + + // eslint-disable-next-line no-unused-vars + ({ testsToDelete, currentTestIdsToNewTestIds } = + compareTestContent( + allTestResults.map(testResult => testResult.test), + newTestPlanVersion.tests + )); + + // eslint-disable-next-line no-unused-vars + copyableTestResults = allTestResults.filter( + testResult => currentTestIdsToNewTestIds[testResult.test.id] + ); + // [SECTION END]: Preparing data to be worked with in a similar way to TestPlanUpdaterModal + + // TODO: If no input.testPlanVersionId, infer it by whatever the latest is for this directory + const [foundOrCreatedTestPlanReport, createdLocationsOfData] = + await getOrCreateTestPlanReport( + { + testPlanVersionId: newTestPlanVersionId, + atId, + browserId + }, + testPlanReportAttributes + ); + + const candidatePhaseReachedAt = + currentTestPlanReport.candidatePhaseReachedAt; + const recommendedPhaseReachedAt = + currentTestPlanReport.recommendedPhaseReachedAt; + const recommendedPhaseTargetDate = + currentTestPlanReport.recommendedPhaseTargetDate; + const vendorReviewStatus = + currentTestPlanReport.vendorReviewStatus; + + await updateTestPlanReport( + foundOrCreatedTestPlanReport.id, + { + candidatePhaseReachedAt, + recommendedPhaseReachedAt, + recommendedPhaseTargetDate, + vendorReviewStatus + }, + testPlanReportAttributes + ); + + // const locationOfData = { + // testPlanReportId: foundOrCreatedTestPlanReport.id + // }; + const preloaded = { + testPlanReport: foundOrCreatedTestPlanReport + }; + + const created = await Promise.all( + createdLocationsOfData.map(createdLocationOfData => + populateData(createdLocationOfData, { + preloaded, + context + }) + ) + ); + const reportIsNew = !!created.find( + item => item.testPlanReport.id + ); + if (!reportIsNew) + // eslint-disable-next-line no-console + console.info( + 'A report already exists and continuing will overwrite its data.' + ); + + for (const testPlanRun of runsWithResults) { + // Create new TestPlanRuns + const { id: testPlanRunId } = await createTestPlanRun( + { + testPlanReportId: foundOrCreatedTestPlanReport.id, + testerUserId: testPlanRun.tester.id + }, + null, + null, + testPlanReportAttributes + ); + + for (const testResult of testPlanRun.testResults) { + const testId = + currentTestIdsToNewTestIds[testResult.test.id]; + const atVersionId = testResult.atVersion.id; + const browserVersionId = testResult.browserVersion.id; + if (!testId) continue; + + // Create new testResults + const { testResult: testResultSkeleton } = + await findOrCreateTestResult( + { + parentContext: { id: testPlanRunId } + }, + { testId, atVersionId, browserVersionId }, + context + ); + + const copiedTestResultInput = copyTestResult( + testResultSkeleton, + testResult + ); + + let savedData; + if (testResult.completedAt) { + savedData = await submitTestResult( + { + parentContext: { + id: copiedTestResultInput.id + } + }, + { input: copiedTestResultInput }, + context + ); + } else { + savedData = await saveTestResult( + { + parentContext: { + id: copiedTestResultInput.id + } + }, + { input: copiedTestResultInput }, + context + ); + } + if (savedData.errors) + console.error('savedData.errors', savedData.errors); + } + } + + // TODO: Delete the old TestPlanReport? + // await removeTestPlanRunByQuery({ testPlanReportId }); + // await removeTestPlanReport(testPlanReportId); + // return populateData(locationOfData, { preloaded, context }); + }; + for (let i = 0; i < Object.values(highestVersions).length; i++) { const highestTestPlanVersion = Object.values(highestVersions)[i]; @@ -210,27 +627,16 @@ module.exports = { console.info( `=== Updating testPlanReportId ${uniqueMatch.testPlanReportId} to testPlanVersionId ${highestTestPlanVersionId} for atId ${uniqueMatch.atId} and browserId ${uniqueMatch.browserId} ===` ); - await updateTestPlanReportTestPlanVersion( - { - parentContext: { - id: uniqueMatch.testPlanReportId - } - }, - { - input: { - testPlanVersionId: highestTestPlanVersionId, - atId: uniqueMatch.atId, - browserId: uniqueMatch.browserId - } - }, - { - atLoader, - browserLoader, - user: { - roles: [{ name: 'ADMIN' }] - } - } - ); + await updateTestPlanReportTestPlanVersion({ + atId: uniqueMatch.atId, + browserId: uniqueMatch.browserId, + testPlanReportId: uniqueMatch.testPlanReportId, + newTestPlanVersionId: highestTestPlanVersionId, + testPlanReportAttributes: + TEST_PLAN_REPORT_ATTRIBUTES.filter( + e => !['markedFinalAt'].includes(e) + ) + }); } } } diff --git a/server/migrations/20230614004831-removeDateColumnsFromTestPlanReportAndRenameCandidateStatusReachedAtToApprovedAt.js b/server/migrations/20230614004831-removeDateColumnsFromTestPlanReportAndRenameCandidateStatusReachedAtToApprovedAt.js index 59b1d5288..713e51bb3 100644 --- a/server/migrations/20230614004831-removeDateColumnsFromTestPlanReportAndRenameCandidateStatusReachedAtToApprovedAt.js +++ b/server/migrations/20230614004831-removeDateColumnsFromTestPlanReportAndRenameCandidateStatusReachedAtToApprovedAt.js @@ -7,7 +7,7 @@ module.exports = { await queryInterface.renameColumn( 'TestPlanReport', 'candidateStatusReachedAt', - 'approvedAt', + 'markedFinalAt', { transaction } ); await queryInterface.removeColumn( @@ -31,7 +31,7 @@ module.exports = { return queryInterface.sequelize.transaction(async transaction => { await queryInterface.renameColumn( 'TestPlanReport', - 'approvedAt', + 'markedFinalAt', 'candidateStatusReachedAt', { transaction } ); diff --git a/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js b/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js new file mode 100644 index 000000000..8ed875cc0 --- /dev/null +++ b/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js @@ -0,0 +1,74 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.changeColumn( + 'TestPlanVersion', + 'phase', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'RD' + }, + { transaction } + ); + + // Need to get all the testPlanVersions that don't have testPlanReports attached to them + // to consider them as just being R&D Complete, but not yet in the DRAFT phase + const testPlanVersions = await queryInterface.sequelize.query( + `SELECT + "TestPlanVersion".id, + "TestPlanVersion"."updatedAt", + CASE + WHEN COUNT("TestPlanReport".id) = 0 THEN false + ELSE true + END AS "hasTestPlanReport" + FROM "TestPlanVersion" + LEFT JOIN "TestPlanReport" ON "TestPlanVersion".id = "TestPlanReport"."testPlanVersionId" + GROUP BY "TestPlanVersion".id;`, + { + type: Sequelize.QueryTypes.SELECT, + transaction + } + ); + + for (const testPlanVersion of testPlanVersions) { + const { id, updatedAt, hasTestPlanReport } = testPlanVersion; + + if (hasTestPlanReport) + await queryInterface.sequelize.query( + `UPDATE "TestPlanVersion" SET "draftPhaseReachedAt" = ? WHERE id = ?`, + { + replacements: [updatedAt, id], + transaction + } + ); + else + await queryInterface.sequelize.query( + `UPDATE "TestPlanVersion" SET phase = ? WHERE id = ?`, + { + replacements: ['RD', id], + transaction + } + ); + } + }); + }, + + async down(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.changeColumn( + 'TestPlanVersion', + 'phase', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'DRAFT' + }, + { transaction } + ); + }); + } +}; diff --git a/server/migrations/20230719174358-removeTestPlanReportStatus.js b/server/migrations/20230719174358-removeTestPlanReportStatus.js new file mode 100644 index 000000000..95242544d --- /dev/null +++ b/server/migrations/20230719174358-removeTestPlanReportStatus.js @@ -0,0 +1,27 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn('TestPlanReport', 'status', { + transaction + }); + }); + }, + + async down(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'TestPlanReport', + 'status', + { + type: Sequelize.DataTypes.TEXT, + defaultValue: 'DRAFT', + allowNull: false + }, + { transaction } + ); + }); + } +}; diff --git a/server/models/AtBrowsers.js b/server/models/AtBrowsers.js index b6a984ca3..0a1e3d9d1 100644 --- a/server/models/AtBrowsers.js +++ b/server/models/AtBrowsers.js @@ -19,6 +19,14 @@ module.exports = function (sequelize, DataTypes) { model: 'Browser', key: 'id' } + }, + isCandidate: { + type: DataTypes.BOOLEAN, + allowNull: false + }, + isRecommended: { + type: DataTypes.BOOLEAN, + allowNull: false } }, { diff --git a/server/models/TestPlanReport.js b/server/models/TestPlanReport.js index 84362f292..504355cb0 100644 --- a/server/models/TestPlanReport.js +++ b/server/models/TestPlanReport.js @@ -1,9 +1,4 @@ const MODEL_NAME = 'TestPlanReport'; -const STATUS = { - DRAFT: 'DRAFT', - CANDIDATE: 'CANDIDATE', - RECOMMENDED: 'RECOMMENDED' -}; module.exports = function (sequelize, DataTypes) { const Model = sequelize.define( @@ -15,11 +10,6 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, autoIncrement: true }, - status: { - type: DataTypes.TEXT, - allowNull: false, - defaultValue: STATUS.DRAFT - }, testPlanVersionId: { type: DataTypes.INTEGER }, testPlanId: { type: DataTypes.INTEGER }, atId: { type: DataTypes.INTEGER }, @@ -37,6 +27,11 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.JSONB, defaultValue: {}, allowNull: false + }, + markedFinalAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true } }, { @@ -45,10 +40,6 @@ module.exports = function (sequelize, DataTypes) { } ); - Model.DRAFT = STATUS.DRAFT; - Model.CANDIDATE = STATUS.CANDIDATE; - Model.RECOMMENDED = STATUS.RECOMMENDED; - Model.TEST_PLAN_VERSION_ASSOCIATION = { foreignKey: 'testPlanVersionId' }; Model.AT_ASSOCIATION = { foreignKey: 'atId' }; diff --git a/server/models/TestPlanVersion.js b/server/models/TestPlanVersion.js index 955dd87af..a82ae854f 100644 --- a/server/models/TestPlanVersion.js +++ b/server/models/TestPlanVersion.js @@ -1,5 +1,6 @@ const MODEL_NAME = 'TestPlanVersion'; const PHASE = { + RD: 'RD', DRAFT: 'DRAFT', CANDIDATE: 'CANDIDATE', RECOMMENDED: 'RECOMMENDED' @@ -18,7 +19,7 @@ module.exports = function (sequelize, DataTypes) { phase: { type: DataTypes.TEXT, allowNull: false, - defaultValue: PHASE.DRAFT + defaultValue: PHASE.RD }, title: { type: DataTypes.TEXT }, directory: { type: DataTypes.TEXT }, @@ -37,6 +38,11 @@ module.exports = function (sequelize, DataTypes) { tests: { type: DataTypes.JSONB }, testPlanId: { type: DataTypes.INTEGER }, metadata: { type: DataTypes.JSONB }, + draftPhaseReachedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + }, candidatePhaseReachedAt: { type: DataTypes.DATE, defaultValue: null, @@ -51,6 +57,11 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DATE, defaultValue: null, allowNull: true + }, + deprecatedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true } }, { diff --git a/server/models/loaders/AtLoader.js b/server/models/loaders/AtLoader.js index b1ad5b335..5a2754ce6 100644 --- a/server/models/loaders/AtLoader.js +++ b/server/models/loaders/AtLoader.js @@ -17,6 +17,21 @@ const AtLoader = () => { ats = await activePromise; + // Sort date of atVersions subarray in desc order by releasedAt date + ats.forEach(item => + item.atVersions.sort((a, b) => b.releasedAt - a.releasedAt) + ); + + ats = ats.map(at => ({ + ...at.dataValues, + candidateBrowsers: at.browsers.filter( + browser => browser.AtBrowsers.isCandidate + ), + recommendedBrowsers: at.browsers.filter( + browser => browser.AtBrowsers.isRecommended + ) + })); + return ats; } }; diff --git a/server/models/loaders/BrowserLoader.js b/server/models/loaders/BrowserLoader.js index 0c58d7c26..76fdfd3aa 100644 --- a/server/models/loaders/BrowserLoader.js +++ b/server/models/loaders/BrowserLoader.js @@ -18,6 +18,16 @@ const BrowserLoader = () => { browsers = await activePromise; + browsers = browsers.map(browser => ({ + ...browser.dataValues, + candidateAts: browser.ats.filter( + at => at.AtBrowsers.isCandidate + ), + recommendedAts: browser.ats.filter( + at => at.AtBrowsers.isRecommended + ) + })); + return browsers; } }; diff --git a/server/models/services/TestPlanReportService.js b/server/models/services/TestPlanReportService.js index 2d1825319..b8fad1ebf 100644 --- a/server/models/services/TestPlanReportService.js +++ b/server/models/services/TestPlanReportService.js @@ -194,7 +194,7 @@ const getTestPlanReports = async ( * @returns {Promise<*>} */ const createTestPlanReport = async ( - { status, testPlanVersionId, atId, browserId }, + { testPlanVersionId, atId, browserId }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -209,7 +209,6 @@ const createTestPlanReport = async ( const testPlanReportResult = await ModelService.create( TestPlanReport, { - status, testPlanVersionId, atId, browserId, @@ -249,11 +248,11 @@ const createTestPlanReport = async ( const updateTestPlanReport = async ( id, { - status, metrics, testPlanTargetId, testPlanVersionId, - vendorReviewStatus + vendorReviewStatus, + markedFinalAt }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, @@ -267,11 +266,11 @@ const updateTestPlanReport = async ( TestPlanReport, { id }, { - status, metrics, testPlanTargetId, testPlanVersionId, - vendorReviewStatus + vendorReviewStatus, + markedFinalAt }, options ); @@ -316,7 +315,6 @@ const removeTestPlanReport = async ( */ const getOrCreateTestPlanReport = async ( { testPlanVersionId, atId, browserId }, - { status } = {}, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -332,8 +330,16 @@ const getOrCreateTestPlanReport = async ( create: createTestPlanReport, update: updateTestPlanReport, values: { testPlanVersionId, atId, browserId }, - updateValues: { status }, - returnAttributes: [null, [], [], [], [], [], [], []] + returnAttributes: [ + testPlanReportAttributes, + [], + [], + [], + [], + [], + [], + [] + ] } ], { transaction: options.transaction } @@ -353,6 +359,22 @@ const getOrCreateTestPlanReport = async ( { transaction: options.transaction } ); + // If a TestPlanReport is being intentionally created that was previously marked as final, + // This will allow it to be displayed in the Test Queue again to be worked on + if (!isNewTestPlanReport && testPlanReport.markedFinalAt) { + await updateTestPlanReport( + testPlanReportId, + { markedFinalAt: null }, + testPlanReportAttributes, + testPlanRunAttributes, + testPlanVersionAttributes, + atAttributes, + browserAttributes, + userAttributes, + { transaction: options.transaction } + ); + } + const created = isNewTestPlanReport ? [{ testPlanReportId }] : []; return [testPlanReport, created]; diff --git a/server/models/services/TestPlanVersionService.js b/server/models/services/TestPlanVersionService.js index 6b8000e84..e9ac56854 100644 --- a/server/models/services/TestPlanVersionService.js +++ b/server/models/services/TestPlanVersionService.js @@ -263,9 +263,11 @@ const updateTestPlanVersion = async ( updatedAt, metadata, tests, + draftPhaseReachedAt, candidatePhaseReachedAt, recommendedPhaseReachedAt, - recommendedPhaseTargetDate + recommendedPhaseTargetDate, + deprecatedAt }, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, @@ -289,9 +291,11 @@ const updateTestPlanVersion = async ( updatedAt, metadata, tests, + draftPhaseReachedAt, candidatePhaseReachedAt, recommendedPhaseReachedAt, - recommendedPhaseTargetDate + recommendedPhaseTargetDate, + deprecatedAt }, options ); diff --git a/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js b/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js index 084ea3556..89cb7ccdb 100644 --- a/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js +++ b/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js @@ -6,11 +6,7 @@ const deepCustomMerge = require('../../util/deepCustomMerge'); * merged because each run might have skipped different tests. */ const finalizedTestResultsResolver = async (testPlanReport, _, context) => { - if ( - // CANDIDATE & RECOMMENDED status should be evaluated - testPlanReport.status === 'DRAFT' || - !testPlanReport.testPlanRuns.length - ) { + if (!testPlanReport.testPlanRuns.length) { return null; } diff --git a/server/resolvers/TestPlanReport/index.js b/server/resolvers/TestPlanReport/index.js index 71fcb5a9e..7f714a3bf 100644 --- a/server/resolvers/TestPlanReport/index.js +++ b/server/resolvers/TestPlanReport/index.js @@ -9,6 +9,7 @@ const atVersions = require('./atVersionsResolver'); const at = require('./atResolver'); const browser = require('./browserResolver'); const latestAtVersionReleasedAt = require('./latestAtVersionReleasedAtResolver'); +const isFinal = require('./isFinalResolver'); module.exports = { runnableTests, @@ -21,5 +22,6 @@ module.exports = { atVersions, at, browser, - latestAtVersionReleasedAt + latestAtVersionReleasedAt, + isFinal }; diff --git a/server/resolvers/TestPlanReport/isFinalResolver.js b/server/resolvers/TestPlanReport/isFinalResolver.js new file mode 100644 index 000000000..cdc06d380 --- /dev/null +++ b/server/resolvers/TestPlanReport/isFinalResolver.js @@ -0,0 +1,5 @@ +const isFinalResolver = async testPlanReport => { + return !!testPlanReport.markedFinalAt; +}; + +module.exports = isFinalResolver; diff --git a/server/resolvers/TestPlanReportOperations/bulkUpdateStatusResolver.js b/server/resolvers/TestPlanReportOperations/bulkUpdateStatusResolver.js deleted file mode 100644 index 877f295de..000000000 --- a/server/resolvers/TestPlanReportOperations/bulkUpdateStatusResolver.js +++ /dev/null @@ -1,45 +0,0 @@ -const { AuthenticationError } = require('apollo-server'); -const updateStatusResolver = require('./updateStatusResolver'); -const conflictsResolver = require('../TestPlanReport/conflictsResolver'); -const { - getTestPlanReportById -} = require('../../models/services/TestPlanReportService'); - -const bulkUpdateStatusResolver = async ( - { parentContext: { ids } }, - { status }, - context -) => { - const { user } = context; - if (!user?.roles.find(role => role.name === 'ADMIN')) { - throw new AuthenticationError(); - } - - let populateDataResultArray = []; - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - - const testPlanReport = await getTestPlanReportById(id); - const conflicts = await conflictsResolver( - testPlanReport, - null, - context - ); - if (conflicts.length > 0) { - throw new Error( - `Cannot update test plan report due to conflicts with the ${testPlanReport.at.name} report.` - ); - } - - const result = await updateStatusResolver( - { parentContext: { id } }, - { status }, - context - ); - populateDataResultArray.push(result); - } - - return populateDataResultArray; -}; - -module.exports = bulkUpdateStatusResolver; diff --git a/server/resolvers/TestPlanReportOperations/index.js b/server/resolvers/TestPlanReportOperations/index.js index 9a730e246..bb33b0145 100644 --- a/server/resolvers/TestPlanReportOperations/index.js +++ b/server/resolvers/TestPlanReportOperations/index.js @@ -1,7 +1,7 @@ const assignTester = require('./assignTesterResolver'); const deleteTestPlanRun = require('./deleteTestPlanRunResolver'); -const updateStatus = require('./updateStatusResolver'); -const bulkUpdateStatus = require('./bulkUpdateStatusResolver'); +const markAsFinal = require('./markAsFinalResolver'); +const unmarkAsFinal = require('./unmarkAsFinalResolver'); const deleteTestPlanReport = require('./deleteTestPlanReportResolver'); const promoteVendorReviewStatus = require('./promoteVendorReviewStatusResolver'); const updateTestPlanReportTestPlanVersion = require('./updateTestPlanReportTestPlanVersionResolver'); @@ -9,8 +9,8 @@ const updateTestPlanReportTestPlanVersion = require('./updateTestPlanReportTestP module.exports = { assignTester, deleteTestPlanRun, - updateStatus, - bulkUpdateStatus, + markAsFinal, + unmarkAsFinal, deleteTestPlanReport, promoteVendorReviewStatus, updateTestPlanReportTestPlanVersion diff --git a/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js b/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js new file mode 100644 index 000000000..56ba497e3 --- /dev/null +++ b/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js @@ -0,0 +1,47 @@ +const { AuthenticationError } = require('apollo-server'); +const { + getTestPlanReportById, + updateTestPlanReport +} = require('../../models/services/TestPlanReportService'); +const finalizedTestResultsResolver = require('../TestPlanReport/finalizedTestResultsResolver'); +const populateData = require('../../services/PopulatedData/populateData'); +const conflictsResolver = require('../TestPlanReport/conflictsResolver'); + +const markAsFinalResolver = async ( + { parentContext: { id: testPlanReportId } }, + _, + context +) => { + const { user } = context; + + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + const testPlanReport = await getTestPlanReportById(testPlanReportId); + + const conflicts = await conflictsResolver(testPlanReport, null, context); + if (conflicts.length > 0) { + throw new Error( + 'Cannot mark test plan report as final due to conflicts' + ); + } + + const finalizedTestResults = await finalizedTestResultsResolver( + testPlanReport, + null, + context + ); + if (!finalizedTestResults || !finalizedTestResults.length) { + throw new Error( + 'Cannot mark test plan report as final because there are no ' + + 'completed test results' + ); + } + + await updateTestPlanReport(testPlanReportId, { markedFinalAt: new Date() }); + + return populateData({ testPlanReportId }, { context }); +}; + +module.exports = markAsFinalResolver; diff --git a/server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js b/server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js new file mode 100644 index 000000000..6fcf0ebbe --- /dev/null +++ b/server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js @@ -0,0 +1,23 @@ +const { AuthenticationError } = require('apollo-server'); +const { + updateTestPlanReport +} = require('../../models/services/TestPlanReportService'); +const populateData = require('../../services/PopulatedData/populateData'); + +const unmarkAsFinalResolver = async ( + { parentContext: { id: testPlanReportId } }, + _, + context +) => { + const { user } = context; + + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + await updateTestPlanReport(testPlanReportId, { markedFinalAt: null }); + + return populateData({ testPlanReportId }, { context }); +}; + +module.exports = unmarkAsFinalResolver; diff --git a/server/resolvers/TestPlanReportOperations/updateStatusResolver.js b/server/resolvers/TestPlanReportOperations/updateStatusResolver.js deleted file mode 100644 index 2a151ccaa..000000000 --- a/server/resolvers/TestPlanReportOperations/updateStatusResolver.js +++ /dev/null @@ -1,88 +0,0 @@ -const { AuthenticationError } = require('apollo-server'); -const { - getTestPlanReportById, - updateTestPlanReport -} = require('../../models/services/TestPlanReportService'); -const conflictsResolver = require('../TestPlanReport/conflictsResolver'); -const finalizedTestResultsResolver = require('../TestPlanReport/finalizedTestResultsResolver'); -const runnableTestsResolver = require('../TestPlanReport/runnableTestsResolver'); -const populateData = require('../../services/PopulatedData/populateData'); -const getMetrics = require('../../util/getMetrics'); -const { updatePhase } = require('../TestPlanVersionOperations'); - -const updateStatusResolver = async ( - { parentContext: { id: testPlanReportId } }, - { status }, - context -) => { - const { user } = context; - if (!user?.roles.find(role => role.name === 'ADMIN')) { - throw new AuthenticationError(); - } - - const testPlanReport = await getTestPlanReportById(testPlanReportId); - const runnableTests = runnableTestsResolver(testPlanReport); - - // Params to be updated on TestPlanReport - let updateParams = { status }; - - if (status !== 'DRAFT') { - const conflicts = await conflictsResolver( - testPlanReport, - null, - context - ); - if (conflicts.length > 0) { - throw new Error('Cannot update test plan report due to conflicts'); - } - } - - if (status === 'CANDIDATE' || status === 'RECOMMENDED') { - const finalizedTestResults = await finalizedTestResultsResolver( - { - ...testPlanReport, - status - }, - null, - context - ); - - if (!finalizedTestResults || !finalizedTestResults.length) { - throw new Error( - 'Cannot update test plan report because there are no ' + - 'completed test results' - ); - } - - const metrics = getMetrics({ - testPlanReport: { - ...testPlanReport, - finalizedTestResults, - runnableTests - } - }); - - if (status === 'CANDIDATE') { - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics }, - vendorReviewStatus: 'READY' - }; - } else if (status === 'RECOMMENDED') { - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics } - }; - } - } - await updateTestPlanReport(testPlanReportId, updateParams); - await updatePhase( - { parentContext: { id: testPlanReport.testPlanVersionId } }, - { phase: status, changePhaseWithoutForcingTestPlanReportsUpdate: true }, - context - ); - - return populateData({ testPlanReportId }, { context }); -}; - -module.exports = updateStatusResolver; diff --git a/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js b/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js index 459fbbb86..883325846 100644 --- a/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js +++ b/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js @@ -1,5 +1,6 @@ const hash = require('object-hash'); const { omit } = require('lodash'); +const { AuthenticationError } = require('apollo-server-express'); const { getTestPlanReportById, getOrCreateTestPlanReport, @@ -16,7 +17,6 @@ const { } = require('../../models/services/TestPlanRunService'); const { findOrCreateTestResult } = require('../TestPlanRunOperations'); const { submitTestResult, saveTestResult } = require('../TestResultOperations'); -const { AuthenticationError } = require('apollo-server-express'); const compareTestContent = (currentTests, newTests) => { const hashTest = test => hash(omit(test, ['id'])); @@ -244,10 +244,7 @@ const updateTestPlanReportTestPlanVersionResolver = async ( // TODO: If no input.testPlanVersionId, infer it by whatever the latest is for this directory const [foundOrCreatedTestPlanReport, createdLocationsOfData] = - await getOrCreateTestPlanReport(input, { - // TODO: Pass a boolean on taking the current testPlanReport's status or use DRAFT - status: currentTestPlanReport.status - }); + await getOrCreateTestPlanReport(input); const candidatePhaseReachedAt = currentTestPlanReport.candidatePhaseReachedAt; diff --git a/server/resolvers/TestPlanVersion/testPlanReportsResolver.js b/server/resolvers/TestPlanVersion/testPlanReportsResolver.js index 6933c9cb4..810cbbf18 100644 --- a/server/resolvers/TestPlanVersion/testPlanReportsResolver.js +++ b/server/resolvers/TestPlanVersion/testPlanReportsResolver.js @@ -3,17 +3,30 @@ const { } = require('../../models/services/TestPlanReportService'); const testPlanReportsResolver = async ( - { id: testPlanVersionId, phase }, - { isCurrentPhase } + { id: testPlanVersionId }, + { isFinal } ) => { const where = { testPlanVersionId }; - if (isCurrentPhase) where.status = phase; - return getTestPlanReports(null, where, null, null, null, null, null, null, { - order: [['createdAt', 'desc']] - }); + const reports = await getTestPlanReports( + null, + where, + null, + null, + null, + null, + null, + null, + { + order: [['createdAt', 'desc']] + } + ); + + if (isFinal === undefined) return reports; + else if (isFinal) return reports.filter(report => !!report.markedFinalAt); + else if (!isFinal) return reports.filter(report => !report.markedFinalAt); }; module.exports = testPlanReportsResolver; diff --git a/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js index 370b3c020..0ce3b99b9 100644 --- a/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js +++ b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js @@ -1,7 +1,8 @@ const { AuthenticationError } = require('apollo-server'); const { updateTestPlanReport, - getTestPlanReports + getTestPlanReports, + getOrCreateTestPlanReport } = require('../../models/services/TestPlanReportService'); const conflictsResolver = require('../TestPlanReport/conflictsResolver'); const finalizedTestResultsResolver = require('../TestPlanReport/finalizedTestResultsResolver'); @@ -9,10 +10,20 @@ const runnableTestsResolver = require('../TestPlanReport/runnableTestsResolver') const recommendedPhaseTargetDateResolver = require('../TestPlanVersion/recommendedPhaseTargetDateResolver'); const populateData = require('../../services/PopulatedData/populateData'); const getMetrics = require('../../util/getMetrics'); +const { hashTest } = require('../../util/aria'); const { getTestPlanVersionById, updateTestPlanVersion } = require('../../models/services/TestPlanVersionService'); +const { + createTestPlanRun, + updateTestPlanRun +} = require('../../models/services/TestPlanRunService'); +const { + createTestResultId, + createScenarioResultId, + createAssertionResultId +} = require('../../services/PopulatedData/locationOfDataId'); const updatePhaseResolver = async ( { parentContext: { id: testPlanVersionId } }, @@ -20,9 +31,7 @@ const updatePhaseResolver = async ( phase, candidatePhaseReachedAt, recommendedPhaseTargetDate, - // TODO: The following flag will be unnecessary once the application no longer allows - // individual TestPlanReport updating of phase - changePhaseWithoutForcingTestPlanReportsUpdate = false + testPlanVersionDataToIncludeId }, context ) => { @@ -31,14 +40,44 @@ const updatePhaseResolver = async ( throw new AuthenticationError(); } + let testPlanVersionDataToInclude; + let testPlanReportsDataToIncludeId = []; + + // The testPlanVersion being updated const testPlanVersion = await getTestPlanVersionById(testPlanVersionId); - // Move only the TestPlanReports which also have the same phase as the TestPlanVersion + // These checks are needed to support the test plan version reports being updated with earlier + // versions' data + if (testPlanVersionDataToIncludeId) { + testPlanVersionDataToInclude = await getTestPlanVersionById( + testPlanVersionDataToIncludeId + ); + + const whereTestPlanVersion = { + testPlanVersionId: testPlanVersionDataToIncludeId + }; + + testPlanReportsDataToIncludeId = await getTestPlanReports( + null, + whereTestPlanVersion, + null, + null, + null, + null, + null, + null, + { + order: [['createdAt', 'desc']] + } + ); + } + + // The test plan reports which will be updated + let testPlanReports; const whereTestPlanVersion = { testPlanVersionId }; - whereTestPlanVersion.status = testPlanVersion.phase; - const testPlanReports = await getTestPlanReports( + testPlanReports = await getTestPlanReports( null, whereTestPlanVersion, null, @@ -52,88 +91,334 @@ const updatePhaseResolver = async ( } ); - // Params to be updated on TestPlanVersion (or TestPlanReports) - let updateParams = { phase, status: phase }; + // If there is an earlier version that for this phase and that version has some test plan runs + // in the test queue, this will run the process for updating existing test plan versions for the + // test plan version and preserving data for tests that have not changed. + if (testPlanReportsDataToIncludeId.length) { + for (const testPlanReportDataToInclude of testPlanReportsDataToIncludeId) { + // Verify the combination does not exist + if ( + !testPlanReports.some( + ({ atId, browserId }) => + atId === testPlanReportDataToInclude.atId && + browserId === testPlanReportDataToInclude.browserId + ) + ) { + // Then this combination needs to be considered if the tests are not different + // between versions + let keptTestIds = {}; + for (const testPlanVersionTest of testPlanVersion.tests) { + const testHash = hashTest(testPlanVersionTest); - if (!changePhaseWithoutForcingTestPlanReportsUpdate) { - for (let key in testPlanReports) { - const testPlanReport = testPlanReports[key]; - const testPlanReportId = testPlanReport.id; + if (keptTestIds[testHash]) continue; - // const testPlanReport = await getTestPlanReportById(testPlanReportId); - const runnableTests = runnableTestsResolver(testPlanReport); + for (const testPlanVersionDataToIncludeTest of testPlanVersionDataToInclude.tests) { + const testDataToIncludeHash = hashTest( + testPlanVersionDataToIncludeTest + ); - if (phase !== 'DRAFT') { - const conflicts = await conflictsResolver( - testPlanReport, - null, - context - ); - if (conflicts.length > 0) { - throw new Error( - 'Cannot update test plan report due to conflicts' - ); + if (testHash === testDataToIncludeHash) { + if (!keptTestIds[testHash]) + keptTestIds[testHash] = { + testId: testPlanVersionTest.id, + testDataToIncludeId: + testPlanVersionDataToIncludeTest.id + }; + } + } + } + + for (const testPlanRun of testPlanReportDataToInclude.testPlanRuns) { + const testResultsToSave = {}; + for (const testResult of testPlanRun.testResults) { + // Check if the testId referenced also matches the hash on any in the + // keptTestIds + Object.keys(keptTestIds).forEach(key => { + const { testId, testDataToIncludeId } = + keptTestIds[key]; + + if (testDataToIncludeId === testResult.testId) { + // Then this data should be preserved + testResultsToSave[testId] = testResult; + } else { + // TODO: Track which tests cannot be preserved + } + }); + } + + if (Object.keys(testResultsToSave).length) { + const [createdTestPlanReport] = + await getOrCreateTestPlanReport({ + testPlanVersionId, + atId: testPlanReportDataToInclude.atId, + browserId: testPlanReportDataToInclude.browserId + }); + + const createdTestPlanRun = await createTestPlanRun({ + testerUserId: testPlanRun.testerUserId, + testPlanReportId: createdTestPlanReport.id + }); + + const testResults = []; + for (const testResultToSaveTestId of Object.keys( + testResultsToSave + )) { + const foundKeptTest = testPlanVersion.tests.find( + test => test.id === testResultToSaveTestId + ); + + let testResultToSave = + testResultsToSave[testResultToSaveTestId]; + + // Updating testResult id references + const testResultId = createTestResultId( + createdTestPlanRun.id, + testResultToSaveTestId + ); + + testResultToSave.testId = testResultToSaveTestId; + testResultToSave.id = testResultId; + + // The hash confirms the sub-arrays should be in the same order, and + // regenerate the test result related ids for the carried over data + testResultToSave.scenarioResults.forEach( + (eachScenarioResult, scenarioIndex) => { + eachScenarioResult.scenarioId = + foundKeptTest.scenarios.filter( + scenario => + scenario.atId === + testPlanReportDataToInclude.atId + )[scenarioIndex].id; + + // Update eachScenarioResult.id + const scenarioResultId = + createScenarioResultId( + testResultId, + eachScenarioResult.scenarioId + ); + eachScenarioResult.id = scenarioResultId; + + eachScenarioResult.assertionResults.forEach( + ( + eachAssertionResult, + assertionIndex + ) => { + eachAssertionResult.assertionId = + foundKeptTest.assertions[ + assertionIndex + ].id; + + // Update eachAssertionResult.id + eachAssertionResult.id = + createAssertionResultId( + scenarioResultId, + eachAssertionResult.assertionId + ); + } + ); + } + ); + + testResults.push(testResultToSave); + } + + await updateTestPlanRun(createdTestPlanRun.id, { + testResults + }); + } } } + } - if (phase === 'CANDIDATE' || phase === 'RECOMMENDED') { - const finalizedTestResults = await finalizedTestResultsResolver( - { - ...testPlanReport, - phase, - status: phase - }, - null, - context - ); + testPlanReports = await getTestPlanReports( + null, + whereTestPlanVersion, + null, + null, + null, + undefined, + undefined, + null, + { + order: [['createdAt', 'desc']] + } + ); + } + + if ( + testPlanReports.length === 0 && + (phase === 'CANDIDATE' || phase === 'RECOMMENDED') + ) { + // Stop update if no testPlanReports were found + throw new Error('No test plan reports found.'); + } + + if ( + !testPlanReports.some(({ markedFinalAt }) => markedFinalAt) && + (phase === 'CANDIDATE' || phase === 'RECOMMENDED') + ) { + // Do not update phase if no reports marked as final were found + throw new Error('No reports have been marked as final.'); + } + + if (phase === 'CANDIDATE' || phase === 'RECOMMENDED') { + const reportsByAtAndBrowser = {}; + + testPlanReports.forEach(testPlanReport => { + const { at, browser } = testPlanReport; + if (!reportsByAtAndBrowser[at.id]) { + reportsByAtAndBrowser[at.id] = {}; + } - if (!finalizedTestResults || !finalizedTestResults.length) { - throw new Error( - 'Cannot update test plan report because there are no ' + - 'completed test results' + reportsByAtAndBrowser[at.id][browser.id] = testPlanReport; + }); + + const ats = await context.atLoader.getAll(); + + const missingAtBrowserCombinations = []; + + ats.forEach(at => { + const browsers = + phase === 'CANDIDATE' + ? at.candidateBrowsers + : at.recommendedBrowsers; + browsers.forEach(browser => { + if (!reportsByAtAndBrowser[at.id]?.[browser.id]) { + missingAtBrowserCombinations.push( + `${at.name} and ${browser.name}` ); } + }); + }); - const metrics = getMetrics({ - testPlanReport: { - ...testPlanReport, - finalizedTestResults, - runnableTests - } - }); - - if (phase === 'CANDIDATE') { - const candidatePhaseReachedAtValue = candidatePhaseReachedAt - ? candidatePhaseReachedAt - : new Date(); - const recommendedPhaseTargetDateValue = - recommendedPhaseTargetDate - ? recommendedPhaseTargetDate - : recommendedPhaseTargetDateResolver({ - candidatePhaseReachedAt - }); - - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics }, - candidatePhaseReachedAt: candidatePhaseReachedAtValue, - recommendedPhaseTargetDate: - recommendedPhaseTargetDateValue, - vendorReviewStatus: 'READY' - }; - } else if (phase === 'RECOMMENDED') { - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics }, - recommendedPhaseReachedAt: new Date() - }; + if (missingAtBrowserCombinations.length) { + throw new Error( + `Cannot set phase to ${phase.toLowerCase()} because the following` + + ` required reports have not been collected:` + + ` ${missingAtBrowserCombinations.join(', ')}.` + ); + } + } + + for (const testPlanReport of testPlanReports) { + const runnableTests = runnableTestsResolver(testPlanReport); + let updateParams = {}; + + if (phase === 'DRAFT') { + const conflicts = await conflictsResolver( + testPlanReport, + null, + context + ); + await updateTestPlanReport(testPlanReport.id, { + metrics: { + ...testPlanReport.metrics, + conflictsCount: conflicts.length + }, + markedFinalAt: null + }); + } + + if (phase === 'CANDIDATE' || phase === 'RECOMMENDED') { + const conflicts = await conflictsResolver( + testPlanReport, + null, + context + ); + if (conflicts.length > 0) { + throw new Error( + 'Cannot update test plan report due to conflicts' + ); + } + + const finalizedTestResults = await finalizedTestResultsResolver( + { + ...testPlanReport + }, + null, + context + ); + + if (!finalizedTestResults || !finalizedTestResults.length) { + throw new Error( + 'Cannot update test plan report because there are no ' + + 'completed test results' + ); + } + + const metrics = getMetrics({ + testPlanReport: { + ...testPlanReport, + finalizedTestResults, + runnableTests } + }); + + if (phase === 'CANDIDATE') { + updateParams = { + ...updateParams, + metrics: { ...testPlanReport.metrics, ...metrics }, + vendorReviewStatus: 'READY' + }; + } else if (phase === 'RECOMMENDED') { + updateParams = { + ...updateParams, + metrics: { ...testPlanReport.metrics, ...metrics } + }; } - await updateTestPlanReport(testPlanReportId, updateParams); } + await updateTestPlanReport(testPlanReport.id, updateParams); } - await updateTestPlanVersion(testPlanVersionId, updateParams); + let updateParams = { phase }; + if (phase === 'RD') + updateParams = { + ...updateParams, + draftPhaseReachedAt: null, + candidatePhaseReachedAt: null, + recommendedPhaseReachedAt: null, + recommendedPhaseTargetDate: null, + deprecatedAt: null + }; + else if (phase === 'DRAFT') + updateParams = { + ...updateParams, + draftPhaseReachedAt: new Date(), + candidatePhaseReachedAt: null, + recommendedPhaseReachedAt: null, + recommendedPhaseTargetDate: null, + deprecatedAt: null + }; + else if (phase === 'CANDIDATE') { + const candidatePhaseReachedAtValue = + candidatePhaseReachedAt || new Date(); + const recommendedPhaseTargetDateValue = + recommendedPhaseTargetDate || + recommendedPhaseTargetDateResolver({ + candidatePhaseReachedAt: candidatePhaseReachedAtValue + }); + updateParams = { + ...updateParams, + candidatePhaseReachedAt: candidatePhaseReachedAtValue, + recommendedPhaseReachedAt: null, + recommendedPhaseTargetDate: recommendedPhaseTargetDateValue, + deprecatedAt: null + }; + } else if (phase === 'RECOMMENDED') + updateParams = { + ...updateParams, + recommendedPhaseReachedAt: new Date(), + deprecatedAt: null + }; + + // If testPlanVersionDataToIncludeId's results are being used to update this earlier version, + // deprecate it + if (testPlanVersionDataToIncludeId) + await updateTestPlanVersion(testPlanVersionDataToIncludeId, { + deprecatedAt: new Date() + }); + + await updateTestPlanVersion(testPlanVersionId, updateParams); return populateData({ testPlanVersionId }, { context }); }; diff --git a/server/resolvers/atsResolver.js b/server/resolvers/atsResolver.js index 9dc381527..0f32869f4 100644 --- a/server/resolvers/atsResolver.js +++ b/server/resolvers/atsResolver.js @@ -1,22 +1,5 @@ -const { getAts } = require('../models/services/AtService'); - -const atsResolver = async () => { - const ats = await getAts( - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - { - order: [['name', 'asc']] - } - ); - // Sort date of atVersions subarray in desc order by releasedAt date - ats.forEach(item => - item.atVersions.sort((a, b) => b.releasedAt - a.releasedAt) - ); - return ats; +const atsResolver = async (_, __, context) => { + return context.atLoader.getAll(); }; module.exports = atsResolver; diff --git a/server/resolvers/browsersResolver.js b/server/resolvers/browsersResolver.js index 3ebe6ae78..14e694fbe 100644 --- a/server/resolvers/browsersResolver.js +++ b/server/resolvers/browsersResolver.js @@ -1,9 +1,5 @@ -const { getBrowsers } = require('../models/services/BrowserService'); - -const browsersResolver = () => { - return getBrowsers(undefined, undefined, undefined, undefined, undefined, { - order: [['name', 'asc']] - }); +const browsersResolver = async (_, __, context) => { + return context.browserLoader.getAll(); }; module.exports = browsersResolver; diff --git a/server/resolvers/findOrCreateTestPlanReportResolver.js b/server/resolvers/findOrCreateTestPlanReportResolver.js index 19fa566cb..8e5f5179e 100644 --- a/server/resolvers/findOrCreateTestPlanReportResolver.js +++ b/server/resolvers/findOrCreateTestPlanReportResolver.js @@ -11,7 +11,7 @@ const findOrCreateTestPlanReportResolver = async (_, { input }, context) => { } const [testPlanReport, createdLocationsOfData] = - await getOrCreateTestPlanReport(input, { status: 'DRAFT' }); + await getOrCreateTestPlanReport(input); const locationOfData = { testPlanReportId: testPlanReport.id }; const preloaded = { testPlanReport }; diff --git a/server/resolvers/helpers/deriveAttributesFromCustomField.js b/server/resolvers/helpers/deriveAttributesFromCustomField.js index 2151c94c5..89711a635 100644 --- a/server/resolvers/helpers/deriveAttributesFromCustomField.js +++ b/server/resolvers/helpers/deriveAttributesFromCustomField.js @@ -53,7 +53,7 @@ const deriveAttributesFromCustomField = (modelName, customFields) => { if (fields.includes('browser')) derived.push('browserId'); if (fields.includes('testPlanVersion')) derived.push('testPlanVersionId'); - if (fields.includes('finalizedTestResults')) derived.push('status'); + if (fields.includes('isFinal')) derived.push('markedFinalAt'); break; } case 'draftTestPlanRuns': { diff --git a/server/resolvers/testPlanReportsResolver.js b/server/resolvers/testPlanReportsResolver.js index 86f8d8db2..fcbcae476 100644 --- a/server/resolvers/testPlanReportsResolver.js +++ b/server/resolvers/testPlanReportsResolver.js @@ -10,12 +10,17 @@ const { const testPlanReportsResolver = async ( _, - { statuses, testPlanVersionId, testPlanVersionIds, atId }, + { + testPlanVersionPhases = [], + testPlanVersionId, + testPlanVersionIds, + atId, + isFinal + }, context, info ) => { const where = {}; - if (statuses) where.status = statuses; if (testPlanVersionId) where.testPlanVersionId = testPlanVersionId; if (testPlanVersionIds) where.testPlanVersionId = testPlanVersionIds; if (atId) where.atId = atId; @@ -45,7 +50,17 @@ const testPlanReportsResolver = async ( if (testPlanReportRawAttributes.includes('conflictsLength')) testPlanReportAttributes.push('metrics'); - return getTestPlanReports( + if (isFinal === undefined) { + // Do nothing + } else testPlanReportAttributes.push('markedFinalAt'); + + if ( + testPlanVersionPhases.length && + !testPlanVersionAttributes.includes('phase') + ) + testPlanVersionAttributes.push('phase'); + + let testPlanReports = await getTestPlanReports( null, where, testPlanReportAttributes, @@ -56,6 +71,23 @@ const testPlanReportsResolver = async ( null, { order: [['createdAt', 'desc']] } ); + + if (isFinal === undefined) { + // Do nothing + } else if (isFinal) + testPlanReports = testPlanReports.filter( + report => !!report.markedFinalAt + ); + else if (!isFinal) + testPlanReports = testPlanReports.filter( + report => !report.markedFinalAt + ); + + if (testPlanVersionPhases.length) { + return testPlanReports.filter(testPlanReport => + testPlanVersionPhases.includes(testPlanReport.testPlanVersion.phase) + ); + } else return testPlanReports; }; module.exports = testPlanReportsResolver; diff --git a/server/scripts/populate-test-data/index.js b/server/scripts/populate-test-data/index.js index 133280392..7d368a667 100644 --- a/server/scripts/populate-test-data/index.js +++ b/server/scripts/populate-test-data/index.js @@ -102,6 +102,42 @@ const populateTestDatabase = async () => { 'completeAndPassing' ]); + await populateFakeTestResults(8, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(9, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(10, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(11, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(12, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(13, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + console.info( 'Successfully populated. Please wait a moment for the process to close.' ); diff --git a/server/scripts/populate-test-data/pg_dump_2021_05_test_data.sql b/server/scripts/populate-test-data/pg_dump_2021_05_test_data.sql index 47669255e..a6aced823 100644 --- a/server/scripts/populate-test-data/pg_dump_2021_05_test_data.sql +++ b/server/scripts/populate-test-data/pg_dump_2021_05_test_data.sql @@ -68,20 +68,29 @@ INSERT INTO "AtMode" ("atId", name) VALUES (3, 'INTERACTION'); -- Data for Name: TestPlanReport; Type: TABLE DATA; Schema: public; Owner: atr -- -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (1, 'DRAFT', get_test_plan_version_id(text 'Toggle Button'), '2021-05-14 14:18:23.602-05', 1, 2); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (2, 'DRAFT', get_test_plan_version_id(text 'Select Only Combobox Example'), '2021-05-14 14:18:23.602-05', 2, 1); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (3, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 1, 2, 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (4, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 2, 1, 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (5, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 3, 3, 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (6, 'CANDIDATE', get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', 3, 3, 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (7, 'DRAFT', get_test_plan_version_id(text 'Alert Example'), '2021-05-14 14:18:23.602-05', 3, 1); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (1, get_test_plan_version_id(text 'Toggle Button'), '2021-05-14 14:18:23.602-05', 1, 2); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (2, get_test_plan_version_id(text 'Select Only Combobox Example'), '2021-05-14 14:18:23.602-05', 2, 1); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (3, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', '2022-07-06', 1, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (4, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', '2022-07-06', 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (5, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', '2022-07-06', 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (6, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', '2022-07-06', 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (7, get_test_plan_version_id(text 'Alert Example'), '2021-05-14 14:18:23.602-05', 3, 1); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (8, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 1, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (9, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 2, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (10, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 3, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (11, get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 3, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (12, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', 1, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (13, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', 2, 2, 'READY'); -- -- Data for Name: TestPlanVersion; Type: TABLE DATA; Schema: public; Owner: atr -- -UPDATE "TestPlanVersion" SET "candidatePhaseReachedAt" = '2022-07-06', "recommendedPhaseTargetDate" = '2023-01-02' WHERE id = get_test_plan_version_id(text 'Modal Dialog Example'); -UPDATE "TestPlanVersion" SET "candidatePhaseReachedAt" = '2022-07-06', "recommendedPhaseTargetDate" = '2023-01-02' WHERE id = get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'); +UPDATE "TestPlanVersion" SET "phase" = 'DRAFT', "draftPhaseReachedAt" = '2022-07-06' WHERE id = get_test_plan_version_id(text 'Toggle Button'); +UPDATE "TestPlanVersion" SET "phase" = 'DRAFT', "draftPhaseReachedAt" = '2022-07-06' WHERE id = get_test_plan_version_id(text 'Select Only Combobox Example'); +UPDATE "TestPlanVersion" SET "phase" = 'CANDIDATE', "draftPhaseReachedAt" = '2022-07-06', "candidatePhaseReachedAt" = '2022-07-06', "recommendedPhaseTargetDate" = '2023-01-02' WHERE id = get_test_plan_version_id(text 'Modal Dialog Example'); +UPDATE "TestPlanVersion" SET "phase" = 'RECOMMENDED', "candidatePhaseReachedAt" = '2022-07-06', "recommendedPhaseTargetDate" = '2023-01-02', "recommendedPhaseReachedAt" = '2023-01-03' WHERE id = get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'); +UPDATE "TestPlanVersion" SET "phase" = 'DRAFT', "draftPhaseReachedAt" = '2022-07-06' WHERE id = get_test_plan_version_id(text 'Alert Example'); -- -- Data for Name: User; Type: TABLE DATA; Schema: public; Owner: atr @@ -108,6 +117,12 @@ INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults" INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (5, 1, 4, '[]'); INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (6, 1, 5, '[]'); INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (7, 2, 6, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (8, 1, 8, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (9, 1, 9, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (10, 1, 10, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (11, 1, 11, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (12, 1, 12, '[]'); +INSERT INTO "TestPlanRun" (id, "testerUserId", "testPlanReportId", "testResults") VALUES (13, 1, 13, '[]'); -- -- Name: At_id_seq; Type: SEQUENCE SET; Schema: public; Owner: atr diff --git a/server/seeders/20230622202911-addAtBrowser.js b/server/seeders/20230622202911-addAtBrowser.js index 9faa06cb9..73afa24bf 100644 --- a/server/seeders/20230622202911-addAtBrowser.js +++ b/server/seeders/20230622202911-addAtBrowser.js @@ -9,16 +9,18 @@ module.exports = { const firefoxBrowserId = 1; const chromeBrowserId = 2; const safariBrowserId = 3; + return queryInterface.bulkInsert( 'AtBrowsers', + // prettier-ignore [ - { atId: jawsAtId, browserId: firefoxBrowserId }, - { atId: jawsAtId, browserId: chromeBrowserId }, - { atId: nvdaAtId, browserId: firefoxBrowserId }, - { atId: nvdaAtId, browserId: chromeBrowserId }, - { atId: voiceOverAtId, browserId: safariBrowserId }, - { atId: voiceOverAtId, browserId: firefoxBrowserId }, - { atId: voiceOverAtId, browserId: chromeBrowserId } + { atId: jawsAtId, browserId: firefoxBrowserId, isCandidate: false, isRecommended: true }, + { atId: jawsAtId, browserId: chromeBrowserId, isCandidate: true, isRecommended: true }, + { atId: nvdaAtId, browserId: firefoxBrowserId, isCandidate: false, isRecommended: true }, + { atId: nvdaAtId, browserId: chromeBrowserId, isCandidate: true, isRecommended: true }, + { atId: voiceOverAtId, browserId: safariBrowserId, isCandidate: true, isRecommended: true }, + { atId: voiceOverAtId, browserId: firefoxBrowserId, isCandidate: false, isRecommended: false }, + { atId: voiceOverAtId, browserId: chromeBrowserId, isCandidate: false, isRecommended: true } ], {} ); diff --git a/server/tests/integration/embed.test.js b/server/tests/integration/embed.test.js index 350925ff8..7924fbb9c 100644 --- a/server/tests/integration/embed.test.js +++ b/server/tests/integration/embed.test.js @@ -28,12 +28,14 @@ describe('embed', () => { // Load the iframe, twice, one with a normal load and a second time from // the cache const initialLoadTimeStart = Number(new Date()); - const res = await sessionAgent.get('/embed/reports/modal-dialog'); + const res = await sessionAgent.get('/embed/reports/checkbox-tri-state'); const initialLoadTimeEnd = Number(new Date()); const initialLoadTime = initialLoadTimeEnd - initialLoadTimeStart; const cachedTimeStart = Number(new Date()); - const res2 = await sessionAgent.get('/embed/reports/modal-dialog'); + const res2 = await sessionAgent.get( + '/embed/reports/checkbox-tri-state' + ); const cachedTimeEnd = Number(new Date()); const cachedTime = cachedTimeEnd - cachedTimeStart; @@ -43,10 +45,11 @@ describe('embed', () => { const nonWarning = screen.queryByText('Recommended Report'); const warning = screen.queryByText('Warning! Unapproved Report'); const unsupportedAtBrowserCombination = - screen.getAllByText('Not Applicable'); - const futureSupportedAtBrowserCombination = screen.getAllByText( + screen.queryAllByText('Not Applicable'); + const futureSupportedAtBrowserCombination = screen.queryAllByText( 'Data Not Yet Available' ); + const nonWarningContents = screen.queryByText( 'The information in this report is generated from candidate tests', { exact: false } @@ -70,8 +73,8 @@ describe('embed', () => { expect(initialLoadTime / 10).toBeGreaterThan(cachedTime); expect(nonWarning || warning).toBeTruthy(); expect(nonWarningContents || warningContents).toBeTruthy(); - expect(unsupportedAtBrowserCombination).toBeTruthy(); - expect(futureSupportedAtBrowserCombination).toBeTruthy(); + expect(unsupportedAtBrowserCombination.length).not.toBe(0); + expect(futureSupportedAtBrowserCombination.length).not.toBe(0); expect(viewReportButton).toBeTruthy(); expect(viewReportButtonOnClick).toMatch( // Onclick should be like the following: @@ -80,7 +83,7 @@ describe('embed', () => { ); expect(copyEmbedButton).toBeTruthy(); expect(copyEmbedButtonOnClick).toMatch( - /announceCopied\('https?:\/\/[\w.:]+\/embed\/reports\/modal-dialog'\)/ + /announceCopied\('https?:\/\/[\w.:]+\/embed\/reports\/checkbox-tri-state'\)/ ); expect(cellWithData).toBeTruthy(); }); diff --git a/server/tests/integration/graphql.test.js b/server/tests/integration/graphql.test.js index a834ff9f4..4e213a540 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -147,9 +147,11 @@ describe('graphql', () => { ['PopulatedData', 'browserVersion'], ['TestPlanReport', 'issues'], ['TestPlanReport', 'vendorReviewStatus'], + ['TestPlanReportOperations', 'updateTestPlanReportTestPlanVersion'], ['TestPlanVersion', 'candidatePhaseReachedAt'], ['TestPlanVersion', 'recommendedPhaseReachedAt'], ['TestPlanVersion', 'recommendedPhaseTargetDate'], + ['TestPlanVersion', 'deprecatedAt'], ['Test', 'viewers'] ]; ({ @@ -185,6 +187,16 @@ describe('graphql', () => { id name } + candidateAts { + __typename + id + name + } + recommendedAts { + __typename + id + name + } browserVersions { __typename id @@ -200,6 +212,16 @@ describe('graphql', () => { id name } + candidateBrowsers { + __typename + id + name + } + recommendedBrowsers { + __typename + id + name + } atVersions { __typename id @@ -278,9 +300,11 @@ describe('graphql', () => { __typename id phase + draftPhaseReachedAt candidatePhaseReachedAt recommendedPhaseTargetDate recommendedPhaseReachedAt + deprecatedAt } testPlanVersion(id: 1) { __typename @@ -296,7 +320,7 @@ describe('graphql', () => { conflictTestPlanReport: testPlanReport(id: 2) { __typename id - status + isFinal createdAt vendorReviewStatus testPlanVersion { @@ -399,6 +423,7 @@ describe('graphql', () => { name releasedAt } + markedFinalAt } testPlanReports { id @@ -518,30 +543,6 @@ describe('graphql', () => { locationOfData } } - reportStatus: testPlanReport(id: 1) { - __typename - updateStatus(status: CANDIDATE) { - locationOfData - } - } - bulkReportStatus: testPlanReport(ids: [1]) { - __typename - bulkUpdateStatus(status: CANDIDATE) { - locationOfData - } - } - updateToTestPlanVersion: testPlanReport(id: 1) { - __typename - updateTestPlanReportTestPlanVersion( - input: { - testPlanVersionId: 34 - atId: 1 - browserId: 2 - } - ) { - locationOfData - } - } updateTestPlanVersionPhase: testPlanVersion(id: 26) { __typename updatePhase(phase: DRAFT) { @@ -576,6 +577,18 @@ describe('graphql', () => { } } } + markReportAsFinal: testPlanReport(id: 2) { + __typename + markAsFinal { + locationOfData + } + } + unmarkReportAsFinal: testPlanReport(id: 2) { + __typename + unmarkAsFinal { + locationOfData + } + } testPlanRun(id: 1) { __typename findOrCreateTestResult( diff --git a/server/tests/integration/test-queue.test.js b/server/tests/integration/test-queue.test.js index 8648ff342..b4ba64a38 100644 --- a/server/tests/integration/test-queue.test.js +++ b/server/tests/integration/test-queue.test.js @@ -14,7 +14,7 @@ describe('test queue', () => { const result = await query( gql` query { - testPlanReports(statuses: [DRAFT]) { + testPlanReports(testPlanVersionPhases: [DRAFT]) { id conflicts { source { @@ -25,6 +25,7 @@ describe('test queue', () => { } testPlanVersion { title + phase gitSha gitMessage tests { @@ -52,6 +53,7 @@ describe('test queue', () => { conflicts: expect.any(Array), testPlanVersion: { title: expect.any(String), + phase: expect.any(String), gitSha: expect.any(String), gitMessage: expect.any(String), tests: expect.arrayContaining([ @@ -189,16 +191,28 @@ describe('test queue', () => { }); }); - it('can be finalized', async () => { + it('can set test plan version to candidate and recommended', async () => { await dbCleaner(async () => { - const testPlanReportId = '3'; - // This report starts in a FINALIZED state. Let's set it to DRAFT. + const candidateTestPlanVersions = await query(gql` + query { + testPlanVersions(phases: [CANDIDATE]) { + id + phase + } + } + `); + const candidateTestPlanVersion = + candidateTestPlanVersions.testPlanVersions[0]; + + let testPlanVersionId = candidateTestPlanVersion.id; + // This version is in 'CANDIDATE' phase. Let's set it to DRAFT + // This will also remove the associated TestPlanReports markedFinalAt values await mutate(gql` mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: DRAFT) { - testPlanReport { - status + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: DRAFT) { + testPlanVersion { + phase } } } @@ -207,46 +221,66 @@ describe('test queue', () => { const previous = await query(gql` query { - testPlanReport(id: ${testPlanReportId}) { - status + testPlanVersion(id: ${testPlanVersionId}) { + phase + testPlanReports { + id + } } } `); - const previousStatus = previous.testPlanReport.status; + const previousPhase = previous.testPlanVersion.phase; + const previousPhaseTestPlanReportId = + previous.testPlanVersion.testPlanReports[0].id; - const candidateResult = await mutate(gql` + // Need to approve at least one of the associated reports + await mutate(gql` mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: CANDIDATE) { + testPlanReport(id: ${previousPhaseTestPlanReportId}) { + markAsFinal { testPlanReport { - status + id + markedFinalAt + } + } + } + } + `); + + const candidateResult = await mutate(gql` + mutation { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: CANDIDATE) { + testPlanVersion { + phase } } } } `); - const candidateResultStatus = - candidateResult.testPlanReport.updateStatus.testPlanReport - .status; + const candidateResultPhase = + candidateResult.testPlanVersion.updatePhase.testPlanVersion + .phase; const recommendedResult = await mutate(gql` mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: RECOMMENDED) { - testPlanReport { - status + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: RECOMMENDED) { + testPlanVersion { + phase } } } } `); - const recommendedResultStatus = - recommendedResult.testPlanReport.updateStatus.testPlanReport - .status; + const recommendedResultPhase = + recommendedResult.testPlanVersion.updatePhase.testPlanVersion + .phase; - expect(previousStatus).not.toBe('CANDIDATE'); - expect(candidateResultStatus).toBe('CANDIDATE'); - expect(recommendedResultStatus).toBe('RECOMMENDED'); + expect(candidateTestPlanVersion.phase).toBe('CANDIDATE'); + expect(previousPhase).not.toBe('CANDIDATE'); + expect(candidateResultPhase).toBe('CANDIDATE'); + expect(recommendedResultPhase).toBe('RECOMMENDED'); }); }); @@ -333,7 +367,6 @@ describe('test queue', () => { populatedData { testPlanReport { id - status at { id } @@ -343,6 +376,7 @@ describe('test queue', () => { } testPlanVersion { id + phase } } created { @@ -371,7 +405,6 @@ describe('test queue', () => { expect(first.testPlanReport).toEqual( expect.objectContaining({ id: expect.anything(), - status: 'DRAFT', at: expect.objectContaining({ id: atId }), @@ -382,7 +415,8 @@ describe('test queue', () => { ); expect(first.testPlanVersion).toEqual( expect.objectContaining({ - id: testPlanVersionId + id: testPlanVersionId, + phase: 'DRAFT' }) ); expect(first.created.length).toBe(1); @@ -403,7 +437,7 @@ describe('test queue', () => { it('can be deleted along with associated runs', async () => { await dbCleaner(async () => { - const testPlanReportId = '3'; + const testPlanReportId = '4'; const queryBefore = await query(gql` query { testPlanReport(id: ${testPlanReportId}) { diff --git a/server/tests/models/At.spec.js b/server/tests/models/At.spec.js index 3e5d884c4..8b8edd364 100644 --- a/server/tests/models/At.spec.js +++ b/server/tests/models/At.spec.js @@ -8,6 +8,7 @@ const { const AtModel = require('../../models/At'); const AtVersionModel = require('../../models/AtVersion'); const AtModeModel = require('../../models/AtMode'); +const BrowserModel = require('../../models/Browser'); describe('AtModel', () => { // A1 @@ -26,11 +27,13 @@ describe('AtModel', () => { // A1 const AT_VERSION_ASSOCIATION = { as: 'atVersions' }; const AT_MODE_ASSOCIATION = { as: 'modes' }; + const BROWSER_ASSOCIATION = { through: 'AtBrowsers', as: 'browsers' }; // A2 beforeAll(() => { Model.hasMany(AtVersionModel, AT_VERSION_ASSOCIATION); Model.hasMany(AtModeModel, AT_MODE_ASSOCIATION); + Model.hasMany(BrowserModel, BROWSER_ASSOCIATION); }); it('defined a hasMany association with AtVersion', () => { diff --git a/server/tests/models/Browser.spec.js b/server/tests/models/Browser.spec.js index 8096b55ff..8569d7439 100644 --- a/server/tests/models/Browser.spec.js +++ b/server/tests/models/Browser.spec.js @@ -7,6 +7,7 @@ const { const BrowserModel = require('../../models/Browser'); const BrowserVersionModel = require('../../models/BrowserVersion'); +const AtModel = require('../../models/At'); describe('BrowserModel', () => { // A1 @@ -24,10 +25,15 @@ describe('BrowserModel', () => { describe('associations', () => { // A1 const BROWSER_VERSION_ASSOCIATION = { as: 'browserVersions' }; + const AT_ASSOCIATION = { + through: 'AtBrowsers', + as: 'ats' + }; // A2 beforeAll(() => { Model.hasMany(BrowserVersionModel, BROWSER_VERSION_ASSOCIATION); + Model.hasMany(AtModel, AT_ASSOCIATION); }); it('defined a hasMany association with BrowserVersion', () => { diff --git a/server/tests/models/TestPlanReport.spec.js b/server/tests/models/TestPlanReport.spec.js index 0fa1abd8d..86ae6a238 100644 --- a/server/tests/models/TestPlanReport.spec.js +++ b/server/tests/models/TestPlanReport.spec.js @@ -21,7 +21,7 @@ describe('TestPlanReportModel', () => { describe('properties', () => { // A3 - ['status', 'testPlanVersionId', 'createdAt'].forEach( + ['testPlanVersionId', 'createdAt'].forEach( checkPropertyExists(modelInstance) ); }); diff --git a/server/tests/models/TestPlanVersion.spec.js b/server/tests/models/TestPlanVersion.spec.js index abd925142..cd711ed53 100644 --- a/server/tests/models/TestPlanVersion.spec.js +++ b/server/tests/models/TestPlanVersion.spec.js @@ -20,6 +20,7 @@ describe('TestPlanVersionModel', () => { // A3 [ 'title', + 'phase', 'directory', 'gitSha', 'gitMessage', diff --git a/server/tests/models/services/TestPlanReportService.test.js b/server/tests/models/services/TestPlanReportService.test.js index a2dbff073..e2b2a3281 100644 --- a/server/tests/models/services/TestPlanReportService.test.js +++ b/server/tests/models/services/TestPlanReportService.test.js @@ -12,10 +12,9 @@ describe('TestPlanReportModel Data Checks', () => { const testPlanReport = await TestPlanReportService.getTestPlanReportById(_id); - const { id, status, testPlanVersionId, createdAt } = testPlanReport; + const { id, testPlanVersionId, createdAt } = testPlanReport; expect(id).toEqual(_id); - expect(status).toMatch(/^(DRAFT|CANDIDATE|RECOMMENDED)$/); expect(testPlanVersionId).toBeTruthy(); expect(createdAt).toBeTruthy(); expect(testPlanReport).toHaveProperty('testPlanRuns'); @@ -37,11 +36,10 @@ describe('TestPlanReportModel Data Checks', () => { [], [] ); - const { id, status, testPlanVersionId, createdAt, atId, browserId } = + const { id, testPlanVersionId, createdAt, atId, browserId } = testPlanReport; expect(id).toEqual(_id); - expect(status).toMatch(/^(DRAFT|CANDIDATE|RECOMMENDED)$/); expect(testPlanVersionId).toBeTruthy(); expect(createdAt).toBeTruthy(); expect(atId).toBeTruthy(); @@ -101,11 +99,9 @@ describe('TestPlanReportModel Data Checks', () => { const _atId = 1; const _browserId = 1; const _testPlanVersionId = 3; - const _status = 'DRAFT'; const testPlanReport = await TestPlanReportService.createTestPlanReport({ - status: _status, testPlanVersionId: _testPlanVersionId, atId: _atId, browserId: _browserId @@ -114,7 +110,6 @@ describe('TestPlanReportModel Data Checks', () => { expect(testPlanReport).toEqual( expect.objectContaining({ id: expect.anything(), - status: _status, testPlanVersion: expect.objectContaining({ id: _testPlanVersionId }), @@ -132,7 +127,6 @@ describe('TestPlanReportModel Data Checks', () => { it('should getOrCreate TestPlanReport', async () => { await dbCleaner(async () => { // A1 - const _status = 'DRAFT'; const _testPlanVersionId = 2; const _atId = 2; const _browserId = 1; @@ -140,7 +134,6 @@ describe('TestPlanReportModel Data Checks', () => { // A2 const [testPlanReport, created] = await TestPlanReportService.getOrCreateTestPlanReport({ - status: _status, testPlanVersionId: _testPlanVersionId, atId: _atId, browserId: _browserId @@ -150,7 +143,6 @@ describe('TestPlanReportModel Data Checks', () => { expect(testPlanReport).toEqual( expect.objectContaining({ id: expect.anything(), - status: _status, testPlanVersion: expect.objectContaining({ id: _testPlanVersionId }), diff --git a/server/tests/models/services/TestPlanVersionService.test.js b/server/tests/models/services/TestPlanVersionService.test.js index 6435f6f93..55d43b0f2 100644 --- a/server/tests/models/services/TestPlanVersionService.test.js +++ b/server/tests/models/services/TestPlanVersionService.test.js @@ -16,6 +16,7 @@ describe('TestPlanReportModel Data Checks', () => { const { id, title, + phase, directory, gitSha, gitMessage, @@ -28,6 +29,7 @@ describe('TestPlanReportModel Data Checks', () => { expect(id).toEqual(_id); expect(title).toBeTruthy(); + expect(phase).toBeTruthy(); expect(directory).toBeTruthy(); expect(gitSha).toBeTruthy(); expect(gitMessage).toBeTruthy(); @@ -55,6 +57,7 @@ describe('TestPlanReportModel Data Checks', () => { const { id, title, + phase, directory, gitSha, gitMessage, @@ -66,6 +69,7 @@ describe('TestPlanReportModel Data Checks', () => { expect(id).toEqual(_id); expect(title).toBeTruthy(); + expect(phase).toBeTruthy(); expect(directory).toBeTruthy(); expect(gitSha).toBeTruthy(); expect(gitMessage).toBeTruthy(); @@ -90,6 +94,7 @@ describe('TestPlanReportModel Data Checks', () => { // A1 const _id = 99; const _title = randomStringGenerator(); + const _phase = 'RD'; const _directory = 'checkbox'; const _gitSha = randomStringGenerator(); const _gitMessage = randomStringGenerator(); @@ -118,6 +123,7 @@ describe('TestPlanReportModel Data Checks', () => { const { id: createdId, title: createdTitle, + phase: createdPhase, directory: createdDirectory, gitSha: createdGitSha, gitMessage: createdGitMessage, @@ -140,6 +146,7 @@ describe('TestPlanReportModel Data Checks', () => { // After testPlanVersion created expect(createdId).toBe(_id); expect(createdTitle).toBe(_title); + expect(createdPhase).toBe(_phase); expect(createdDirectory).toBe(_directory); expect(createdGitSha).toBe(_gitSha); expect(createdGitMessage).toBe(_gitMessage);