diff --git a/.github/workflows/runtest.yml b/.github/workflows/runtest.yml index 4d0d725aa..0a8684497 100644 --- a/.github/workflows/runtest.yml +++ b/.github/workflows/runtest.yml @@ -37,6 +37,7 @@ jobs: yarn sequelize:test db:migrate yarn sequelize:test db:seed:all yarn workspace server db-import-tests:test -c ${IMPORT_ARIA_AT_TESTS_COMMIT_1} + yarn workspace server db-import-tests:test -c ${IMPORT_ARIA_AT_TESTS_COMMIT_2} yarn workspace server db-import-tests:test yarn workspace server db-populate-sample-data:test - name: test diff --git a/client/components/AddTestToQueueWithConfirmation/index.jsx b/client/components/AddTestToQueueWithConfirmation/index.jsx new file mode 100644 index 000000000..4a0c9dcba --- /dev/null +++ b/client/components/AddTestToQueueWithConfirmation/index.jsx @@ -0,0 +1,92 @@ +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-bootstrap'; +import BasicModal from '../common/BasicModal'; +import { useMutation } from '@apollo/client'; +import { ADD_TEST_QUEUE_MUTATION } from '../TestQueue/queries'; +import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; + +function AddTestToQueueWithConfirmation({ + testPlanVersion, + browser, + at, + disabled = false, + buttonText = 'Add to Test Queue', + triggerUpdate = () => {} +}) { + const [showConfirmation, setShowConfirmation] = useState(false); + const [addTestPlanReport] = useMutation(ADD_TEST_QUEUE_MUTATION); + const { triggerLoad, loadingMessage } = useTriggerLoad(); + const buttonRef = useRef(); + + const feedbackModalTitle = 'Successfully Added Test Plan'; + + const feedbackModalContent = ( + <> + Successfully added {testPlanVersion?.title} for{' '} + + {at?.name} and {browser?.name} + {' '} + to the Test Queue. + + ); + + const addTestToQueue = async () => { + await triggerLoad(async () => { + await addTestPlanReport({ + variables: { + testPlanVersionId: testPlanVersion.id, + atId: at.id, + browserId: browser.id + } + }); + }, 'Adding Test Plan to Test Queue'); + setShowConfirmation(true); + }; + + return ( + <> + + + { + await triggerUpdate(); + setShowConfirmation(false); + setTimeout(() => { + if (buttonRef?.current) { + buttonRef.current.focus(); + } + }, 0); + }} + /> + + ); +} + +AddTestToQueueWithConfirmation.propTypes = { + testPlanVersion: PropTypes.object, + browser: PropTypes.object, + at: PropTypes.object, + buttonRef: PropTypes.object, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + disabled: PropTypes.bool, + buttonText: PropTypes.string, + triggerUpdate: PropTypes.func +}; + +export default AddTestToQueueWithConfirmation; diff --git a/client/components/App/App.jsx b/client/components/App/App.jsx index 756a30de2..4cd487051 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 85% rename from client/components/CandidateTests/CandidateTestPlanRun/index.jsx rename to client/components/CandidateReview/CandidateTestPlanRun/index.jsx index e0476de87..39c861a20 100644 --- a/client/components/CandidateTests/CandidateTestPlanRun/index.jsx +++ b/client/components/CandidateReview/CandidateTestPlanRun/index.jsx @@ -20,7 +20,6 @@ import { Helmet } from 'react-helmet'; import './CandidateTestPlanRun.css'; import '../../TestRun/TestRun.css'; import '../../App/App.css'; -import useResizeObserver from '@react-hook/resize-observer'; import { useMediaQuery } from 'react-responsive'; import { convertDateToString } from '../../../utils/formatter'; import TestPlanResultsTable from '../../Reports/TestPlanResultsTable'; @@ -29,19 +28,9 @@ import ThankYouModal from '../CandidateModals/ThankYouModal'; import getMetrics from '../../Reports/getMetrics'; import FeedbackListItem from '../FeedbackListItem'; import DisclosureComponent from '../../common/DisclosureComponent'; - -// https://codesandbox.io/s/react-hookresize-observer-example-ft88x -function useSize(target) { - const [size, setSize] = React.useState(); - - React.useLayoutEffect(() => { - target && setSize(target.getBoundingClientRect()); - }, [target]); - - // Where the magic happens - useResizeObserver(target, entry => setSize(entry.contentRect)); - return size; -} +import createIssueLink, { + getIssueSearchLink +} from '../../../utils/createIssueLink'; const CandidateTestPlanRun = () => { const { atId, testPlanVersionId } = useParams(); @@ -80,10 +69,6 @@ const CandidateTestPlanRun = () => { const [showBrowserBools, setShowBrowserBools] = useState([]); const [showBrowserClicks, setShowBrowserClicks] = useState([]); - const [issuesHeading, setIssuesHeading] = React.useState(); - const issuesHeadingSize = useSize(issuesHeading); - const [issuesList, setIssuesList] = React.useState(); - const issuesListSize = useSize(issuesList); const isLaptopOrLarger = useMediaQuery({ query: '(min-width: 792px)' }); @@ -293,8 +278,13 @@ const CandidateTestPlanRun = () => { })); const currentTest = tests[currentTestIndex]; - const { testPlanVersion, vendorReviewStatus, recommendedStatusTargetDate } = - testPlanReport; + const { testPlanVersion, vendorReviewStatus } = testPlanReport; + const { recommendedPhaseTargetDate } = testPlanVersion; + + const versionString = `V${convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}`; const vendorReviewStatusMap = { READY: 'Ready', @@ -305,71 +295,86 @@ const CandidateTestPlanRun = () => { const reviewStatusText = vendorReviewStatusMap[reviewStatus]; const targetCompletionDate = convertDateToString( - new Date(recommendedStatusTargetDate), + new Date(recommendedPhaseTargetDate), 'MMMM D, YYYY' ); // Assumes that the issues are across the entire AT/Browser combination const changesRequestedIssues = testPlanReport.issues?.filter( issue => + issue.isCandidateReview && issue.feedbackType === 'CHANGES_REQUESTED' && issue.testNumberFilteredByAt === currentTest.seq ); const feedbackIssues = testPlanReport.issues?.filter( issue => + issue.isCandidateReview && issue.feedbackType === 'FEEDBACK' && issue.testNumberFilteredByAt === currentTest.seq ); + const issue = { + isCandidateReview: true, + isCandidateReviewChangesRequested: true, + testPlanTitle: testPlanVersion.title, + testPlanDirectory: testPlanVersion.testPlan.directory, + versionString, + testTitle: currentTest.title, + testRowNumber: currentTest.rowNumber, + testRenderedUrl: currentTest.renderedUrl, + atName: testPlanReport.at.name + }; + + const requestChangesUrl = createIssueLink(issue); + + const feedbackUrl = createIssueLink({ + ...issue, + isCandidateReviewChangesRequested: false + }); + + const generalFeedbackUrl = createIssueLink({ + ...issue, + isCandidateReviewChangesRequested: false, + testTitle: undefined, + testRowNumber: undefined, + testRenderedUrl: undefined + }); + + const issueQuery = { + isCandidateReview: true, + isCandidateReviewChangesRequested: false, + testPlanTitle: testPlanVersion.title, + versionString, + testRowNumber: currentTest.rowNumber, + username: data.me.username, + atName: testPlanReport.at.name + }; + + const feedbackGithubUrl = getIssueSearchLink(issueQuery); + + const changesRequestedGithubUrl = getIssueSearchLink({ + ...issueQuery, + isCandidateReviewChangesRequested: true + }); + + let fileBugUrl; + const githubAtLabelMap = { 'VoiceOver for macOS': 'vo', JAWS: 'jaws', NVDA: 'nvda' }; - const generateGithubUrl = ( - test = false, - type = '', - titleAddition = '', - search = false, - author = '' - ) => { - const testPlanVersionDate = convertDateToString( - new Date(testPlanVersion.updatedAt), - 'DD-MM-YYYY' - ); - - const generateGithubTitle = () => { - return `${at} Feedback: "${currentTest.title}" (${ - testPlanVersion.title - }${ - test ? `, Test ${currentTest.seq}` : '' - }, ${testPlanVersionDate})${ - titleAddition ? ` - ${titleAddition}` : '' - }`; - }; - - const githubIssueUrlTitle = generateGithubTitle(); - const defaultGithubLabels = 'app,candidate-review'; - let githubUrl; - - if (!search) { - githubUrl = `https://github.com/w3c/aria-at/issues/new?title=${encodeURI( - githubIssueUrlTitle - )}&labels=${defaultGithubLabels},${githubAtLabelMap[at]}`; - return `${githubUrl},${type}`; - } else { - let title = generateGithubTitle(); - let query = encodeURI( - `label:app label:candidate-review label:${type} label:${ - githubAtLabelMap[at] - } ${author ? `author:${author}` : ''} ${title}` - ); - githubUrl = `https://github.com/w3c/aria-at/issues?q=${query}`; - return githubUrl; - } - }; + if (githubAtLabelMap[at] == 'vo') { + fileBugUrl = + 'https://bugs.webkit.org/buglist.cgi?quicksearch=voiceover'; + } else if (githubAtLabelMap[at] == 'nvda') { + fileBugUrl = 'https://github.com/nvaccess/nvda/issues'; + } else { + fileBugUrl = + 'https://github.com/FreedomScientific/VFO-standards-support/issues'; + } const heading = (
    @@ -426,27 +431,16 @@ const CandidateTestPlanRun = () => { ); const feedback = testPlanReport.issues.filter( - issue => issue.testNumberFilteredByAt == currentTest.seq + issue => + issue.isCandidateReview && + issue.testNumberFilteredByAt == currentTest.seq ).length > 0 && (
    -

    +

    Feedback from{' '} {at} Representative

    -
      +
        {[changesRequestedIssues, feedbackIssues].map((list, index) => { if (list.length > 0) { const uniqueAuthors = [ @@ -467,15 +461,15 @@ const CandidateTestPlanRun = () => { } issues={list} individualTest={true} - githubUrl={generateGithubUrl( - true, - index === 0 - ? 'changes-requested' - : 'feedback', - null, - true, - !differentAuthors ? data.me.username : null - )} + githubUrl={getIssueSearchLink({ + isCandidateReview: true, + isCandidateReviewChangesRequested: + index === 0, + atName: testPlanReport.at.name, + testPlanTitle: testPlanVersion.title, + versionString, + testRowNumber: currentTest.rowNumber + })} /> ); } @@ -540,20 +534,6 @@ const CandidateTestPlanRun = () => {
    ); - const requestChangesUrl = generateGithubUrl(true, 'changes-requested'); - const feedbackUrl = generateGithubUrl(true, 'feedback'); - let fileBugUrl; - - if (githubAtLabelMap[at] == 'vo') { - fileBugUrl = - 'https://bugs.webkit.org/buglist.cgi?quicksearch=voiceover'; - } else if (githubAtLabelMap[at] == 'nvda') { - fileBugUrl = 'https://github.com/nvaccess/nvda/issues'; - } else { - fileBugUrl = - 'https://github.com/FreedomScientific/VFO-standards-support/issues'; - } - return ( @@ -580,11 +560,10 @@ const CandidateTestPlanRun = () => { {heading} {testInfo} - + {feedback} @@ -691,28 +670,18 @@ const CandidateTestPlanRun = () => { testPlan={testPlanVersion.title} feedbackIssues={testPlanReport.issues?.filter( issue => + issue.isCandidateReview && issue.feedbackType === 'FEEDBACK' && issue.author == data.me.username )} - feedbackGithubUrl={generateGithubUrl( - false, - 'feedback', - null, - true, - data.me.username - )} + feedbackGithubUrl={feedbackGithubUrl} changesRequestedIssues={testPlanReport.issues?.filter( issue => + issue.isCandidateReview && issue.feedbackType === 'CHANGES_REQUESTED' && issue.author == data.me.username )} - changesRequestedGithubUrl={generateGithubUrl( - false, - 'changes-requested', - null, - true, - data.me.username - )} + changesRequestedGithubUrl={changesRequestedGithubUrl} handleAction={submitApproval} handleHide={() => setFeedbackModalShowing(false)} /> @@ -724,13 +693,9 @@ const CandidateTestPlanRun = () => { show={true} handleAction={async () => { setThankYouModalShowing(false); - navigate('/candidate-tests'); + navigate('/candidate-review'); }} - githubUrl={generateGithubUrl( - false, - 'feedback', - 'General Feedback' - )} + githubUrl={generalFeedbackUrl} /> ) : ( <> diff --git a/client/components/CandidateTests/CandidateTestPlanRun/queries.js b/client/components/CandidateReview/CandidateTestPlanRun/queries.js similarity index 95% rename from client/components/CandidateTests/CandidateTestPlanRun/queries.js rename to client/components/CandidateReview/CandidateTestPlanRun/queries.js index fa056e72d..9b2f6d3e3 100644 --- a/client/components/CandidateTests/CandidateTestPlanRun/queries.js +++ b/client/components/CandidateReview/CandidateTestPlanRun/queries.js @@ -37,16 +37,15 @@ export const CANDIDATE_REPORTS_QUERY = gql` } testPlanReports( atId: $atId - statuses: [CANDIDATE] + testPlanVersionPhases: [CANDIDATE] testPlanVersionId: $testPlanVersionId testPlanVersionIds: $testPlanVersionIds ) { id - candidateStatusReachedAt - recommendedStatusTargetDate vendorReviewStatus issues { author + isCandidateReview feedbackType testNumberFilteredByAt link @@ -67,6 +66,7 @@ export const CANDIDATE_REPORTS_QUERY = gql` testPlanVersion { id title + phase gitSha testPlan { directory @@ -74,10 +74,13 @@ export const CANDIDATE_REPORTS_QUERY = gql` metadata testPageUrl updatedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate } runnableTests { id title + rowNumber renderedUrl renderableContent viewers { @@ -142,7 +145,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/CandidateReview/TestPlans/index.jsx b/client/components/CandidateReview/TestPlans/index.jsx new file mode 100644 index 000000000..9b02c1432 --- /dev/null +++ b/client/components/CandidateReview/TestPlans/index.jsx @@ -0,0 +1,879 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; +import styled from '@emotion/styled'; +import { Container, Table } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faFlag, + faCheck, + faChevronUp, + faChevronDown, + faCommentAlt +} from '@fortawesome/free-solid-svg-icons'; +import alphabetizeObjectBy from '@client/utils/alphabetizeObjectBy'; +import { + getTestPlanTargetTitle, + getTestPlanVersionTitle +} from '@components/Reports/getTitles'; +import ClippedProgressBar from '@components/common/ClippedProgressBar'; +import { convertDateToString } from '@client/utils/formatter'; +import './TestPlans.css'; + +const FullHeightContainer = styled(Container)` + min-height: calc(100vh - 64px); +`; + +const StatusText = styled.span` + height: 1.625em; + font-size: 0.875em; + padding: 4px 10px; + border-radius: 1.625rem; + + overflow: hidden; + white-space: nowrap; + + &.feedback { + border: 2px solid #b253f8; + svg { + color: #b253f8; + } + } + + &.changes-requested { + border: 2px solid #f87f1b; + svg { + color: #f87f1b; + } + } + + &.ready-for-review { + border: 2px solid #edbb1d; + + span.dot { + height: 10px; + width: 10px; + padding: 0; + margin-right: 8px; + border-radius: 50%; + background: #edbb1d; + } + } + + &.in-progress { + border: 2px solid #2560ab; + + span.dot { + height: 10px; + width: 10px; + padding: 0; + margin-right: 8px; + border-radius: 50%; + background: #2560ab; + } + } + + &.approved { + border: 2px solid #309d08; + svg { + color: #309d08; + } + } +`; + +const DisclosureParent = styled.div` + border: 1px solid #d3d5da; + border-radius: 3px; + margin-bottom: 3rem; + + h3 { + margin: 0; + padding: 0; + } +`; + +const DisclosureButton = styled.button` + position: relative; + width: 100%; + margin: 0; + padding: 0.75rem; + text-align: left; + font-size: 1.5rem; + font-weight: 500; + border: none; + border-radius: 3px; + background-color: transparent; + + &:hover, + &:focus { + padding: 0.75rem; + border: 0 solid #005a9c; + background-color: #def; + cursor: pointer; + } + + svg { + position: absolute; + margin: 0; + top: 50%; + right: 1.25rem; + + color: #969696; + transform: translateY(-50%); + } +`; + +const DisclosureContainer = styled.div` + display: ${({ show }) => (show ? 'flex' : 'none')}; + flex-direction: column; + gap: 1.25rem; + + background-color: #f8f9fa; + + table { + margin-bottom: 0; + } +`; + +const CellSubRow = styled.span` + display: flex; + flex-direction: row; + gap: 0.5rem; + margin-top: 0.5rem; + font-size: 0.875rem; + + svg { + align-self: center; + margin: 0; + } +`; + +const CenteredTh = styled.th` + text-align: center; +`; + +const CenteredTd = styled.td` + text-align: center; + vertical-align: middle !important; +`; + +const StyledH3 = styled.h3` + padding: 0; + margin: 0 0 0.75rem; + text-align: left; + font-size: 1.5rem; + font-weight: 500; +`; + +const None = styled.span` + font-style: italic; + color: #727272; + padding: 12px; + + &.bordered { + border-top: 1px solid #d2d5d9; + } +`; + +const TestPlans = ({ testPlanVersions }) => { + const [atExpandTableItems, setAtExpandTableItems] = useState({ + 1: true, + 2: true, + 3: true + }); + + const none = None; + const borderedNone = None; + + const onClickExpandAtTable = atId => { + // { jaws/nvda/vo: boolean } ] + if (!atExpandTableItems[atId]) + setAtExpandTableItems({ ...atExpandTableItems, [atId]: true }); + else + setAtExpandTableItems({ + ...atExpandTableItems, + [atId]: !atExpandTableItems[atId] + }); + }; + + const testPlanReportsExist = testPlanVersions.some( + testPlanVersion => testPlanVersion.testPlanReports.length + ); + + if (!testPlanReportsExist) { + return ( + + + Candidate Review | ARIA-AT + +

    Candidate Review

    +

    + There are no results to show just yet. Please check back + soon! +

    +
    + ); + } + + const getRowStatus = ({ + issues = [], + isInProgressStatusExists, + isApprovedStatusExists + }) => { + let issueChangesRequestedTypeCount = 0; + let issueFeedbackTypeCount = 0; + + for (let i = 0; i < issues.length; i++) { + if (issues[i].feedbackType === 'CHANGES_REQUESTED') + issueChangesRequestedTypeCount++; + else issueFeedbackTypeCount++; + } + + const changesRequestedContent = ( + <> + + + Changes requested for {issueChangesRequestedTypeCount} test + {issueChangesRequestedTypeCount !== 1 ? 's' : ''} + + + ); + + const issueFeedbackContent = ( + <> + + + Feedback left for {issueFeedbackTypeCount} test + {issueFeedbackTypeCount !== 1 ? 's' : ''} + + + ); + + const approvedContent = ( + <> + + + Approved + + + ); + + const inProgressContent = ( + <> + + + Review in Progress + + + ); + + const readyForReviewContent = ( + <> + + + Ready for Review + + + ); + + let result = null; + if (issueChangesRequestedTypeCount) result = changesRequestedContent; + else if (issueFeedbackTypeCount) { + result = issueFeedbackContent; + if (isApprovedStatusExists) + result = ( + <> + {result && ( + <> + {result} +
    +
    + + )} + {approvedContent} + + ); + } else if (isInProgressStatusExists) result = inProgressContent; + else if (isApprovedStatusExists) result = approvedContent; + else result = readyForReviewContent; + + return result; + }; + + const evaluateTestsAssertionsMessage = ({ + totalSupportPercent, + browsersLength, + totalTestsFailedCount, + totalAssertionsFailedCount + }) => { + if (totalSupportPercent === 100) { + return ( + <> + No assertions failed + + ); + } else { + return ( + <> + + {totalAssertionsFailedCount} assertion + {totalAssertionsFailedCount !== 1 ? 's' : ''} + {' '} + failed across{' '} + + {totalTestsFailedCount} test + {totalTestsFailedCount !== 1 ? 's' : ''} + {' '} + run with{' '} + + {browsersLength} browser + {browsersLength !== 1 ? 's' : ''} + + + ); + } + }; + + const uniqueFilter = (element, unique, key) => { + const isDuplicate = unique.includes(element[key]); + if (!isDuplicate) { + unique.push(element[key]); + return true; + } + return false; + }; + + const constructTableForAtById = (atId, atName) => { + const testPlanReportsForAtExists = testPlanVersions.some( + testPlanVersion => + testPlanVersion.testPlanReports.some( + testPlanReport => testPlanReport.at.id == atId + ) + ); + + // return 'None' element if no reports exists for AT + if (!testPlanReportsForAtExists) { + return ( + +

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

    + + {borderedNone} + +
    + ); + } + + let testPlanTargetsById = {}; + testPlanVersions.forEach(testPlanVersion => { + const { testPlanReports } = testPlanVersion; + + testPlanReports.forEach(testPlanReport => { + const { at, browser } = testPlanReport; + // Construct testPlanTarget + const testPlanTarget = { + id: `${at.id}${browser.id}`, + at, + browser + }; + testPlanTargetsById[testPlanTarget.id] = testPlanTarget; + }); + }); + testPlanTargetsById = alphabetizeObjectBy( + testPlanTargetsById, + keyValue => getTestPlanTargetTitle(keyValue[1]) + ); + + return ( + +

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

    + + + + + + + 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.filter( + ({ at }) => at.id === atId + ); + 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 && + testPlanReport.at.id === + atId + ) { + dataExists = true; + } + + 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.supportPercent, + 0 + ) / allMetrics.length + ) || 0 + }; + + // Make sure issues are unique + const uniqueLinks = []; + const allIssues = testPlanReports + .map(testPlanReport => [ + ...testPlanReport.issues + ]) + .flat() + .filter( + t => t.isCandidateReview === true + ) + .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' + ) + })} + + + + + + + + {evaluateTestsAssertionsMessage( + metrics + )} + + + + + ) + ); + })} + +
    Candidate Test Plans
    + + {getTestPlanVersionTitle( + testPlanVersion + )}{' '} + V + {convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}{' '} + ({testsCount} Test + {testsCount === 0 || + testsCount > 1 + ? `s` + : ''} + ) + +
    +
    +
    + ); + }; + + const constructTableForResultsSummary = () => { + if (!testPlanReportsExist) return borderedNone; + + let testPlanTargetsById = {}; + testPlanVersions.forEach(testPlanVersion => { + const { testPlanReports } = testPlanVersion; + + testPlanReports.forEach(testPlanReport => { + const { at, browser } = testPlanReport; + // Construct testPlanTarget + const testPlanTarget = { + id: `${at.id}${browser.id}`, + at, + browser + }; + testPlanTargetsById[testPlanTarget.id] = testPlanTarget; + }); + }); + testPlanTargetsById = alphabetizeObjectBy( + testPlanTargetsById, + keyValue => getTestPlanTargetTitle(keyValue[1]) + ); + + return ( + <> + Review Status Summary + + + + + JAWS + NVDA + VoiceOver for macOS + + + + {Object.values(testPlanVersions) + .sort((a, b) => (a.title < b.title ? -1 : 1)) + .map(testPlanVersion => { + const testPlanReports = + testPlanVersion.testPlanReports; + + let jawsDataExists = false; + let nvdaDataExists = false; + let voDataExists = false; + + Object.values(testPlanTargetsById).map( + testPlanTarget => { + const testPlanReport = + testPlanReports.find( + testPlanReport => + testPlanReport.at.id === + testPlanTarget.at.id && + testPlanReport.browser + .id === + testPlanTarget.browser + .id + ); + + if (testPlanReport) { + if ( + !jawsDataExists && + testPlanReport.at.id === '1' + ) { + jawsDataExists = true; + } + if ( + !nvdaDataExists && + testPlanReport.at.id === '2' + ) { + nvdaDataExists = true; + } + if ( + !voDataExists && + testPlanReport.at.id === '3' + ) { + voDataExists = true; + } + } + } + ); + + const allJawsIssues = []; + const allNvdaIssues = []; + const allVoIssues = []; + + const jawsTestPlanReports = + testPlanReports.filter(t => { + if (t.at.id === '1') { + allJawsIssues.push(...t.issues); + return true; + } else return false; + }); + const nvdaTestPlanReports = + testPlanReports.filter(t => { + if (t.at.id === '2') { + allNvdaIssues.push(...t.issues); + return true; + } else return false; + }); + const voTestPlanReports = + testPlanReports.filter(t => { + if (t.at.id === '3') { + allVoIssues.push(...t.issues); + return true; + } else return false; + }); + + const uniqueLinks = []; + const jawsIssues = allJawsIssues.filter(t => + uniqueFilter(t, uniqueLinks, 'link') + ); + const nvdaIssues = allNvdaIssues.filter(t => + uniqueFilter(t, uniqueLinks, 'link') + ); + const voIssues = allVoIssues.filter(t => + uniqueFilter(t, uniqueLinks, 'link') + ); + + return ( + + + + {jawsDataExists + ? getRowStatus({ + issues: jawsIssues, + isInProgressStatusExists: + jawsTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'IN_PROGRESS' + ), + isApprovedStatusExists: + jawsTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'APPROVED' + ) + }) + : none} + + + {nvdaDataExists + ? getRowStatus({ + issues: nvdaIssues, + isInProgressStatusExists: + nvdaTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'IN_PROGRESS' + ), + isApprovedStatusExists: + nvdaTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'APPROVED' + ) + }) + : none} + + + {voDataExists + ? getRowStatus({ + issues: voIssues, + isInProgressStatusExists: + voTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'IN_PROGRESS' + ), + isApprovedStatusExists: + voTestPlanReports.some( + testPlanReport => + testPlanReport.vendorReviewStatus === + 'APPROVED' + ) + }) + : none} + + + ); + })} + +
    Test Plan
    + {getTestPlanVersionTitle( + testPlanVersion + )}{' '} + V + {convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )} +
    + + ); + }; + + return ( + + + Candidate Review | ARIA-AT + +

    Candidate Review

    +

    Introduction

    +

    + This page summarizes the test results for each AT and Browser + which executed the Test Plan. +

    + {constructTableForAtById('1', 'JAWS')} + {constructTableForAtById('2', 'NVDA')} + {constructTableForAtById('3', 'VoiceOver for macOS')} + {constructTableForResultsSummary()} +
    + ); +}; + +TestPlans.propTypes = { + testPlanVersions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + phase: PropTypes.string.isRequired, + gitSha: PropTypes.string, + testPlan: PropTypes.shape({ + directory: PropTypes.string + }), + metadata: PropTypes.object, + testPlanReports: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + metrics: PropTypes.object.isRequired, + at: PropTypes.object.isRequired, + browser: PropTypes.object.isRequired + }) + ) + }) + ).isRequired, + triggerPageUpdate: PropTypes.func +}; + +export default TestPlans; 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/CandidateReview/queries.js b/client/components/CandidateReview/queries.js new file mode 100644 index 000000000..02c82e7b2 --- /dev/null +++ b/client/components/CandidateReview/queries.js @@ -0,0 +1,53 @@ +import { gql } from '@apollo/client'; + +export const CANDIDATE_REVIEW_PAGE_QUERY = gql` + query { + testPlanVersions(phases: [CANDIDATE]) { + id + phase + title + gitSha + testPlan { + directory + } + metadata + updatedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + testPlanReports(isFinal: true) { + id + metrics + at { + id + name + } + latestAtVersionReleasedAt { + id + name + releasedAt + } + browser { + id + name + } + testPlanVersion { + id + title + gitSha + testPlan { + directory + } + metadata + updatedAt + } + vendorReviewStatus + issues { + link + isOpen + isCandidateReview + feedbackType + } + } + } + } +`; diff --git a/client/components/CandidateTests/TestPlans/index.jsx b/client/components/CandidateTests/TestPlans/index.jsx deleted file mode 100644 index aeea2a299..000000000 --- a/client/components/CandidateTests/TestPlans/index.jsx +++ /dev/null @@ -1,1290 +0,0 @@ -import React, { useRef, 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 { Link } from 'react-router-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faFlag, - faCheck, - faChevronUp, - faChevronDown, - faCommentAlt -} from '@fortawesome/free-solid-svg-icons'; -import alphabetizeObjectBy from '@client/utils/alphabetizeObjectBy'; -import { - getTestPlanTargetTitle, - getTestPlanVersionTitle -} from '@components/Reports/getTitles'; -import { - LoadingStatus, - useTriggerLoad -} from '@components/common/LoadingStatus'; -import { - UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION, - UPDATE_TEST_PLAN_REPORT_RECOMMENDED_TARGET_DATE_MUTATION -} from '@components/TestQueue/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 './TestPlans.css'; - -const FullHeightContainer = styled(Container)` - min-height: calc(100vh - 64px); -`; - -const StatusText = styled.span` - height: 1.625em; - font-size: 0.875em; - padding: 4px 10px; - border-radius: 1.625rem; - - overflow: hidden; - white-space: nowrap; - - &.feedback { - border: 2px solid #b253f8; - svg { - color: #b253f8; - } - } - - &.changes-requested { - border: 2px solid #f87f1b; - svg { - color: #f87f1b; - } - } - - &.ready-for-review { - border: 2px solid #edbb1d; - - span.dot { - height: 10px; - width: 10px; - padding: 0; - margin-right: 8px; - border-radius: 50%; - background: #edbb1d; - } - } - - &.in-progress { - border: 2px solid #2560ab; - - span.dot { - height: 10px; - width: 10px; - padding: 0; - margin-right: 8px; - border-radius: 50%; - background: #2560ab; - } - } - - &.approved { - border: 2px solid #309d08; - svg { - color: #309d08; - } - } -`; - -const DisclosureParent = styled.div` - border: 1px solid #d3d5da; - border-radius: 3px; - margin-bottom: 3rem; - - h3 { - margin: 0; - padding: 0; - } -`; - -const DisclosureButton = styled.button` - position: relative; - width: 100%; - margin: 0; - padding: 0.75rem; - text-align: left; - font-size: 1.5rem; - font-weight: 500; - border: none; - border-radius: 3px; - background-color: transparent; - - &:hover, - &:focus { - padding: 0.75rem; - border: 0 solid #005a9c; - background-color: #def; - cursor: pointer; - } - - svg { - position: absolute; - margin: 0; - top: 50%; - right: 1.25rem; - - color: #969696; - transform: translateY(-50%); - } -`; - -const DisclosureContainer = styled.div` - display: ${({ show }) => (show ? 'flex' : 'none')}; - flex-direction: column; - gap: 1.25rem; - - background-color: #f8f9fa; - - table { - margin-bottom: 0; - } -`; - -const CellSubRow = styled.span` - display: flex; - flex-direction: row; - gap: 1rem; - margin-top: 0.5rem; - font-size: 0.875rem; -`; - -const CenteredTh = styled.th` - text-align: center; -`; - -const CenteredTd = styled.td` - text-align: center; - vertical-align: middle !important; -`; - -const StyledH3 = styled.h3` - padding: 0; - margin: 0 0 0.75rem; - text-align: left; - font-size: 1.5rem; - font-weight: 500; -`; - -const None = styled.span` - font-style: italic; - color: #727272; - padding: 12px; - - &.bordered { - border-top: 1px solid #d2d5d9; - } -`; - -const TestPlans = ({ - candidateTestPlanReports, - recommendedTestPlanReports, - 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 [updateTestPlanReportRecommendedTargetDate] = useMutation( - UPDATE_TEST_PLAN_REPORT_RECOMMENDED_TARGET_DATE_MUTATION - ); - - const changeTargetDateButtonRefs = useRef({}); - const focusButtonRef = useRef(); - - const [atExpandTableItems, setAtExpandTableItems] = useState({ - 1: true, - 2: true, - 3: true - }); - const [showUpdateTargetDateModal, setShowUpdateTargetDateModal] = - useState(false); - const [updateTargetDateModalTitle, setUpdateTargetDateModalTitle] = - useState(''); - const [updateTargetDateModalDateText, setUpdateTargetDateModalDateText] = - useState(''); - const [testPlanReportsToUpdate, setTestPlanReportsToUpdate] = useState([]); - - const none = None; - const borderedNone = None; - - const onClickExpandAtTable = atId => { - // { jaws/nvda/vo: boolean } ] - if (!atExpandTableItems[atId]) - setAtExpandTableItems({ ...atExpandTableItems, [atId]: true }); - else - setAtExpandTableItems({ - ...atExpandTableItems, - [atId]: !atExpandTableItems[atId] - }); - }; - - 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}); - } - }; - - // Compare testPlanReports with recommendedTestPlanReports to make sure there aren't any test - // plan reports which will never be shown on the Reports page when promoted - const ignoredIds = []; - - recommendedTestPlanReports.forEach(r => { - candidateTestPlanReports.forEach(t => { - if ( - !ignoredIds.includes(t.id) && - t.at.id == r.at.id && - t.browser.id == r.browser.id && - t.testPlanVersion.testPlan.directory == - r.testPlanVersion.testPlan.directory && - new Date(t.latestAtVersionReleasedAt.releasedAt) < - new Date(r.latestAtVersionReleasedAt.releasedAt) - ) - ignoredIds.push(t.id); - }); - }); - - const testPlanReports = candidateTestPlanReports.filter( - t => !ignoredIds.includes(t.id) - ); - - if (!testPlanReports.length) { - return ( - - - Candidate Tests | ARIA-AT - -

    Candidate Tests

    -

    - There are no results to show just yet. Please check back - soon! -

    -
    - ); - } - - const getRowStatus = ({ - issues = [], - isInProgressStatusExists, - isApprovedStatusExists - }) => { - let issueChangesRequestedTypeCount = 0; - let issueFeedbackTypeCount = 0; - - for (let i = 0; i < issues.length; i++) { - if (issues[i].feedbackType === 'CHANGES_REQUESTED') - issueChangesRequestedTypeCount++; - else issueFeedbackTypeCount++; - } - - const changesRequestedContent = ( - <> - - - Changes requested for {issueChangesRequestedTypeCount} test - {issueChangesRequestedTypeCount !== 1 ? 's' : ''} - - - ); - - const issueFeedbackContent = ( - <> - - - Feedback left for {issueFeedbackTypeCount} test - {issueFeedbackTypeCount !== 1 ? 's' : ''} - - - ); - - const approvedContent = ( - <> - - - Approved - - - ); - - const inProgressContent = ( - <> - - - Review in Progress - - - ); - - const readyForReviewContent = ( - <> - - - Ready for Review - - - ); - - let result = null; - if (issueChangesRequestedTypeCount) result = changesRequestedContent; - else if (issueFeedbackTypeCount) { - result = issueFeedbackContent; - if (isApprovedStatusExists) - result = ( - <> - {result && ( - <> - {result} -
    -
    - - )} - {approvedContent} - - ); - } else if (isInProgressStatusExists) result = inProgressContent; - else if (isApprovedStatusExists) result = approvedContent; - else result = readyForReviewContent; - - return result; - }; - - const evaluateTestsAssertionsMessage = ({ - totalSupportPercent, - browsersLength, - totalTestsFailedCount, - totalAssertionsFailedCount - }) => { - if (totalSupportPercent === 100) { - return ( - <> - No assertions failed - - ); - } else { - return ( - <> - - {totalAssertionsFailedCount} assertion - {totalAssertionsFailedCount !== 1 ? 's' : ''} - {' '} - failed across{' '} - - {totalTestsFailedCount} test - {totalTestsFailedCount !== 1 ? 's' : ''} - {' '} - run with{' '} - - {browsersLength} browser - {browsersLength !== 1 ? 's' : ''} - - - ); - } - }; - - const uniqueFilter = (element, unique, key) => { - const isDuplicate = unique.includes(element[key]); - if (!isDuplicate) { - unique.push(element[key]); - return true; - } - return false; - }; - - const constructTableForAtById = (atId, atName) => { - const testPlanReportsByAtId = testPlanReports.filter( - t => t.at.id === atId - ); - - // return 'None' element if no reports exists for AT - if (!testPlanReportsByAtId.length) { - return ( - -

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

    - - {borderedNone} - -
    - ); - } - - const testPlanReportsById = {}; - let testPlanTargetsById = {}; - let testPlanVersionsById = {}; - testPlanReportsByAtId.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; - }); - }); - testPlanReportsByAtId.forEach(testPlanReport => { - const { testPlanVersion, at, browser } = testPlanReport; - const directory = testPlanVersion.testPlan.directory; - - // Construct testPlanTarget - const testPlanTarget = { id: `${at.id}${browser.id}`, at, browser }; - - if (!tabularReports[testPlanVersion.id][testPlanTarget.id]) - tabularReports[testPlanVersion.id][testPlanTarget.id] = - testPlanReport; - - if ( - !tabularReportsByDirectory[directory][testPlanVersion.id][ - testPlanTarget.id - ] - ) - tabularReportsByDirectory[directory][testPlanVersion.id][ - testPlanTarget.id - ] = testPlanReport; - - if ( - !tabularReportsByDirectory[directory][testPlanVersion.id] - .testPlanVersion - ) - tabularReportsByDirectory[directory][ - testPlanVersion.id - ].testPlanVersion = testPlanVersion; - }); - - return ( - - -

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

    - - - - - - Review Status - Results Summary - - - - {Object.values(tabularReportsByDirectory) - .sort((a, b) => - Object.values(a)[0].testPlanVersion - .title < - Object.values(b)[0].testPlanVersion - .title - ? -1 - : 1 - ) - .map(tabularReport => { - let reportResult = null; - let testPlanVersionId = null; - - // Evaluate what is prioritised across the - // collection of testPlanVersions - 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; - - // All testPlanReports across browsers per AT - const testPlanReports = []; - const allMetrics = []; - - let candidateStatusReachedAt; - let recommendedStatusTargetDate; - let testsCount = 0; - - Object.values(testPlanTargetsById).map( - testPlanTarget => { - const testPlanReport = - reportResult[ - testPlanTarget.id - ]; - - if (testPlanReport) { - const metrics = - testPlanReport.metrics; - testPlanReports.push( - testPlanReport - ); - allMetrics.push(metrics); - - const { - candidateStatusReachedAt: - testPlanReportCandidateStatusReachedAt, - recommendedStatusTargetDate: - testPlanReportRecommendedStatusTargetDate - } = testPlanReport; - - if ( - !candidateStatusReachedAt - ) { - candidateStatusReachedAt = - testPlanReportCandidateStatusReachedAt; - } - // Use earliest candidateStatusReachedAt across browser results for AT - else { - candidateStatusReachedAt = - new Date( - testPlanReportCandidateStatusReachedAt - ) < - new Date( - candidateStatusReachedAt - ) - ? testPlanReportCandidateStatusReachedAt - : candidateStatusReachedAt; - } - - if ( - !recommendedStatusTargetDate - ) { - recommendedStatusTargetDate = - testPlanReportRecommendedStatusTargetDate; - } - // Use latest recommendedStatusTargetDate across browser results for AT - else { - recommendedStatusTargetDate = - new Date( - testPlanReportRecommendedStatusTargetDate - ) > - new Date( - recommendedStatusTargetDate - ) - ? testPlanReportRecommendedStatusTargetDate - : recommendedStatusTargetDate; - } - - 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.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' - ) - ); - - return ( - - - - {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` - : ''} - ) - - - - Candidate Phase - Start Date{' '} - - {convertDateToString( - candidateStatusReachedAt, - 'MMM D, YYYY' - )} - - - - Target Completion - Date{' '} - - {convertDateToString( - recommendedStatusTargetDate, - 'MMM D, YYYY' - )} - - - - - - - Mark as ... - - - { - await updateReportStatus( - testPlanReports, - 'DRAFT' - ); - }} - > - Draft - - { - await updateReportStatus( - testPlanReports, - 'RECOMMENDED' - ); - }} - disabled={testPlanReports.some( - t => - t.vendorReviewStatus !== - 'APPROVED' - )} - > - Recommended - - - - - -
    -
    -
    -
    - ); - }; - - 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 }; - }; - - const constructTableForResultsSummary = () => { - if (!testPlanReports.length) return borderedNone; - - 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; - }); - - return ( - <> - Review Status Summary - - - - - JAWS - NVDA - VoiceOver for macOS - - - - {Object.values(tabularReportsByDirectory) - .sort((a, b) => - Object.values(a)[0].testPlanVersion.title < - Object.values(b)[0].testPlanVersion.title - ? -1 - : 1 - ) - .map(tabularReport => { - let reportResult = null; - let testPlanVersionId = null; - - // Evaluate what is prioritised across the - // collection of testPlanVersions - 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; - - const testPlanReports = []; - let jawsDataExists = false; - let nvdaDataExists = false; - let voDataExists = false; - - Object.values(testPlanTargetsById).map( - testPlanTarget => { - const testPlanReport = - reportResult[testPlanTarget.id]; - - if (testPlanReport) { - testPlanReports.push( - testPlanReport - ); - if ( - !jawsDataExists && - testPlanReport.at.id === '1' - ) { - jawsDataExists = true; - } - if ( - !nvdaDataExists && - testPlanReport.at.id === '2' - ) { - nvdaDataExists = true; - } - if ( - !voDataExists && - testPlanReport.at.id === '3' - ) { - voDataExists = true; - } - } - } - ); - - const allJawsIssues = []; - const allNvdaIssues = []; - const allVoIssues = []; - - const jawsTestPlanReports = - testPlanReports.filter(t => { - if (t.at.id === '1') { - allJawsIssues.push(...t.issues); - return true; - } else return false; - }); - const nvdaTestPlanReports = - testPlanReports.filter(t => { - if (t.at.id === '2') { - allNvdaIssues.push(...t.issues); - return true; - } else return false; - }); - const voTestPlanReports = - testPlanReports.filter(t => { - if (t.at.id === '3') { - allVoIssues.push(...t.issues); - return true; - } else return false; - }); - - const uniqueLinks = []; - const jawsIssues = allJawsIssues.filter(t => - uniqueFilter(t, uniqueLinks, 'link') - ); - const nvdaIssues = allNvdaIssues.filter(t => - uniqueFilter(t, uniqueLinks, 'link') - ); - const voIssues = allVoIssues.filter(t => - uniqueFilter(t, uniqueLinks, 'link') - ); - - return ( - - - - {jawsDataExists - ? getRowStatus({ - issues: jawsIssues, - isInProgressStatusExists: - jawsTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'IN_PROGRESS' - ), - isApprovedStatusExists: - jawsTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'APPROVED' - ) - }) - : none} - - - {nvdaDataExists - ? getRowStatus({ - issues: nvdaIssues, - isInProgressStatusExists: - nvdaTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'IN_PROGRESS' - ), - isApprovedStatusExists: - nvdaTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'APPROVED' - ) - }) - : none} - - - {voDataExists - ? getRowStatus({ - issues: voIssues, - isInProgressStatusExists: - voTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'IN_PROGRESS' - ), - isApprovedStatusExists: - voTestPlanReports.some( - testPlanReport => - testPlanReport.vendorReviewStatus === - 'APPROVED' - ) - }) - : none} - - - ); - })} - -
    Test Plan
    - {getTestPlanVersionTitle( - testPlanVersion - )} -
    - - ); - }; - - const onUpdateTargetDateAction = async ({ updatedDateText }) => { - onUpdateTargetDateModalClose(); - try { - const updateTestPlanReportPromises = testPlanReportsToUpdate.map( - testPlanReport => { - return updateTestPlanReportRecommendedTargetDate({ - variables: { - testReportId: testPlanReport.id, - recommendedStatusTargetDate: - convertStringFormatToAnotherFormat( - updatedDateText - ) - } - }); - } - ); - - await triggerLoad(async () => { - await Promise.all(updateTestPlanReportPromises); - 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 Tests

    -

    Introduction

    -

    - This page summarizes the test results for each AT and Browser - which executed the Test Plan. -

    - {constructTableForAtById('1', 'JAWS')} - {constructTableForAtById('2', 'NVDA')} - {constructTableForAtById('3', 'VoiceOver for macOS')} - {constructTableForResultsSummary()} - {showThemedModal && themedModal} - {showUpdateTargetDateModal && ( - - )} -
    - ); -}; - -TestPlans.propTypes = { - candidateTestPlanReports: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - testPlanVersion: PropTypes.shape({ - id: PropTypes.string.isRequired, - title: PropTypes.string, - testPlan: PropTypes.shape({ - directory: PropTypes.string.isRequired - }).isRequired - }).isRequired - }) - ).isRequired, - recommendedTestPlanReports: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - testPlanVersion: PropTypes.shape({ - id: PropTypes.string.isRequired, - title: PropTypes.string, - testPlan: PropTypes.shape({ - directory: PropTypes.string.isRequired - }).isRequired - }).isRequired - }) - ).isRequired, - triggerPageUpdate: PropTypes.func -}; - -export default TestPlans; diff --git a/client/components/CandidateTests/index.jsx b/client/components/CandidateTests/index.jsx deleted file mode 100644 index a7dae1673..000000000 --- a/client/components/CandidateTests/index.jsx +++ /dev/null @@ -1,53 +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 candidateTestPlanReports = data.testPlanReports.filter( - t => t.status === 'CANDIDATE' - ); - const recommendedTestPlanReports = data.testPlanReports.filter( - t => t.status === 'RECOMMENDED' - ); - - return ( - - ); -}; - -export default CandidateTests; diff --git a/client/components/CandidateTests/queries.js b/client/components/CandidateTests/queries.js deleted file mode 100644 index 9670912d3..000000000 --- a/client/components/CandidateTests/queries.js +++ /dev/null @@ -1,42 +0,0 @@ -import { gql } from '@apollo/client'; - -export const CANDIDATE_TESTS_PAGE_QUERY = gql` - query { - testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) { - id - status - metrics - at { - id - name - } - latestAtVersionReleasedAt { - id - name - releasedAt - } - browser { - id - name - } - testPlanVersion { - id - title - gitSha - testPlan { - directory - } - metadata - updatedAt - } - vendorReviewStatus - candidateStatusReachedAt - recommendedStatusTargetDate - issues { - link - isOpen - feedbackType - } - } - } -`; diff --git a/client/components/DataManagement/DataManagement.css b/client/components/DataManagement/DataManagement.css new file mode 100644 index 000000000..5513e08cf --- /dev/null +++ b/client/components/DataManagement/DataManagement.css @@ -0,0 +1,52 @@ +.data-management.table { + padding: 0; + margin: 0; + height: 100%; +} + +.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; + padding: 0; + height: 100%; +} + +.data-management.table tbody tr td > * { + padding: 0.75rem; +} + +.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..eabe4e9c6 --- /dev/null +++ b/client/components/DataManagement/DataManagementRow/index.jsx @@ -0,0 +1,1133 @@ +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 { + 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 TestPlanReportStatusDialogWithButton from '../../TestPlanReportStatusDialog/WithButton'; +import ReportStatusDot from '../../common/ReportStatusDot'; +import UpdateTargetDateModal from '@components/common/UpdateTargetDateModal'; +import VersionString from '../../common/VersionString'; +import PhasePill from '../../common/PhasePill'; +import { uniq as unique, uniqBy as uniqueBy } from 'lodash'; +import { getVersionData } from '../utils'; + +const StatusCell = styled.div` + display: flex; + flex-direction: column; + height: 100%; + + .review-text { + margin-top: 1rem; + font-size: 14px; + text-align: center; + + margin-bottom: 88px; + } + + .versions-in-progress { + 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` + padding: 0 !important; /* override padding for td and add margins into specific children */ + + display: flex; + flex-direction: column; + height: 100%; + + > span.review-complete { + display: block; + font-size: 14px; + text-align: center; + margin: 12px 0.75rem; + color: #333f4d; + } + + > span.more { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + padding: 0.5rem; + font-size: 14px; + + margin-top: 6px; + + color: #6a7989; + background: #f6f8fa; + + > span.more-issues-container { + width: 100%; + text-align: center; + + .issues { + margin-right: 4px; + } + + align-items: center; + } + + > span.target-days-container { + text-align: center; + + button { + appearance: none; + border: none; + background: none; + color: inherit; + + margin: 0; + padding: 0; + } + } + } + + > .advance-button { + margin: 12px 0.75rem; + width: calc(100% - 1.5rem); + } +`; + +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, + tableRowIndex +}) => { + 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]); + + 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; + } + + const dateString = convertDateToString(versionDate, 'MMM D, YYYY'); + + return ( + <> + {phase} +

    + {phaseText} + {dateString} +

    + + ); + }; + + 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 ( + + + {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 ( + + + + 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 = []; + 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)) + coveredReports.push(value); + }); + + // Phase is "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + {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 ( + + + + 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 uniqueAtsCount = unique( + testPlanVersions + .flatMap( + testPlanVersion => + testPlanVersion.testPlanReports + ) + .filter( + testPlanReport => testPlanReport.issues.length + ) + .map(testPlanReport => testPlanReport.at.id) + ).length; + + const issuesCount = uniqueBy( + testPlanVersions.flatMap(testPlanVersion => + testPlanVersion.testPlanReports.flatMap( + testPlanReport => + testPlanReport.issues.filter( + issue => issue.isOpen + ) + ) + ), + item => item.link + ).length; + + // 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 markedFinalAt = testPlanReport.markedFinalAt; + const atName = testPlanReport.at.name; + const browserName = testPlanReport.browser.name; + const value = `${atName}_${browserName}`; + + if (markedFinalAt && !coveredReports.includes(value)) + coveredReports.push(value); + }); + + // Phase is "active" + insertActivePhaseForTestPlan(latestVersion); + return ( + + + {shouldShowAdvanceButton && ( + + )} + + + + + + + {issuesCount} Open Issue + {`${issuesCount === 1 ? '' : 's'}`} + {`${ + issuesCount >= 2 + ? ` from ${uniqueAtsCount} ATs` + : '' + }`} + + + {isAdmin ? ( + + ) : ( + <> + Target  + + {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 ( + + + + + + + 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, + tableRowIndex: PropTypes.number.isRequired, + setTestPlanVersions: PropTypes.func +}; + +export default DataManagementRow; diff --git a/client/components/DataManagement/filterSortHooks.js b/client/components/DataManagement/filterSortHooks.js new file mode 100644 index 000000000..7123346fd --- /dev/null +++ b/client/components/DataManagement/filterSortHooks.js @@ -0,0 +1,235 @@ +import { useMemo, useState } from 'react'; +import { + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS, + DATA_MANAGEMENT_TABLE_SORT_OPTIONS, + getVersionData +} from './utils'; +import { TEST_PLAN_VERSION_PHASES } from '../../utils/constants'; +import { TABLE_SORT_ORDERS } from '../common/SortableTableHeader'; + +export const useTestPlanVersionsByPhase = testPlanVersions => { + const testPlanVersionsByPhase = useMemo(() => { + const initialPhases = Object.keys(TEST_PLAN_VERSION_PHASES).reduce( + (acc, key) => { + acc[TEST_PLAN_VERSION_PHASES[key]] = []; + return acc; + }, + {} + ); + + return testPlanVersions.reduce((acc, testPlanVersion) => { + acc[testPlanVersion.phase].push(testPlanVersion); + return acc; + }, initialPhases); + }, [testPlanVersions]); + + return { testPlanVersionsByPhase }; +}; + +export const useDerivedOverallPhaseByTestPlanId = ( + testPlans, + testPlanVersions +) => { + const { testPlanVersionsByPhase } = + useTestPlanVersionsByPhase(testPlanVersions); + + const getVersionDataByDirectory = (directory, phaseKey, phaseTimeKey) => { + if ( + testPlanVersionsByPhase[phaseKey].some( + testPlanVersion => + testPlanVersion.testPlan.directory === directory + ) + ) { + const { earliestVersion, latestVersion } = getVersionData( + testPlanVersionsByPhase[phaseKey], + phaseTimeKey + ); + return phaseTimeKey ? earliestVersion?.phase : latestVersion?.phase; + } + return undefined; + }; + + const derivedOverallPhaseByTestPlanId = useMemo(() => { + const derivedOverallPhaseByTestPlanId = {}; + const phases = [ + { + phase: TEST_PLAN_VERSION_PHASES.RECOMMENDED, + timeKey: 'recommendedPhaseReachedAt' + }, + { + phase: TEST_PLAN_VERSION_PHASES.CANDIDATE, + timeKey: 'candidatePhaseReachedAt' + }, + { + phase: TEST_PLAN_VERSION_PHASES.DRAFT, + timeKey: 'draftPhaseReachedAt' + }, + { phase: TEST_PLAN_VERSION_PHASES.RD } + ]; + for (const testPlan of testPlans) { + for (const { phase, timeKey } of phases) { + const derivedPhase = getVersionDataByDirectory( + testPlan.directory, + phase, + timeKey + ); + if (derivedPhase) { + derivedOverallPhaseByTestPlanId[testPlan.id] = derivedPhase; + break; + } + } + } + return derivedOverallPhaseByTestPlanId; + }, [testPlans, testPlanVersions]); + + return { derivedOverallPhaseByTestPlanId }; +}; + +export const useTestPlansByPhase = (testPlans, testPlanVersions) => { + const { derivedOverallPhaseByTestPlanId } = + useDerivedOverallPhaseByTestPlanId(testPlans, testPlanVersions); + + const testPlansByPhase = useMemo(() => { + const testPlansByPhase = {}; + for (const key of Object.keys(TEST_PLAN_VERSION_PHASES)) { + testPlansByPhase[TEST_PLAN_VERSION_PHASES[key]] = []; + } + for (const testPlan of testPlans) { + testPlansByPhase[ + derivedOverallPhaseByTestPlanId[testPlan.id] + ]?.push(testPlan); + } + return testPlansByPhase; + }, [derivedOverallPhaseByTestPlanId]); + + return { testPlansByPhase }; +}; + +export const useDataManagementTableFiltering = ( + testPlans, + testPlanVersions, + filter +) => { + const { testPlansByPhase } = useTestPlansByPhase( + testPlans, + testPlanVersions + ); + + const filteredTestPlans = useMemo(() => { + if (!filter || filter === DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL) { + return testPlans; + } else { + return testPlansByPhase[filter]; + } + }, [filter, testPlansByPhase, testPlans]); + + const filterLabels = { + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL]: `All Plans (${testPlans.length})` + }; + + if (testPlansByPhase[TEST_PLAN_VERSION_PHASES.RD].length > 0) { + filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RD + ] = `R&D Complete (${ + testPlansByPhase[TEST_PLAN_VERSION_PHASES.RD].length + })`; + } + + if (testPlansByPhase[TEST_PLAN_VERSION_PHASES.DRAFT].length > 0) { + filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.DRAFT + ] = `In Draft Review (${ + testPlansByPhase[TEST_PLAN_VERSION_PHASES.DRAFT].length + })`; + } + + if (testPlansByPhase[TEST_PLAN_VERSION_PHASES.CANDIDATE].length > 0) { + filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.CANDIDATE + ] = `In Candidate Review (${ + testPlansByPhase[TEST_PLAN_VERSION_PHASES.CANDIDATE].length + })`; + } + + if (testPlansByPhase[TEST_PLAN_VERSION_PHASES.RECOMMENDED].length > 0) { + filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED + ] = `Recommended Plans (${ + testPlansByPhase[TEST_PLAN_VERSION_PHASES.RECOMMENDED].length + })`; + } + + return { filteredTestPlans, filterLabels }; +}; + +export const useDataManagementTableSorting = ( + testPlans, + testPlanVersions, + ats, + initialSortDirection = TABLE_SORT_ORDERS.ASC +) => { + const [activeSort, setActiveSort] = useState({ + key: DATA_MANAGEMENT_TABLE_SORT_OPTIONS.PHASE, + direction: initialSortDirection + }); + + const { derivedOverallPhaseByTestPlanId } = + useDerivedOverallPhaseByTestPlanId(testPlans, testPlanVersions); + + const sortedTestPlans = useMemo(() => { + // Ascending and descending interpreted differently for statuses + // (ascending = earlier phase first, descending = later phase first) + const phaseOrder = { + NOT_STARTED: 4, + RD: 3, + DRAFT: 2, + CANDIDATE: 1, + RECOMMENDED: 0 + }; + const directionMod = + activeSort.direction === TABLE_SORT_ORDERS.ASC ? -1 : 1; + + const sortByName = (a, b, dir = directionMod) => + dir * (a.title < b.title ? 1 : -1); + + const sortByAts = (a, b) => { + const countA = ats.length; // Stubs based on current rendering in DataManagementRow + const countB = ats.length; + if (countA === countB) return sortByName(a, b, -1); + return directionMod * (countA - countB); + }; + + const sortByPhase = (a, b) => { + const testPlanVersionOverallA = + derivedOverallPhaseByTestPlanId[a.id] ?? 'NOT_STARTED'; + const testPlanVersionOverallB = + derivedOverallPhaseByTestPlanId[b.id] ?? 'NOT_STARTED'; + if (testPlanVersionOverallA === testPlanVersionOverallB) { + return sortByName(a, b, -1); + } + return ( + directionMod * + (phaseOrder[testPlanVersionOverallA] - + phaseOrder[testPlanVersionOverallB]) + ); + }; + + const sortFunctions = { + NAME: sortByName, + ATS: sortByAts, + PHASE: sortByPhase + }; + + return testPlans.slice().sort(sortFunctions[activeSort.key]); + }, [activeSort, testPlans]); + + const updateSort = ({ key, direction }) => { + setActiveSort({ key, direction }); + }; + + return { + sortedTestPlans, + updateSort, + activeSort + }; +}; diff --git a/client/components/DataManagement/index.jsx b/client/components/DataManagement/index.jsx new file mode 100644 index 000000000..3c68d48d7 --- /dev/null +++ b/client/components/DataManagement/index.jsx @@ -0,0 +1,242 @@ +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'; +import SortableTableHeader, { + TABLE_SORT_ORDERS +} from '../common/SortableTableHeader'; +import FilterButtons from '../common/FilterButtons'; +import { + useDataManagementTableFiltering, + useDataManagementTableSorting +} from './filterSortHooks'; +import { + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS, + DATA_MANAGEMENT_TABLE_SORT_OPTIONS +} from './utils'; +import { AriaLiveRegionProvider } from '../providers/AriaLiveRegionProvider'; + +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 [filter, setFilter] = useState( + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL + ); + + 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]); + + const { filteredTestPlans, filterLabels } = useDataManagementTableFiltering( + testPlans, + testPlanVersions, + filter + ); + + const { sortedTestPlans, updateSort, activeSort } = + useDataManagementTableSorting( + filteredTestPlans, + testPlanVersions, + ats, + TABLE_SORT_ORDERS.DESC + ); + + 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 ? ( + <> +

    Introduction

    +

    + This page provides a view of the latest test plan + version information, and where they currently are in the{' '} + + ARIA-AT Community Group’s review process + + .
    + Use this page to manage Test Plans in the Test Queue and + their phases. +

    + + + + ) : ( + <> +

    Introduction

    +

    + This page provides a view of the latest test plan + version information, and where they currently are in the{' '} + + ARIA-AT Community Group’s review process + + . +

    + + )} + +

    Test Plans Status Summary

    + + + + + + + updateSort({ + key: DATA_MANAGEMENT_TABLE_SORT_OPTIONS.NAME, + direction + }) + } + /> + + updateSort({ + key: DATA_MANAGEMENT_TABLE_SORT_OPTIONS.ATS, + direction + }) + } + /> + + updateSort({ + key: DATA_MANAGEMENT_TABLE_SORT_OPTIONS.PHASE, + direction + }) + } + initialSortDirection={TABLE_SORT_ORDERS.DESC} + /> + + + + + + + + {sortedTestPlans.map((testPlan, index) => { + return ( + + testPlanVersion.testPlan + .directory === + testPlan.directory + )} + tableRowIndex={index} + setTestPlanVersions={setTestPlanVersions} + /> + ); + })} + +
    R&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..b7f4233d7 --- /dev/null +++ b/client/components/DataManagement/queries.js @@ -0,0 +1,176 @@ +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(phases: [RD, DRAFT, CANDIDATE, RECOMMENDED]) { + id + title + phase + gitSha + gitMessage + updatedAt + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + testPlan { + directory + } + testPlanReports { + id + metrics + markedFinalAt + at { + id + name + } + browser { + id + name + } + issues { + link + isOpen + feedbackType + } + draftTestPlanRuns { + tester { + username + } + testPlanReport { + id + } + testResults { + test { + id + } + atVersion { + id + name + } + browserVersion { + id + name + } + completedAt + } + } + } + } + } +`; + +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/DataManagement/utils.js b/client/components/DataManagement/utils.js new file mode 100644 index 000000000..292d3a270 --- /dev/null +++ b/client/components/DataManagement/utils.js @@ -0,0 +1,34 @@ +// Get the version information based on the latest or earliest date info from a group of + +import { TEST_PLAN_VERSION_PHASES } from '../../utils/constants'; + +// TestPlanVersions +export 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 + }; +}; + +export const DATA_MANAGEMENT_TABLE_SORT_OPTIONS = { + NAME: 'NAME', + ATS: 'ATS', + PHASE: 'PHASE' +}; + +export const DATA_MANAGEMENT_TABLE_FILTER_OPTIONS = { + ALL: 'ALL', + ...TEST_PLAN_VERSION_PHASES +}; diff --git a/client/components/DisclaimerInfo/index.jsx b/client/components/DisclaimerInfo/index.jsx index 8e04b54b8..822dd8739 100644 --- a/client/components/DisclaimerInfo/index.jsx +++ b/client/components/DisclaimerInfo/index.jsx @@ -70,6 +70,14 @@ const recommendedMessageContent = ( ); +const deprecatedTitle = 'Deprecated Report'; +const deprecatedMessageContent = ( + <> + The information in this report is generated from previously set + candidate or recommended tests. + +); + const content = { CANDIDATE: { title: candidateTitle, @@ -78,14 +86,19 @@ const content = { RECOMMENDED: { title: recommendedTitle, messageContent: recommendedMessageContent + }, + DEPRECATED: { + title: deprecatedTitle, + messageContent: deprecatedMessageContent } }; -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 || content.CANDIDATE.title; + const messageContent = + content[phase]?.messageContent || content.CANDIDATE.messageContent; return ( @@ -112,7 +125,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..1d884bf30 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { useMutation } from '@apollo/client'; -import { Button, Form } from 'react-bootstrap'; +import { Form } from 'react-bootstrap'; import styled from '@emotion/styled'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; @@ -11,13 +11,13 @@ import BasicThemedModal from '../common/BasicThemedModal'; import { ADD_AT_VERSION_MUTATION, EDIT_AT_VERSION_MUTATION, - DELETE_AT_VERSION_MUTATION, - ADD_TEST_QUEUE_MUTATION + DELETE_AT_VERSION_MUTATION } from '../TestQueue/queries'; import { gitUpdatedDateToString } from '../../utils/gitUtils'; import { convertStringToDate } from '../../utils/formatter'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; import DisclosureComponent from '../common/DisclosureComponent'; +import AddTestToQueueWithConfirmation from '../AddTestToQueueWithConfirmation'; const DisclosureContainer = styled.div` // Following directives are related to the ManageTestQueue component @@ -102,7 +102,6 @@ const ManageTestQueue = ({ const addAtVersionButtonRef = useRef(); const editAtVersionButtonRef = useRef(); const deleteAtVersionButtonRef = useRef(); - const addTestPlanReportButtonRef = useRef(); const [showManageATs, setShowManageATs] = useState(false); const [showAddTestPlans, setShowAddTestPlans] = useState(false); @@ -145,7 +144,6 @@ const ManageTestQueue = ({ const [addAtVersion] = useMutation(ADD_AT_VERSION_MUTATION); const [editAtVersion] = useMutation(EDIT_AT_VERSION_MUTATION); const [deleteAtVersion] = useMutation(DELETE_AT_VERSION_MUTATION); - const [addTestPlanReport] = useMutation(ADD_TEST_QUEUE_MUTATION); const onManageAtsClick = () => setShowManageATs(!showManageATs); const onAddTestPlansClick = () => setShowAddTestPlans(!showAddTestPlans); @@ -156,14 +154,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 +173,7 @@ const ManageTestQueue = ({ } setAllTestPlanVersions(allTestPlanVersions); - setFilteredTestPlanVersions( - filteredTestPlanVersions.sort((a, b) => - a.title < b.title ? -1 : 1 - ) - ); + setFilteredTestPlanVersions(filteredTestPlanVersions); }, [testPlanVersions]); useEffect(() => { @@ -204,13 +201,23 @@ 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 && + item.phase !== 'DEPRECATED' && + item.phase !== 'RD' + ) + .sort((a, b) => + new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1 + ); setMatchingTestPlanVersions(matchingTestPlanVersions); - setSelectedTestPlanVersionId(matchingTestPlanVersions[0].id); + + if (matchingTestPlanVersions.length) + setSelectedTestPlanVersionId(matchingTestPlanVersions[0].id); + else setSelectedTestPlanVersionId(null); }; const onManageAtChange = e => { @@ -447,40 +454,6 @@ const ManageTestQueue = ({ } }; - const handleAddTestPlanToTestQueue = async () => { - focusButtonRef.current = addTestPlanReportButtonRef.current; - - const selectedTestPlanVersion = allTestPlanVersions.find( - item => item.id === selectedTestPlanVersionId - ); - const selectedAt = ats.find(item => item.id === selectedAtId); - const selectedBrowser = browsers.find( - item => item.id === selectedBrowserId - ); - - await triggerLoad(async () => { - await addTestPlanReport({ - variables: { - testPlanVersionId: selectedTestPlanVersionId, - atId: selectedAtId, - browserId: selectedBrowserId - } - }); - await triggerUpdate(); - }, 'Adding Test Plan to Test Queue'); - - showFeedbackMessage( - 'Successfully Added Test Plan', - <> - Successfully added {selectedTestPlanVersion.title} for{' '} - - {selectedAt.name} and {selectedBrowser.name} - {' '} - to the Test Queue. - - ); - }; - const showFeedbackMessage = (title, content) => { setFeedbackModalTitle(title); setFeedbackModalContent(content); @@ -615,21 +588,33 @@ const ManageTestQueue = ({ Test Plan Version - {matchingTestPlanVersions.map(item => ( - + )) + ) : ( + - ))} + )} @@ -675,18 +660,21 @@ const ManageTestQueue = ({
    - + /> ]} onClick={[onManageAtsClick, onAddTestPlansClick]} diff --git a/client/components/Reports/Report.jsx b/client/components/Reports/Report.jsx index 228d9171b..89b8d0247 100644 --- a/client/components/Reports/Report.jsx +++ b/client/components/Reports/Report.jsx @@ -1,25 +1,17 @@ import React from 'react'; import { useQuery } from '@apollo/client'; -import { Route, Routes, Navigate } from 'react-router'; +import { Route, Routes, Navigate, useParams } from 'react-router-dom'; import SummarizeTestPlanVersion from './SummarizeTestPlanVersion'; import SummarizeTestPlanReport from './SummarizeTestPlanReport'; import PageStatus from '../common/PageStatus'; import { REPORT_PAGE_QUERY } from './queries'; import './Reports.css'; -import { useParams } from 'react-router-dom'; const Report = () => { const { testPlanVersionId } = useParams(); - let testPlanVersionIds = []; - - if (testPlanVersionId.includes(',')) - testPlanVersionIds = testPlanVersionId.split(','); - const { loading, data, error } = useQuery(REPORT_PAGE_QUERY, { - variables: testPlanVersionIds.length - ? { testPlanVersionIds } - : { testPlanVersionId }, + variables: { testPlanVersionId: testPlanVersionId }, fetchPolicy: 'cache-and-network' }); @@ -45,55 +37,14 @@ const Report = () => { if (!data) return null; - const testPlanReports = data.testPlanReports.filter( - each => - each.testPlanVersion.id === testPlanVersionId || - testPlanVersionIds.includes(each.testPlanVersion.id) - ); - - const combineArray = testPlanReports => { - let testPlanTargetsById = {}; - testPlanReports.forEach(testPlanReport => { - const { at, browser } = testPlanReport; - - // Construct testPlanTarget - const testPlanTargetId = `${at.id}${browser.id}`; - - if (!testPlanTargetsById[testPlanTargetId]) { - testPlanTargetsById[testPlanTargetId] = [{ ...testPlanReport }]; - } else - testPlanTargetsById[testPlanTargetId].push({ - ...testPlanReport - }); - }); - - return Object.values(testPlanTargetsById).map(testPlanReports => { - return testPlanReports.reduce((prev, curr) => { - const latestPrevDate = new Date( - prev.latestAtVersionReleasedAt.releasedAt - ); - - const latestCurrDate = new Date( - curr.latestAtVersionReleasedAt.releasedAt - ); - - return latestPrevDate > latestCurrDate ? prev : curr; - }); - }); - }; - - if (!testPlanReports || testPlanReports.length < 1) { - return ; - } - return ( } /> @@ -101,7 +52,8 @@ const Report = () => { path="targets/:testPlanReportId" element={ } /> diff --git a/client/components/Reports/Reports.jsx b/client/components/Reports/Reports.jsx index 31601a4b7..33a911310 100644 --- a/client/components/Reports/Reports.jsx +++ b/client/components/Reports/Reports.jsx @@ -32,7 +32,13 @@ const Reports = () => { if (!data) return null; - return ; + return ( + testPlanVersion.testPlanReports.length + )} + /> + ); }; export default Reports; diff --git a/client/components/Reports/SummarizeTestPlanReport.jsx b/client/components/Reports/SummarizeTestPlanReport.jsx index 08f0109b6..2daaf2323 100644 --- a/client/components/Reports/SummarizeTestPlanReport.jsx +++ b/client/components/Reports/SummarizeTestPlanReport.jsx @@ -1,7 +1,6 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { createGitHubIssueWithTitleAndBody } from '../TestRun'; import { getTestPlanTargetTitle, getTestPlanVersionTitle } from './getTitles'; import { Breadcrumb, Button, Container } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; @@ -16,8 +15,8 @@ import { convertDateToString } from '../../utils/formatter'; import DisclaimerInfo from '../DisclaimerInfo'; import TestPlanResultsTable from './TestPlanResultsTable'; import DisclosureComponent from '../common/DisclosureComponent'; -import { Navigate } from 'react-router'; -import { useLocation, useParams } from 'react-router-dom'; +import { Navigate, useLocation, useParams } from 'react-router-dom'; +import createIssueLink from '../../utils/createIssueLink'; const getTestersRunHistory = ( testPlanReport, @@ -31,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(
  • { +const SummarizeTestPlanReport = ({ testPlanVersion, testPlanReports }) => { const location = useLocation(); const { testPlanReportId } = useParams(); @@ -86,7 +81,7 @@ const SummarizeTestPlanReport = ({ testPlanReports }) => { if (!testPlanReport) return ; - const { testPlanVersion, at, browser } = testPlanReport; + const { at, browser } = testPlanReport; // Construct testPlanTarget const testPlanTarget = { @@ -146,13 +141,23 @@ const SummarizeTestPlanReport = ({ testPlanReports }) => { {testPlanReport.finalizedTestResults.map(testResult => { const test = testResult.test; - const fromReportPageLink = `https://aria-at.w3.org${location.pathname}#result-${testResult.id}`; - const gitHubIssueLinkWithTitleAndBody = - createGitHubIssueWithTitleAndBody({ - test, - testPlanReport, - fromReportPageLink - }); + const reportLink = `https://aria-at.w3.org${location.pathname}#result-${testResult.id}`; + const issueLink = createIssueLink({ + testPlanTitle: testPlanVersion.title, + testPlanDirectory: testPlanVersion.testPlan.directory, + versionString: `V${convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}`, + testTitle: test.title, + testRowNumber: test.rowNumber, + testRenderedUrl: test.renderedUrl, + atName: testPlanReport.at.name, + atVersionName: testResult.atVersion.name, + browserName: testPlanReport.browser.name, + browserVersionName: testResult.browserVersion.name, + reportLink + }); // TODO: fix renderedUrl let modifiedRenderedUrl = test.renderedUrl.replace( @@ -168,15 +173,13 @@ const SummarizeTestPlanReport = ({ testPlanReports }) => { Details for test: {test.title} - +
    {skippedTests.length ? ( { None; -const sum = arr => arr.reduce((total, item) => total + item, 0); +const sum = arr => arr?.reduce((total, item) => total + item, 0) || 0; const countTests = ({ testPlanReport, // Choose one to provide @@ -17,25 +17,29 @@ const countTests = ({ passedOnly }) => { const countScenarioResult = scenarioResult => { - return scenarioResult.assertionResults.every( - assertionResult => assertionResult.passed + return ( + scenarioResult?.assertionResults?.every( + assertionResult => assertionResult.passed + ) || 0 ); }; const countTestResult = testResult => { if (passedOnly) - return testResult.scenarioResults.every(countScenarioResult) + return testResult?.scenarioResults?.every(countScenarioResult) ? 1 : 0; return testResult ? 1 : 0; }; const countTestPlanReport = testPlanReport => { - return sum(testPlanReport.finalizedTestResults.map(countTestResult)); + return sum( + testPlanReport?.finalizedTestResults?.map(countTestResult) || [] + ); }; if (testPlanReport) return countTestPlanReport(testPlanReport); if (testResult) return countTestResult(testResult); - return countScenarioResult(scenarioResult, testResult); + return countScenarioResult(scenarioResult); }; const countAssertions = ({ @@ -46,15 +50,18 @@ const countAssertions = ({ passedOnly }) => { const countScenarioResult = scenarioResult => { - const all = scenarioResult[`${priority.toLowerCase()}AssertionResults`]; + const all = + scenarioResult?.[`${priority.toLowerCase()}AssertionResults`] || []; if (passedOnly) return all.filter(each => each.passed).length; return all.length; }; const countTestResult = testResult => { - return sum(testResult.scenarioResults.map(countScenarioResult)); + return sum(testResult?.scenarioResults?.map(countScenarioResult) || []); }; const countTestPlanReport = testPlanReport => { - return sum(testPlanReport.finalizedTestResults.map(countTestResult)); + return sum( + testPlanReport?.finalizedTestResults?.map(countTestResult) || [] + ); }; if (testPlanReport) return countTestPlanReport(testPlanReport); @@ -68,13 +75,15 @@ const countUnexpectedBehaviors = ({ testPlanReport // Choose one to provide }) => { const countScenarioResult = scenarioResult => { - return scenarioResult.unexpectedBehaviors.length; + return scenarioResult?.unexpectedBehaviors?.length || 0; }; const countTestResult = testResult => { - return sum(testResult.scenarioResults.map(countScenarioResult)); + return sum(testResult?.scenarioResults?.map(countScenarioResult) || []); }; const countTestPlanReport = testPlanReport => { - return sum(testPlanReport.finalizedTestResults.map(countTestResult)); + return sum( + testPlanReport?.finalizedTestResults?.map(countTestResult) || [] + ); }; if (testPlanReport) return countTestPlanReport(testPlanReport); @@ -117,7 +126,7 @@ const getMetrics = ({ passedOnly: true }); const testsCount = - testPlanReport?.runnableTests.length || countTests({ ...result }); + testPlanReport?.runnableTests?.length || countTests({ ...result }); const testsFailedCount = testsCount - testsPassedCount; const requiredFormatted = `${requiredAssertionsPassedCount} of ${requiredAssertionsCount} passed`; diff --git a/client/components/Reports/queries.js b/client/components/Reports/queries.js index f3116d627..caece7d7e 100644 --- a/client/components/Reports/queries.js +++ b/client/components/Reports/queries.js @@ -2,148 +2,137 @@ import { gql } from '@apollo/client'; export const REPORTS_PAGE_QUERY = gql` query ReportsPageQuery { - testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) { + testPlanVersions(phases: [CANDIDATE, RECOMMENDED]) { id - status - metrics - candidateStatusReachedAt - recommendedStatusReachedAt - at { - id - name - } - latestAtVersionReleasedAt { - id - name - releasedAt + title + phase + gitSha + updatedAt + testPlan { + directory } - browser { + metadata + testPlanReports(isFinal: true) { id - name - } - testPlanVersion { - id - title - gitSha - testPlan { - directory + metrics + at { + id + name + } + browser { + id + name } - metadata } } } `; export const REPORT_PAGE_QUERY = gql` - query ReportPageQuery($testPlanVersionId: ID, $testPlanVersionIds: [ID]) { - testPlanReports( - statuses: [CANDIDATE, RECOMMENDED] - testPlanVersionId: $testPlanVersionId - testPlanVersionIds: $testPlanVersionIds - ) { + query ReportPageQuery($testPlanVersionId: ID) { + testPlanVersion(id: $testPlanVersionId) { id - status - metrics - candidateStatusReachedAt - recommendedStatusReachedAt - at { - id - name - } - latestAtVersionReleasedAt { - id - name - releasedAt + title + phase + gitSha + updatedAt + testPlan { + directory } - browser { + metadata + testPlanReports(isFinal: true) { id - name - } - testPlanVersion { - id - title - gitSha - testPlan { - directory + metrics + at { + id + name } - metadata - } - runnableTests { - id - title - renderedUrl - } - finalizedTestResults { - id - test { + browser { + id + name + } + runnableTests { id - rowNumber title renderedUrl } - scenarioResults { + finalizedTestResults { id - scenario { - commands { - id - text - } + test { + id + rowNumber + title + renderedUrl } - output - assertionResults { + scenarioResults { id - assertion { - text + scenario { + commands { + id + text + } } - passed - failedReason - } - requiredAssertionResults: assertionResults( - priority: REQUIRED - ) { - assertion { - text + output + assertionResults { + id + assertion { + text + } + passed + failedReason } - passed - failedReason - } - optionalAssertionResults: assertionResults( - priority: OPTIONAL - ) { - assertion { + requiredAssertionResults: assertionResults( + priority: REQUIRED + ) { + assertion { + text + } + passed + failedReason + } + optionalAssertionResults: assertionResults( + priority: OPTIONAL + ) { + assertion { + text + } + passed + failedReason + } + unexpectedBehaviors { + id text + otherUnexpectedBehaviorText } - passed - failedReason - } - unexpectedBehaviors { - id - text - otherUnexpectedBehaviorText - } - } - } - draftTestPlanRuns { - tester { - username - } - testPlanReport { - id - status - } - testResults { - test { - id } atVersion { - id name } browserVersion { - id name } - completedAt + } + draftTestPlanRuns { + tester { + username + } + testPlanReport { + id + } + testResults { + test { + id + } + atVersion { + id + name + } + browserVersion { + id + name + } + completedAt + } } } } 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 394775ade..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/TestPlanReportStatusDialog/WithButton.jsx b/client/components/TestPlanReportStatusDialog/WithButton.jsx new file mode 100644 index 000000000..b7ee923ad --- /dev/null +++ b/client/components/TestPlanReportStatusDialog/WithButton.jsx @@ -0,0 +1,161 @@ +import React, { useMemo, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-bootstrap'; +import TestPlanReportStatusDialog from './index'; +import { getRequiredReports } from './isRequired'; +import { calculateTestPlanReportCompletionPercentage } from './calculateTestPlanReportCompletionPercentage'; +import styled from '@emotion/styled'; +import ReportStatusDot from '../common/ReportStatusDot'; +import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from './queries'; +import { useQuery } from '@apollo/client'; + +const TestPlanReportStatusDialogButton = styled(Button)` + display: flex; + justify-content: center; + align-items: center; + + padding: 0.5rem; + font-size: 0.875rem; + + border: none; + border-radius: 0; + + color: #6a7989; + background: #f6f8fa; + + margin-top: auto; +`; + +const TestPlanReportStatusDialogWithButton = ({ testPlanVersionId }) => { + const { + data: { testPlanVersion } = {}, + refetch, + loading + } = useQuery(TEST_PLAN_REPORT_STATUS_DIALOG_QUERY, { + variables: { testPlanVersionId }, + fetchPolicy: 'cache-and-network' + }); + + const buttonRef = useRef(null); + + const [showDialog, setShowDialog] = useState(false); + const { testPlanReports } = testPlanVersion ?? {}; + + // TODO: Use the DB provided AtBrowsers combinations when doing the edit UI task + const requiredReports = useMemo( + () => getRequiredReports(testPlanVersion?.phase), + [testPlanVersion?.phase] + ); + + const buttonLabel = useMemo(() => { + const initialCounts = { completed: 0, inProgress: 0, missing: 0 }; + + const counts = requiredReports.reduce((acc, requiredReport) => { + const matchingReport = testPlanReports.find( + report => + report.at.id === requiredReport.at.id && + report.browser.id === requiredReport.browser.id + ); + if (matchingReport) { + const percentComplete = + calculateTestPlanReportCompletionPercentage(matchingReport); + if (percentComplete === 100 && matchingReport.markedFinalAt) { + acc.completed++; + } else { + acc.inProgress++; + } + } else { + acc.missing++; + } + return acc; + }, initialCounts); + + // All AT/browser pairs that require a report have a complete report + if (counts.completed === requiredReports.length) { + return ( + + + Required Reports Complete + + ); + } + // At least one AT/browser pair that requires a report does not have a complete report and is in the test queue. + // All other AT/browser pairs that require a report are either complete or are in the test queue. + else if (counts.inProgress > 0 && counts.missing === 0) { + return ( + + + Required Reports In Progress + + ); + } + // At least one of the AT/browser pairs that requires a report neither has a complete report nor has a run in the test queue. + // At the same time, at least one of the AT/browser pairs that requires a report either has a complete report or has a run in the test queue. + else if ( + counts.missing > 0 && + (counts.completed > 0 || counts.inProgress > 0) + ) { + return ( + + + Some Required Reports Missing + + ); + } + // For every AT/browser pair that requires a report, the report is neither complete nor in the test queue. + else if (counts.missing === requiredReports.length) { + return ( + + + Required Reports Not Started + + ); + } + // Fallback case + else { + return ( + + + Some Reports Complete + + ); + } + }, [requiredReports, testPlanReports]); + + if ( + loading || + !testPlanVersion || + !testPlanVersion.phase || + (testPlanVersion.phase !== 'DRAFT' && + testPlanVersion.phase !== 'CANDIDATE' && + testPlanVersion.phase !== 'RECOMMENDED') + ) { + return; + } + + return ( + <> + setShowDialog(true)} + > + {buttonLabel} + + { + setShowDialog(false); + buttonRef.current.focus(); + }} + triggerUpdate={refetch} + /> + + ); +}; + +TestPlanReportStatusDialogWithButton.propTypes = { + testPlanVersionId: PropTypes.string.isRequired +}; + +export default TestPlanReportStatusDialogWithButton; diff --git a/client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js b/client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js new file mode 100644 index 000000000..451e1523b --- /dev/null +++ b/client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js @@ -0,0 +1,15 @@ +export const calculateTestPlanReportCompletionPercentage = ({ + metrics, + draftTestPlanRuns +}) => { + if (!metrics || !draftTestPlanRuns) return 0; + const assignedUserCount = draftTestPlanRuns.length || 1; + const totalTestsPossible = metrics.testsCount * assignedUserCount; + let totalTestsCompleted = 0; + draftTestPlanRuns.forEach(draftTestPlanRun => { + totalTestsCompleted += draftTestPlanRun.testResults.length; + }); + const percentage = (totalTestsCompleted / totalTestsPossible) * 100; + if (isNaN(percentage) || !isFinite(percentage)) return 0; + return Math.floor(percentage); +}; diff --git a/client/components/TestPlanReportStatusDialog/index.jsx b/client/components/TestPlanReportStatusDialog/index.jsx new file mode 100644 index 000000000..1d01b5775 --- /dev/null +++ b/client/components/TestPlanReportStatusDialog/index.jsx @@ -0,0 +1,261 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Modal } from 'react-bootstrap'; +import styled from '@emotion/styled'; +import { getRequiredReports } from './isRequired'; +import AddTestToQueueWithConfirmation from '../AddTestToQueueWithConfirmation'; +import { useQuery } from '@apollo/client'; +import { ME_QUERY } from '../App/queries'; +import { evaluateAuth } from '../../utils/evaluateAuth'; +import getMetrics from '../Reports/getMetrics'; +import { calculateTestPlanReportCompletionPercentage } from './calculateTestPlanReportCompletionPercentage'; +import { convertDateToString } from '../../utils/formatter'; +import { ThemeTable } from '../common/ThemeTable'; + +const TestPlanReportStatusModal = styled(Modal)` + .modal-dialog { + max-width: 90%; + width: max-content; + } +`; + +const IncompleteStatusReport = styled.span` + min-width: 5rem; + display: inline-block; +`; + +const TestPlanReportStatusDialog = ({ + testPlanVersion, + show, + handleHide = () => {}, + triggerUpdate = () => {} +}) => { + const { data: { me } = {} } = useQuery(ME_QUERY, { + fetchPolicy: 'cache-and-network' + }); + + const { testPlanReports } = testPlanVersion; + + const auth = evaluateAuth(me ?? {}); + const { isSignedIn, isAdmin } = auth; + + const requiredReports = useMemo( + () => getRequiredReports(testPlanVersion?.phase), + [testPlanVersion] + ); + + const [matchedReports, unmatchedTestPlanReports, unmatchedRequiredReports] = + useMemo(() => { + const matched = []; + const unmatchedTestPlan = [...testPlanReports]; + const unmatchedRequired = [...requiredReports]; + + for (let i = 0; i < requiredReports.length; i++) { + for (let j = 0; j < testPlanReports.length; j++) { + if ( + requiredReports[i].at.name === + testPlanReports[j].at.name && + requiredReports[i].browser.name === + testPlanReports[j].browser.name + ) { + if (testPlanReports[j].status === 'DRAFT') { + matched.push({ + ...testPlanReports[j], + metrics: getMetrics(testPlanReports[j]) + }); + } else { + matched.push(testPlanReports[j]); + } + + unmatchedTestPlan.splice( + unmatchedTestPlan.indexOf(testPlanReports[j]), + 1 + ); + unmatchedRequired.splice( + unmatchedRequired.indexOf(requiredReports[i]), + 1 + ); + break; + } + } + } + return [matched, unmatchedTestPlan, unmatchedRequired]; + }, [testPlanReports, requiredReports]); + + const renderTableRow = (testPlanReport, required = 'Yes') => { + return ( + + {required} + {testPlanReport.at.name} + {testPlanReport.browser.name} + {renderReportStatus(testPlanReport)} + + ); + }; + + const renderCompleteReportStatus = testPlanReport => { + const formattedDate = convertDateToString( + testPlanReport.markedFinalAt, + 'MMM D, YYYY' + ); + return ( + + Report completed on {formattedDate} + + ); + }; + + const renderPartialCompleteReportStatus = testPlanReport => { + const { metrics, draftTestPlanRuns } = testPlanReport; + const conflictsCount = metrics.conflictsCount ?? 0; + const percentComplete = + calculateTestPlanReportCompletionPercentage(testPlanReport); + switch (draftTestPlanRuns?.length) { + case 0: + return In test queue with no testers assigned.; + case 1: + return ( + + {percentComplete}% complete by  + + {draftTestPlanRuns[0].tester.username} + +  with {conflictsCount} conflicts + + ); + default: + return ( + + {percentComplete}% complete by  + {draftTestPlanRuns.length} testers with {conflictsCount} +  conflicts + + ); + } + }; + + const renderReportStatus = testPlanReport => { + const { metrics, at, browser, markedFinalAt } = testPlanReport; + const { phase } = testPlanVersion; + if (metrics) { + if ( + markedFinalAt && + (phase === 'CANDIDATE' || phase === 'RECOMMENDED') + ) { + return renderCompleteReportStatus(testPlanReport); + } else { + return renderPartialCompleteReportStatus(testPlanReport); + } + } else { + return ( + <> + Missing + {isSignedIn && isAdmin ? ( + + ) : null} + + ); + } + }; + + return ( + + +

    + Report Status for the  + {testPlanVersion.title} +  Test Plan +

    +
    + + + {testPlanVersion.phase && ( +

    + This plan is in the  + + {/* text-transform: capitalize will not work on all-caps string */} + {testPlanVersion.phase[0] + + testPlanVersion.phase.slice(1).toLowerCase()} + +  Review phase.  + + {requiredReports.length} AT/browser  + + pairs require reports in this phase. +

    + )} + + + + + Required + AT + Browser + Report Status + + + + {matchedReports.map(report => renderTableRow(report))} + {unmatchedRequiredReports.map(report => + renderTableRow(report) + )} + {unmatchedTestPlanReports.map(report => + renderTableRow(report, 'No') + )} + + +
    +
    + ); +}; + +TestPlanReportStatusDialog.propTypes = { + testPlanVersion: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + phase: PropTypes.string.isRequired, + testPlanReports: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string, + runnableTests: PropTypes.arrayOf(PropTypes.object), + finalizedTestResults: PropTypes.arrayOf(PropTypes.object), + at: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + browser: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired + }).isRequired + ).isRequired + }).isRequired, + handleHide: PropTypes.func.isRequired, + triggerUpdate: PropTypes.func, + show: PropTypes.bool.isRequired +}; + +export default TestPlanReportStatusDialog; diff --git a/client/components/TestPlanReportStatusDialog/isRequired.js b/client/components/TestPlanReportStatusDialog/isRequired.js new file mode 100644 index 000000000..bf2281e46 --- /dev/null +++ b/client/components/TestPlanReportStatusDialog/isRequired.js @@ -0,0 +1,60 @@ +const requiredReports = { + DRAFT: [ + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'JAWS', id: '1' } + }, + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'NVDA', id: '2' } + }, + { + browser: { name: 'Safari', id: '3' }, + at: { name: 'VoiceOver for macOS', id: '3' } + } + ], + CANDIDATE: [ + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'JAWS', id: '1' } + }, + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'NVDA', id: '2' } + }, + { + browser: { name: 'Safari', id: '3' }, + at: { name: 'VoiceOver for macOS', id: '3' } + } + ], + RECOMMENDED: [ + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'JAWS', id: '1' } + }, + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'NVDA', id: '2' } + }, + { + browser: { name: 'Safari', id: '3' }, + at: { name: 'VoiceOver for macOS', id: '3' } + }, + { + browser: { name: 'Firefox', id: '1' }, + at: { name: 'NVDA', id: '2' } + }, + { + browser: { name: 'Firefox', id: '1' }, + at: { name: 'JAWS', id: '1' } + }, + { + browser: { name: 'Chrome', id: '2' }, + at: { name: 'VoiceOver for macOS', id: '3' } + } + ] +}; + +export const getRequiredReports = testPlanPhase => { + return requiredReports[testPlanPhase] ?? []; +}; diff --git a/client/components/TestPlanReportStatusDialog/queries.js b/client/components/TestPlanReportStatusDialog/queries.js new file mode 100644 index 000000000..5a20b6f8c --- /dev/null +++ b/client/components/TestPlanReportStatusDialog/queries.js @@ -0,0 +1,61 @@ +import { gql } from '@apollo/client'; + +export const TEST_PLAN_REPORT_STATUS_DIALOG_QUERY = gql` + query TestPlanReportStatusDialog($testPlanVersionId: ID!) { + testPlanVersion(id: $testPlanVersionId) { + id + title + phase + gitSha + gitMessage + updatedAt + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + testPlan { + directory + } + testPlanReports { + id + metrics + markedFinalAt + at { + id + name + } + browser { + id + name + } + issues { + link + isOpen + feedbackType + } + draftTestPlanRuns { + tester { + username + } + testPlanReport { + id + } + testResults { + test { + id + } + atVersion { + id + name + } + browserVersion { + id + name + } + completedAt + } + } + } + } + } +`; diff --git a/client/components/TestPlanVersionsPage/index.jsx b/client/components/TestPlanVersionsPage/index.jsx new file mode 100644 index 000000000..2f40e768e --- /dev/null +++ b/client/components/TestPlanVersionsPage/index.jsx @@ -0,0 +1,661 @@ +import React from 'react'; +import { useQuery } from '@apollo/client'; +import { TEST_PLAN_VERSIONS_PAGE_QUERY } from './queries'; +import PageStatus from '../common/PageStatus'; +import { useParams } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import { Container } from 'react-bootstrap'; +import { + ThemeTable, + ThemeTableUnavailable, + ThemeTableHeaderH3 as UnstyledThemeTableHeader +} from '../common/ThemeTable'; +import VersionString from '../common/VersionString'; +import PhasePill from '../common/PhasePill'; +import { convertDateToString } from '../../utils/formatter'; +import { derivePhaseName } from '../../utils/aria'; +import styled from '@emotion/styled'; +import { + faArrowUpRightFromSquare, + faCodeCommit +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +const H2 = styled.h2` + font-size: 1.25em; + padding-top: 3rem; + padding-bottom: 15px; + border-bottom: solid 1px #d2d5d9; + margin-bottom: 2rem !important; +`; + +const NoneText = styled.span` + font-style: italic; + color: #6a7989; +`; + +const PageCommitHistory = styled.div` + padding: 1.5rem 0 1.5rem; +`; + +const PageUl = styled.ul` + margin-bottom: 2rem; + + li:not(:last-of-type) { + margin-bottom: 8px; + } +`; + +const PageSpacer = styled.div` + height: 3rem; +`; + +const CoveredAtDl = styled.dl` + margin-bottom: 2rem; + + dt { + margin-bottom: 8px; + } + li:not(:last-of-type) { + margin-bottom: 8px; + } +`; + +const ThemeTableHeader = styled(UnstyledThemeTableHeader)` + margin: 0 !important; +`; + +const TestPlanVersionsPage = () => { + const { testPlanDirectory } = useParams(); + + const { loading, data, error } = useQuery(TEST_PLAN_VERSIONS_PAGE_QUERY, { + variables: { testPlanDirectory }, + fetchPolicy: 'cache-and-network' + }); + + if (error) { + return ( + + ); + } + + if (loading) { + return ( + + ); + } + + if (!data) return null; + + const getPhaseChangeDate = testPlanVersion => { + let date; + switch (testPlanVersion.phase) { + case 'DRAFT': + date = testPlanVersion.draftPhaseReachedAt; + break; + case 'CANDIDATE': + date = testPlanVersion.candidatePhaseReachedAt; + break; + case 'RECOMMENDED': + date = testPlanVersion.recommendedPhaseReachedAt; + break; + case 'RD': + date = testPlanVersion.updatedAt; + break; + case 'DEPRECATED': + date = testPlanVersion.deprecatedAt; + break; + default: + throw new Error('Unexpected case'); + } + return convertDateToString(date, 'MMM D, YYYY'); + }; + + const getIconColor = testPlanVersion => { + return testPlanVersion.phase === 'DEPRECATED' || + testPlanVersion.phase === 'RD' + ? '#818F98' + : '#2BA51C'; + }; + + const getEventDate = testPlanVersion => { + return convertDateToString( + (() => { + if (testPlanVersion.deprecatedAt) { + return testPlanVersion.deprecatedAt; + } + switch (testPlanVersion.phase) { + case 'RD': + return testPlanVersion.updatedAt; + case 'DRAFT': + return testPlanVersion.draftPhaseReachedAt; + case 'CANDIDATE': + return testPlanVersion.candidatePhaseReachedAt; + case 'RECOMMENDED': + return testPlanVersion.recommendedPhaseReachedAt; + case 'DEPRECATED': + return testPlanVersion.deprecatedAt; + } + })(), + 'MMM D, YYYY' + ); + }; + + const getEventBody = phase => { + const phasePill = {phase}; + + switch (phase) { + case 'RD': + return <>{phasePill} Complete; + case 'DRAFT': + case 'CANDIDATE': + return <>{phasePill} Review Started; + case 'RECOMMENDED': + return <>{phasePill} Approved; + case 'DEPRECATED': + return <>{phasePill}; + } + }; + + const deriveDeprecatedDuringPhase = testPlanVersion => { + let derivedPhaseDeprecatedDuring = 'RD'; + if (testPlanVersion.recommendedPhaseReachedAt) + derivedPhaseDeprecatedDuring = 'RECOMMENDED'; + else if (testPlanVersion.candidatePhaseReachedAt) + derivedPhaseDeprecatedDuring = 'CANDIDATE'; + else if (testPlanVersion.draftPhaseReachedAt) + derivedPhaseDeprecatedDuring = 'DRAFT'; + + return derivedPhaseDeprecatedDuring; + }; + + const testPlan = data.testPlan; + + // GraphQL results are read only so they need to be cloned before sorting + const issues = [...testPlan.issues].sort((a, b) => { + const aCreatedAt = new Date(a.createdAt); + const bCreatedAt = new Date(b.createdAt); + return bCreatedAt - aCreatedAt; + }); + + const ats = data.ats; + + const testPlanVersions = data.testPlan.testPlanVersions + .slice() + .sort((a, b) => { + return new Date(b.updatedAt) - new Date(a.updatedAt); + }); + + const timelineForAllVersions = []; + + testPlanVersions.forEach(testPlanVersion => { + const event = { + id: testPlanVersion.id, + updatedAt: testPlanVersion.updatedAt + }; + timelineForAllVersions.push({ ...event, phase: 'RD' }); + + if (testPlanVersion.draftPhaseReachedAt) + timelineForAllVersions.push({ + ...event, + phase: 'DRAFT', + draftPhaseReachedAt: testPlanVersion.draftPhaseReachedAt + }); + if (testPlanVersion.candidatePhaseReachedAt) + timelineForAllVersions.push({ + ...event, + phase: 'CANDIDATE', + candidatePhaseReachedAt: testPlanVersion.candidatePhaseReachedAt + }); + if (testPlanVersion.recommendedPhaseReachedAt) + timelineForAllVersions.push({ + ...event, + phase: 'RECOMMENDED', + recommendedPhaseReachedAt: + testPlanVersion.recommendedPhaseReachedAt + }); + if (testPlanVersion.deprecatedAt) + timelineForAllVersions.push({ + ...event, + phase: 'DEPRECATED', + deprecatedAt: testPlanVersion.deprecatedAt + }); + }); + + const phaseOrder = { + RD: 0, + DRAFT: 1, + CANDIDATE: 2, + RECOMMENDED: 3, + DEPRECATED: 4 + }; + + timelineForAllVersions.sort((a, b) => { + const dateA = + a.recommendedPhaseReachedAt || + a.candidatePhaseReachedAt || + a.draftPhaseReachedAt || + a.deprecatedAt || + a.updatedAt; + const dateB = + b.recommendedPhaseReachedAt || + b.candidatePhaseReachedAt || + b.draftPhaseReachedAt || + b.deprecatedAt || + b.updatedAt; + + // If dates are the same, compare phases + if (dateA === dateB) return phaseOrder[a.phase] - phaseOrder[b.phase]; + return new Date(dateA) - new Date(dateB); + }); + + return ( + + + {testPlan.title} Test Plan Versions | ARIA-AT + +

    {testPlan.title} Test Plan Versions

    + + + + Commit History for aria-at/tests/{testPlanDirectory} + + + {!testPlanVersions.length ? null : ( + <> + + Version Summary + + + + + Version + Latest Phase + Phase Change Date + + + + {testPlanVersions.map(testPlanVersion => ( + + + + + + {(() => { + // Gets the derived phase even if deprecated by checking + // the known dates on the testPlanVersion object + const derivedDeprecatedAtPhase = + deriveDeprecatedDuringPhase( + testPlanVersion + ); + + const phasePill = ( + + {derivedDeprecatedAtPhase} + + ); + + if (testPlanVersion.deprecatedAt) { + const deprecatedPill = ( + + DEPRECATED + + ); + + const draftPill = ( + + DRAFT + + ); + + if ( + derivedDeprecatedAtPhase === + 'RD' + ) { + return ( + <> + {deprecatedPill} + {` before `} + {draftPill} + {` review `} + + ); + } + + if ( + derivedDeprecatedAtPhase === + 'RECOMMENDED' + ) { + return ( + <> + {deprecatedPill} + {` after being approved as `} + {phasePill} + + ); + } + + return ( + <> + {deprecatedPill} + {` during `} + {phasePill} + {` review `} + + ); + } + return phasePill; + })()} + + + {getPhaseChangeDate(testPlanVersion)} + + + ))} + + + + + )} + + + GitHub Issues + + {!issues.length ? ( + + No GitHub Issues + + ) : ( + + + + Author + Issue + Status + AT + Created On + Closed On + + + + {issues.map(issue => { + return ( + + + + {issue.author} + + + + + {issue.title} + + + {issue.isOpen ? 'Open' : 'Closed'} + + {issue.at?.name ?? 'AT not specified'} + + + {convertDateToString( + issue.createdAt, + 'MMM D, YYYY' + )} + + + {!issue.closedAt ? ( + N/A + ) : ( + convertDateToString( + issue.closedAt, + 'MMM D, YYYY' + ) + )} + + + ); + })} + + + )} + + + + Timeline for All Versions + + + + + Date + Event + + + + {timelineForAllVersions.map(testPlanVersion => { + const versionString = ( + + ); + + const eventBody = getEventBody(testPlanVersion.phase); + + return ( + + {getEventDate(testPlanVersion)} + + {versionString} {eventBody} + + + ); + })} + + + + {testPlanVersions.map(testPlanVersion => { + const vString = `V${convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}`; + + // Gets the derived phase even if deprecated by checking + // the known dates on the testPlanVersion object + const derivedDeprecatedAtPhase = + deriveDeprecatedDuringPhase(testPlanVersion); + + const hasFinalReports = + (derivedDeprecatedAtPhase === 'CANDIDATE' || + derivedDeprecatedAtPhase === 'RECOMMENDED') && + !!testPlanVersion.testPlanReports.filter( + report => report.isFinal + ).length; + + return ( +
    +

    + +   + + {testPlanVersion.phase} + +  on {getEventDate(testPlanVersion)} +

    + +
  • + + + Commit {testPlanVersion.gitSha.substr(0, 7)} + : {testPlanVersion.gitMessage} + +
  • +
  • + + + View tests in {vString} + +
  • + {!hasFinalReports ? null : ( +
  • + + + View reports generated from {vString} + +
  • + )} + + +
    Covered AT
    +
    +
      + {ats.map(at => ( +
    • {at.name}
    • + ))} +
    +
    +
    + + Timeline for {vString} + + + + + Date + Event + + + + {(() => { + let events = [ + ['RD', testPlanVersion.updatedAt], + [ + 'DRAFT', + testPlanVersion.draftPhaseReachedAt + ], + [ + 'CANDIDATE', + testPlanVersion.candidatePhaseReachedAt + ], + [ + 'RECOMMENDED', + testPlanVersion.recommendedPhaseReachedAt + ], + [ + 'DEPRECATED', + testPlanVersion.deprecatedAt + ] + ] + .filter(event => event[1]) + .sort((a, b) => { + const dateSort = + new Date(a[1]) - new Date(b[1]); + if (dateSort === 0) return 1; // maintain order above + return dateSort; + }); + + return events.map(([phase, date]) => ( + + + {convertDateToString( + date, + 'MMM D, YYYY' + )} + + {getEventBody(phase)} + + )); + })()} + + + + ); + })} + + ); +}; + +export default TestPlanVersionsPage; diff --git a/client/components/TestPlanVersionsPage/queries.js b/client/components/TestPlanVersionsPage/queries.js new file mode 100644 index 000000000..b8557ffcb --- /dev/null +++ b/client/components/TestPlanVersionsPage/queries.js @@ -0,0 +1,46 @@ +import { gql } from '@apollo/client'; + +export const TEST_PLAN_VERSIONS_PAGE_QUERY = gql` + query TestPlanVersionsPageQuery($testPlanDirectory: ID!) { + ats { + id + name + } + testPlan(id: $testPlanDirectory) { + title + issues { + author + title + link + feedbackType + isOpen + createdAt + closedAt + at { + name + } + } + testPlanVersions { + id + testPlan { + directory + } + phase + updatedAt + deprecatedAt + gitSha + gitMessage + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseReachedAt + testPlanReports { + id + isFinal + at { + name + } + } + } + } + } +`; diff --git a/client/components/TestQueue/index.jsx b/client/components/TestQueue/index.jsx index 2d0995f2a..2fb8ef344 100644 --- a/client/components/TestQueue/index.jsx +++ b/client/components/TestQueue/index.jsx @@ -101,7 +101,6 @@ const TestQueue = () => { className="test-queue" aria-labelledby={tableId} bordered - responsive > diff --git a/client/components/TestQueue/queries.js b/client/components/TestQueue/queries.js index 0455069bd..065c5e929 100644 --- a/client/components/TestQueue/queries.js +++ b/client/components/TestQueue/queries.js @@ -28,6 +28,7 @@ export const TEST_QUEUE_PAGE_QUERY = gql` testPlanVersions { id title + phase gitSha gitMessage testPlan { @@ -35,11 +36,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 +52,7 @@ export const TEST_QUEUE_PAGE_QUERY = gql` testPlanVersion { id title + phase gitSha gitMessage testPlan { @@ -83,7 +85,6 @@ export const TEST_PLAN_REPORT_QUERY = gql` query TestPlanReport($testPlanReportId: ID!) { testPlanReport(id: $testPlanReportId) { id - status conflictsLength runnableTests { id @@ -126,39 +127,6 @@ export const TEST_PLAN_REPORT_QUERY = gql` } `; -export const TEST_PLAN_REPORT_CANDIDATE_RECOMMENDED_QUERY = gql` - query CandidateTestPlanReportsQuery { - testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) { - id - status - latestAtVersionReleasedAt { - id - name - releasedAt - } - candidateStatusReachedAt - recommendedStatusTargetDate - at { - id - name - } - browser { - id - name - } - testPlanVersion { - id - title - gitSha - testPlan { - directory - } - metadata - } - } - } -`; - export const ADD_AT_VERSION_MUTATION = gql` mutation AddAtVersion($atId: ID!, $name: String!, $releasedAt: Timestamp!) { at(id: $atId) { @@ -234,7 +202,6 @@ export const ADD_TEST_QUEUE_MUTATION = gql` populatedData { testPlanReport { id - status at { id } @@ -270,38 +237,12 @@ export const ASSIGN_TESTER_MUTATION = gql` } `; -export const UPDATE_TEST_PLAN_REPORT_STATUS_MUTATION = gql` - mutation UpdateTestPlanReportStatus( - $testReportId: ID! - $status: TestPlanReportStatus! - $candidateStatusReachedAt: Timestamp - $recommendedStatusTargetDate: Timestamp - ) { - testPlanReport(id: $testReportId) { - updateStatus( - status: $status - candidateStatusReachedAt: $candidateStatusReachedAt - recommendedStatusTargetDate: $recommendedStatusTargetDate - ) { - testPlanReport { - status - } - } - } - } -`; - -export const UPDATE_TEST_PLAN_REPORT_RECOMMENDED_TARGET_DATE_MUTATION = gql` - mutation UpdateTestPlanReportRecommendedTargetDate( - $testReportId: ID! - $recommendedStatusTargetDate: Timestamp! - ) { +export const UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION = gql` + mutation UpdateTestPlanReportMarkedFinalAt($testReportId: ID!) { testPlanReport(id: $testReportId) { - updateRecommendedStatusTargetDate( - recommendedStatusTargetDate: $recommendedStatusTargetDate - ) { + markAsFinal { testPlanReport { - recommendedStatusTargetDate + markedFinalAt } } } diff --git a/client/components/TestQueueRow/CandidatePhaseSelectModal.jsx b/client/components/TestQueueRow/CandidatePhaseSelectModal.jsx deleted file mode 100644 index d0b1edfe7..000000000 --- a/client/components/TestQueueRow/CandidatePhaseSelectModal.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { Form } from 'react-bootstrap'; -import styled from '@emotion/styled'; -import BasicModal from '../common/BasicModal'; -import { convertDateToString } from '../../utils/formatter'; -import FormCheck from 'react-bootstrap/FormCheck'; - -const ModalInnerSectionContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const CandidatePhaseSelectModal = ({ - show = false, - title = null, - dates = [], - handleAction = () => {}, - handleClose = () => {} -}) => { - const [selectedDateIndex, setSelectedDateIndex] = useState('0'); - - const handleChange = e => { - const { value } = e.target; - setSelectedDateIndex(value); - }; - - const onSubmit = () => { - if (selectedDateIndex == -1) handleAction(null); - const date = dates[selectedDateIndex]; - handleAction(date); - }; - - return ( - - - {dates.map((d, index) => { - return ( - - - - Candidate Phase Start Date on{' '} - - {convertDateToString( - d.candidateStatusReachedAt, - 'MMMM D, YYYY' - )} - {' '} - and Recommended Phase Target Completion - Date on{' '} - - {convertDateToString( - d.recommendedStatusTargetDate, - 'MMMM D, YYYY' - )} - - - - ); - })} - - - - Create new Candidate Phase starting today - - - - - } - actionLabel={'Select Candidate Phase'} - handleAction={onSubmit} - handleClose={handleClose} - /> - ); -}; - -CandidatePhaseSelectModal.propTypes = { - show: PropTypes.bool, - title: PropTypes.node.isRequired, - dates: PropTypes.array, - handleAction: PropTypes.func, - handleClose: PropTypes.func -}; - -export default CandidatePhaseSelectModal; diff --git a/client/components/TestQueueRow/TestQueueRow.css b/client/components/TestQueueRow/TestQueueRow.css index 4035df1db..97fbfdeab 100644 --- a/client/components/TestQueueRow/TestQueueRow.css +++ b/client/components/TestQueueRow/TestQueueRow.css @@ -110,3 +110,8 @@ button.more-actions:active { /* tr.test-queue-run-row td:first-child { padding: 0.75rem; } */ + +[role='menu'].assign-menu { + height: 200px; + overflow-y: scroll; +} diff --git a/client/components/TestQueueRow/index.jsx b/client/components/TestQueueRow/index.jsx index 4da3939ed..ed88d6906 100644 --- a/client/components/TestQueueRow/index.jsx +++ b/client/components/TestQueueRow/index.jsx @@ -11,28 +11,24 @@ 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, - TEST_PLAN_REPORT_CANDIDATE_RECOMMENDED_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 } from '../TestQueue/queries'; -import { gitUpdatedDateToString } from '../../utils/gitUtils'; import TestPlanUpdaterModal from '../TestPlanUpdater/TestPlanUpdaterModal'; import BasicThemedModal from '../common/BasicThemedModal'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; +import { convertDateToString } from '../../utils/formatter'; import './TestQueueRow.css'; -import CandidatePhaseSelectModal from '@components/TestQueueRow/CandidatePhaseSelectModal'; const TestQueueRow = ({ user = {}, testers = [], testPlanReportData = {}, - latestTestPlanVersions = [], triggerDeleteTestPlanReportModal = () => {}, triggerDeleteResultsModal = () => {}, triggerPageUpdate = () => {} @@ -48,8 +44,6 @@ const TestQueueRow = ({ const deleteTestPlanButtonRef = useRef(); const updateTestPlanStatusButtonRef = useRef(); - const [candidatePhaseTestPlanReports, setCandidatePhaseTestPlanReports] = - useState([]); const [alertMessage, setAlertMessage] = useState(''); const [showThemedModal, setShowThemedModal] = useState(false); @@ -58,8 +52,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 @@ -69,8 +63,6 @@ const TestQueueRow = ({ const [showTestPlanUpdaterModal, setShowTestPlanUpdaterModal] = useState(false); - const [showCandidatePhaseSelectModal, setShowCandidatePhaseSelectModal] = - useState(false); const [testPlanReport, setTestPlanReport] = useState(testPlanReportData); const [isLoading, setIsLoading] = useState(false); @@ -178,26 +170,22 @@ const TestQueueRow = ({ }; const renderAssignedUserToTestPlan = () => { - const gitUpdatedDateString = ( -

    - Published {gitUpdatedDateToString(testPlanVersion.updatedAt)} -

    + const dateString = convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' ); - const latestTestPlanVersion = latestTestPlanVersions.filter( - version => version.latestTestPlanVersion?.id === testPlanVersion.id + const titleElement = ( + <> + {testPlanVersion.title} {'V' + dateString} +  ({runnableTestsLength} Test + {runnableTestsLength === 0 || runnableTestsLength > 1 + ? `s` + : ''} + ) + ); - const updateTestPlanVersionButton = isAdmin && - latestTestPlanVersion.length === 0 && ( - - ); + // Determine if current user is assigned to testPlan if (currentUserAssigned) return ( @@ -206,11 +194,8 @@ const TestQueueRow = ({ className="test-plan" to={`/run/${currentUserTestPlanRun.id}`} > - {testPlanVersion.title || - `"${testPlanVersion.testPlan.directory}"`} + {titleElement} - {gitUpdatedDateString} - {updateTestPlanVersionButton} ); @@ -218,21 +203,12 @@ const TestQueueRow = ({ return ( <> - {testPlanVersion.title || - `"${testPlanVersion.testPlan.directory}"`} + {titleElement} - {gitUpdatedDateString} ); - return ( -
    - {testPlanVersion.title || - `"${testPlanVersion.testPlan.directory}"`} - {gitUpdatedDateString} - {updateTestPlanVersionButton} -
    - ); + return
    {titleElement}
    ; }; const renderAssignMenu = () => { @@ -247,7 +223,7 @@ const TestQueueRow = ({ > - + {testers.length ? ( testers.map(({ username }) => { const isTesterAssigned = @@ -314,19 +290,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} + + ); + })} ); @@ -382,110 +363,15 @@ const TestQueueRow = ({ } }; - const updateReportStatus = async status => { + const updateReportStatus = async () => { try { await triggerLoad(async () => { - if (status === 'CANDIDATE') { - // Get list of testPlanReports which are already in CANDIDATE phase and check to see which - // is also the same pattern as what's being updated here to provide as a date option - const { data } = await client.query({ - query: TEST_PLAN_REPORT_CANDIDATE_RECOMMENDED_QUERY, - fetchPolicy: 'network-only' - }); - - // TODO: Check to see if the proposed test plan report to be promoted to CANDIDATE - // has a test plan version date less than whatever is in CANDIDATE or RECOMMENDED - // tests already, if any, and note it may never be displayed to the admin - - // --- SECTION START: OutdatedCandidatePhaseComparison - // Check to see if there are candidate test reports are already being compared - // which can never be shown in the test reports page as it currently is, so do - // not show it as an existing candidate phase option - const ignoredIds = []; - - const directory = testPlanVersion.testPlan.directory; - const candidateReports = data.testPlanReports.filter( - r => - r.status === 'CANDIDATE' && - r.testPlanVersion.testPlan.directory === directory - ); - - const recommendedReports = data.testPlanReports.filter( - r => - r.status === 'RECOMMENDED' && - r.testPlanVersion.testPlan.directory === directory - ); - - recommendedReports.forEach(r => { - candidateReports.forEach(t => { - if ( - !ignoredIds.includes(t.id) && - t.at.id == r.at.id && - t.browser.id == r.browser.id && - t.testPlanVersion.testPlan.directory == - r.testPlanVersion.testPlan.directory && - new Date( - t.latestAtVersionReleasedAt.releasedAt - ) < - new Date( - r.latestAtVersionReleasedAt.releasedAt - ) - ) - ignoredIds.push(t.id); - }); - }); - - const filteredCandidateReports = candidateReports.filter( - t => !ignoredIds.includes(t.id) - ); - // --- SECTION END: OutdatedCandidatePhaseComparison - - const dates = filteredCandidateReports - .map(c => { - return { - candidateStatusReachedAt: - c.candidateStatusReachedAt, - recommendedStatusTargetDate: - c.recommendedStatusTargetDate - }; - }) - // Sort in descending order of dates (top is the latest date the existing - // candidate report status was reached at) - .sort((a, b) => - new Date(a.candidateStatusReachedAt) > - new Date(b.candidateStatusReachedAt) - ? -1 - : 1 - ); - - const stringifiedDates = dates.map(d => JSON.stringify(d)); - const uniq = [...new Set(stringifiedDates)]; - const candidatePhaseList = uniq.map(u => JSON.parse(u)); - setCandidatePhaseTestPlanReports(candidatePhaseList); - - if (candidatePhaseList.length > 0) { - // There already exists a Test Plan Report which uses this pattern so - // there is already a candidate phase to select from - setShowCandidatePhaseSelectModal(true); - } else { - await updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status: status - } - }); - await triggerPageUpdate(); + await updateTestPlanMarkedFinalAt({ + variables: { + testReportId: testPlanReport.id } - } else { - // Create a new candidate phase since no others exist - await updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status: status - } - }); - await triggerTestPlanReportUpdate(); - } + }); + await triggerPageUpdate(); }, 'Updating Test Plan Status'); } catch (e) { showThemedMessage( @@ -496,47 +382,33 @@ const TestQueueRow = ({ } }; - const evaluateStatusAndResults = () => { - const { status: runStatus, conflictsLength } = testPlanReport; + const evaluateLabelStatus = () => { + const { conflictsLength } = testPlanReport; - 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 { + labelStatus = ( + Draft + ); } - return null; + return labelStatus; }; - const { status, results } = evaluateStatusAndResults(); - const nextReportStatus = evaluateNewReportStatus(); - const getRowId = tester => [ 'plan', @@ -548,45 +420,6 @@ const TestQueueRow = ({ 'completed' ].join('-'); - const onHandleCandidatePhaseSelectModalAction = async date => { - let variables = {}; - - if (date) { - const { candidateStatusReachedAt, recommendedStatusTargetDate } = - date; - variables = { - candidateStatusReachedAt, - recommendedStatusTargetDate - }; - } - - // Null 'date' if candidate phase is to be created; promote to candidate phase as normal - try { - await triggerLoad(async () => { - setShowCandidatePhaseSelectModal(false); - await updateTestPlanReportStatus({ - variables: { - testReportId: testPlanReport.id, - status: 'CANDIDATE', - ...variables - } - }); - await triggerPageUpdate(); - }, 'Updating Test Plan Status'); - } catch (e) { - showThemedMessage( - 'Error Updating Test Plan Status', - <>{e.message}, - 'warning' - ); - } - }; - - const onCandidatePhaseSelectModalClose = () => { - setShowCandidatePhaseSelectModal(false); - focusButtonRef.current.focus(); - }; - return ( @@ -657,30 +490,28 @@ const TestQueueRow = ({ -
    {status}
    +
    + {evaluateLabelStatus()} +
    {isSignedIn && isTester && (
    - {isAdmin && !isLoading && nextReportStatus && ( - <> - - - )} - {results} + {isAdmin && + !isLoading && + !testPlanReport.conflictsLength && ( + <> + + + )}
    )} @@ -782,15 +613,6 @@ const TestQueueRow = ({ triggerTestPlanReportUpdate={triggerTestPlanReportUpdate} /> )} - {showCandidatePhaseSelectModal && ( - - )} {showThemedModal && ( issue.testNumberFilteredByAt == test.seq + issue => + issue.isCandidateReview && + issue.testNumberFilteredByAt == test.seq ).length; if (test) { diff --git a/client/components/TestRun/index.jsx b/client/components/TestRun/index.jsx index 2e3359382..d86cbd9e1 100644 --- a/client/components/TestRun/index.jsx +++ b/client/components/TestRun/index.jsx @@ -38,63 +38,8 @@ import { import { evaluateAuth } from '../../utils/evaluateAuth'; import './TestRun.css'; import ReviewConflicts from '../ReviewConflicts'; - -const createGitHubIssueWithTitleAndBody = ({ - test = {}, - testPlanReport = {}, - atVersion, - browserVersion, - conflictMarkdown = null, - fromReportPageLink = null -}) => { - // TODO: fix renderedUrl - let modifiedRenderedUrl = test?.renderedUrl?.replace( - /.+(?=\/tests)/, - 'https://aria-at.netlify.app' - ); - - const { testPlanVersion = {}, at = {}, browser = {} } = testPlanReport; - - const title = - `Feedback: "${test?.title}" (${testPlanVersion.title}, ` + - `Test ${test?.rowNumber})`; - - const shortenedUrl = modifiedRenderedUrl?.match(/[^/]+$/)[0]; - - let body = - `## Description of Behavior\n\n` + - `\n\n` + - `## Test Setup\n\n` + - `- Test File: ` + - `[${shortenedUrl}](${modifiedRenderedUrl})\n` + - `- AT: ` + - `${at.name} (version ${atVersion})\n` + - `- Browser: ` + - `${browser.name} (version ${browserVersion})\n`; - - if (fromReportPageLink) - body = - `## Description of Behavior\n\n` + - `\n\n` + - `## Test Setup\n\n` + - `- Test File: ` + - `[${shortenedUrl}](${modifiedRenderedUrl})\n` + - `- Report Page: ` + - `[Link](${fromReportPageLink})\n` + - `- AT: ` + - `${at.name}\n` + - `- Browser: ` + - `${browser.name}\n`; - - if (conflictMarkdown) { - body += `\n${conflictMarkdown}`; - } - - return ( - `https://github.com/w3c/aria-at/issues/new?title=${encodeURI(title)}&` + - `body=${encodeURIComponent(body)}` - ); -}; +import createIssueLink from '../../utils/createIssueLink'; +import { convertDateToString } from '../../utils/formatter'; const TestRun = () => { const params = useParams(); @@ -387,14 +332,26 @@ const TestRun = () => { adminReviewerCheckedRef.current = true; - // For creating content for GitHub Issue - const gitHubIssueLinkWithTitleAndBody = createGitHubIssueWithTitleAndBody({ - test: currentTest, - testPlanReport, - atVersion: currentAtVersion?.name, - browserVersion: currentBrowserVersion?.name, - conflictMarkdown: conflictMarkdownRef.current - }); + let issueLink; + const hasLoadingCompleted = Object.keys(currentTest).length; + if (hasLoadingCompleted) { + issueLink = createIssueLink({ + testPlanTitle: testPlanVersion.title, + testPlanDirectory: testPlanVersion.testPlan.directory, + versionString: `V${convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}`, + testTitle: currentTest.title, + testRowNumber: currentTest.rowNumber, + testRenderedUrl: currentTest.renderedUrl, + atName: testPlanReport.at.name, + browserName: testPlanReport.browser.name, + atVersionName: currentAtVersion?.name, + browserVersionName: currentBrowserVersion?.name, + conflictMarkdown: conflictMarkdownRef.current + }); + } const remapScenarioResults = ( rendererState, @@ -953,7 +910,7 @@ const TestRun = () => { /> } target="_blank" - href={gitHubIssueLinkWithTitleAndBody} + href={issueLink} />
  • @@ -1108,7 +1065,7 @@ const TestRun = () => { test={currentTest} key={`ReviewConflictsModal__${currentTestIndex}`} userId={testerId} - issueLink={gitHubIssueLinkWithTitleAndBody} + issueLink={issueLink} handleClose={() => setShowReviewConflictsModal(false)} /> )} @@ -1328,5 +1285,4 @@ const TestRun = () => { ); }; -export { createGitHubIssueWithTitleAndBody }; export default TestRun; diff --git a/client/components/TestRun/queries.js b/client/components/TestRun/queries.js index 8e334021c..fcb09ec9e 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,8 @@ export const TEST_RUN_PAGE_QUERY = gql` testPlanVersion { id title + phase + updatedAt gitSha testPageUrl testPlan { @@ -150,7 +151,6 @@ export const TEST_RUN_PAGE_ANON_QUERY = gql` query TestPlanRunAnonPage($testPlanReportId: ID!) { testPlanReport(id: $testPlanReportId) { id - status conflicts { source { test { @@ -208,6 +208,8 @@ export const TEST_RUN_PAGE_ANON_QUERY = gql` testPlanVersion { id title + phase + updatedAt gitSha testPageUrl testPlan { @@ -295,7 +297,6 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -353,6 +354,7 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -444,7 +446,6 @@ export const SAVE_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -502,6 +503,7 @@ export const SAVE_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { @@ -593,7 +595,6 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` } testPlanReport { id - status conflicts { source { test { @@ -651,6 +652,7 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` testPlanVersion { id title + phase gitSha testPageUrl testPlan { diff --git a/client/components/common/AtAndBrowserDetailsModal/index.jsx b/client/components/common/AtAndBrowserDetailsModal/index.jsx index 59fce4708..b5785637d 100644 --- a/client/components/common/AtAndBrowserDetailsModal/index.jsx +++ b/client/components/common/AtAndBrowserDetailsModal/index.jsx @@ -601,6 +601,7 @@ const AtAndBrowserDetailsModal = ({ handleClose={!isFirstLoad ? handleClose : null} handleHide={handleHide} staticBackdrop={true} + useOnHide={true} /> )} diff --git a/client/components/common/BasicModal/index.jsx b/client/components/common/BasicModal/index.jsx index 8d0a23dd8..8331d6b82 100644 --- a/client/components/common/BasicModal/index.jsx +++ b/client/components/common/BasicModal/index.jsx @@ -26,7 +26,8 @@ const BasicModal = ({ handleClose = null, handleAction = null, handleHide = null, - staticBackdrop = false + staticBackdrop = false, + useOnHide = false }) => { const headerRef = useRef(); @@ -41,7 +42,8 @@ const BasicModal = ({ show={show} centered={centered} animation={animation} - onHide={handleHide || handleClose} + onHide={useOnHide ? handleHide || handleClose : null} + onExit={!useOnHide ? handleHide || handleClose : null} /* Disabled due to buggy implementation which jumps the page */ autoFocus={false} aria-labelledby="basic-modal" @@ -101,7 +103,8 @@ BasicModal.propTypes = { handleClose: PropTypes.func, handleAction: PropTypes.func, handleHide: PropTypes.func, - staticBackdrop: PropTypes.bool + staticBackdrop: PropTypes.bool, + useOnHide: PropTypes.bool }; export default BasicModal; diff --git a/client/components/common/BasicThemedModal/index.jsx b/client/components/common/BasicThemedModal/index.jsx index c7b019497..428a3e445 100644 --- a/client/components/common/BasicThemedModal/index.jsx +++ b/client/components/common/BasicThemedModal/index.jsx @@ -56,7 +56,7 @@ const BasicThemedModal = ({ show={show} centered={centered} animation={animation} - onHide={handleClose} + onExit={handleClose} /* Disabled due to buggy implementation which jumps the page */ autoFocus={false} aria-labelledby="basic-modal" diff --git a/client/components/common/FilterButtons/index.jsx b/client/components/common/FilterButtons/index.jsx new file mode 100644 index 000000000..5e64521ca --- /dev/null +++ b/client/components/common/FilterButtons/index.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Button } from 'react-bootstrap'; + +const StyledFilterButton = styled(Button)` + background: #e9ebee; + border-radius: 16px; + margin-left: 12px; + background-color: white; + font-weight: 400; + + &.active, + &:active { + background: #eaf3fe !important; + border: #517dbc 2px solid !important; + box-shadow: none !important; + + &:hover, + :focus { + box-shadow: 0 0 0 0.2rem rgba(103, 171, 197, 0.5) !important; + } + } +`; + +const FilterButtons = ({ + filterOptions, + optionLabels, + activeFilter, + onFilterChange +}) => { + return ( +
      + + {Object.keys(optionLabels).map(key => { + const option = filterOptions[key]; + const isActive = activeFilter === option; + return ( +
    • + onFilterChange(option)} + > + {optionLabels[option]} + +
    • + ); + })} +
    + ); +}; + +FilterButtons.propTypes = { + filterOptions: PropTypes.objectOf(PropTypes.string).isRequired, + optionLabels: PropTypes.objectOf(PropTypes.string).isRequired, + activeFilter: PropTypes.string.isRequired, + onFilterChange: PropTypes.func.isRequired +}; + +export default FilterButtons; diff --git a/client/components/common/PhasePill/index.jsx b/client/components/common/PhasePill/index.jsx new file mode 100644 index 000000000..992dce3b8 --- /dev/null +++ b/client/components/common/PhasePill/index.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { derivePhaseName } from '@client/utils/aria'; +import styled from '@emotion/styled'; + +const PhaseText = styled.span` + display: inline-block; + padding: 2px 4px; + border-radius: 14px; + + text-align: center; + overflow: hidden; + white-space: nowrap; + color: white; + + &.full-width { + width: 100%; + } + &:not(.full-width) { + width: min-content; + padding: 2px 15px; + vertical-align: middle; + position: relative; + top: -4px; + margin-top: 4px; /* Improve appearance when text wraps */ + } + + &.rd { + background: #4177de; + } + + &.draft { + background: #818f98; + } + + &.candidate { + background: #ff6c00; + } + + &.recommended { + background: #8441de; + } + + &.deprecated { + background: #ce1b4c; + } +`; + +const PhasePill = ({ fullWidth = true, children: phase }) => { + return ( + str) + .join(' ')} + > + {derivePhaseName(phase)} + + ); +}; + +PhasePill.propTypes = { + fullWidth: PropTypes.bool, + children: PropTypes.string.isRequired +}; + +export default PhasePill; diff --git a/client/components/common/ReportStatusDot/index.jsx b/client/components/common/ReportStatusDot/index.jsx new file mode 100644 index 000000000..70c0e469d --- /dev/null +++ b/client/components/common/ReportStatusDot/index.jsx @@ -0,0 +1,32 @@ +import styled from '@emotion/styled'; + +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; + } +`; + +export default ReportStatusDot; diff --git a/client/components/common/SortableTableHeader/index.js b/client/components/common/SortableTableHeader/index.js new file mode 100644 index 000000000..8dc1a24c2 --- /dev/null +++ b/client/components/common/SortableTableHeader/index.js @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-bootstrap'; +import styled from '@emotion/styled'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faArrowDownShortWide, + faArrowUpShortWide +} from '@fortawesome/free-solid-svg-icons'; +import { useAriaLiveRegion } from '../../providers/AriaLiveRegionProvider'; + +const SortableTableHeaderWrapper = styled.th` + position: relative; + padding: 0; +`; + +const SortableTableHeaderButton = styled(Button)` + background: #e9ebee; + border: none; + color: black; + font-size: 1rem; + padding: 0; + font-weight: 700; + text-align: left; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + justify-content: space-between; + align-items: flex-end; + padding: 8px 12px; + border-radius: 0; + z-index: 0; + display: flex; + &:hover, + &:focus { + background: #e9ebee; + z-index: 1; + color: #0b60ab; + background-color: var(--bs-table-hover-bg); + } + + &:hover { + border: none; + } +`; + +const InactiveIcon = styled(FontAwesomeIcon)` + color: rgb(155, 155, 155); +`; + +export const TABLE_SORT_ORDERS = { + ASC: 'ASCENDING', + DESC: 'DESCENDING' +}; + +const SortableTableHeader = ({ + title, + active, + onSort = () => {}, + initialSortDirection = TABLE_SORT_ORDERS.ASC +}) => { + const [currentSortOrder, setCurrentSortOrder] = + useState(initialSortDirection); + + const { setMessage } = useAriaLiveRegion(); + + useEffect(() => { + if (!setMessage) return; + if (active) { + const message = ` now sorted by ${title} in ${currentSortOrder.toLowerCase()} order`; + setMessage(message); + } + }, [active, currentSortOrder, setMessage, title]); + + const handleClick = () => { + if (active) { + const newSortOrder = + currentSortOrder === TABLE_SORT_ORDERS.ASC + ? TABLE_SORT_ORDERS.DESC + : TABLE_SORT_ORDERS.ASC; + setCurrentSortOrder(newSortOrder); + onSort(newSortOrder); + } else { + onSort(currentSortOrder); + } + }; + + const getIcon = () => { + const icon = + currentSortOrder === TABLE_SORT_ORDERS.ASC + ? faArrowUpShortWide + : faArrowDownShortWide; + + const attribs = { + 'aria-hidden': 'true', + focusable: 'false', + icon: icon + }; + + if (active) { + return ; + } else { + return ; + } + }; + + const getAriaSort = () => { + if (!active) { + return 'none'; + } else { + return currentSortOrder === TABLE_SORT_ORDERS.ASC + ? 'ascending' + : 'descending'; + } + }; + + return ( + + + {title} + {getIcon()} + + + ); +}; + +SortableTableHeader.propTypes = { + title: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired, + onSort: PropTypes.func.isRequired, + initialSortDirection: PropTypes.oneOf([ + TABLE_SORT_ORDERS.ASC, + TABLE_SORT_ORDERS.DESC + ]) +}; + +export default SortableTableHeader; diff --git a/client/components/common/ThemeTable/index.jsx b/client/components/common/ThemeTable/index.jsx new file mode 100644 index 000000000..12d2e739c --- /dev/null +++ b/client/components/common/ThemeTable/index.jsx @@ -0,0 +1,38 @@ +import { Table } from 'react-bootstrap'; +import styled from '@emotion/styled'; + +export const ThemeTableHeaderH2 = styled.h2` + background-color: var(--bs-table-bg) !important; + font-size: 1.5rem; + font-weight: 600; + border: solid 1px #d2d5d9; + border-bottom: none; + padding: 0.5rem 1rem; + margin: 0.5rem 0 0 0; +`; + +export const ThemeTableHeaderH3 = styled.h3` + background-color: var(--bs-table-bg) !important; + font-size: 1.25rem; + font-weight: 600; + border: solid 1px #d2d5d9; + border-bottom: none; + padding: 0.5rem 1rem; + margin: 0.5rem 0 0 0; +`; + +export const ThemeTable = styled(Table)` + margin-bottom: 0; + + td, + th { + padding-left: 1rem; + min-width: 165px; + vertical-align: middle; + } +`; + +export const ThemeTableUnavailable = styled.div` + border: solid 1px #d2d5d9; + padding: 0.5rem 1rem; +`; diff --git a/client/components/common/VersionString/index.js b/client/components/common/VersionString/index.js new file mode 100644 index 000000000..45a4f9601 --- /dev/null +++ b/client/components/common/VersionString/index.js @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { faCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { convertDateToString } from '../../../utils/formatter'; +import styled from '@emotion/styled'; + +const StyledPill = styled.span` + display: inline-block; + + line-height: 2rem; + border-radius: 4px; + + background: #f6f8fa; + white-space: nowrap; + text-align: center; + + // Needed for presenting component on Version History page + &.full-width { + width: 100%; + } + + &:not(.full-width) { + width: 8em; + margin-right: 10px; + } + + // Needed for presenting component on Data Management page + // Override full-width's width if both are set + &.auto-width { + width: auto; + margin: 0.75rem; + } +`; + +const VersionString = ({ + date, + fullWidth = true, + autoWidth = true, + iconColor = '#818F98', + linkRef, + linkHref, + ...restProps +}) => { + const dateString = convertDateToString(date, 'YY.MM.DD'); + + const body = ( + + + {'V' + dateString} + + ); + + let possibleLink; + if (linkHref) { + if (linkRef) { + possibleLink = ( + + {body} + + ); + } else { + possibleLink = ( + + {body} + + ); + } + } else { + possibleLink = body; + } + + let classes = fullWidth ? 'full-width' : ''; + classes = autoWidth ? `${classes} auto-width` : classes; + + return ( + + {possibleLink} + + ); +}; + +VersionString.propTypes = { + date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]) + .isRequired, + fullWidth: PropTypes.bool, + autoWidth: PropTypes.bool, + iconColor: PropTypes.string, + linkRef: PropTypes.shape({ current: PropTypes.any }), + linkHref: PropTypes.string +}; + +export default VersionString; diff --git a/client/components/providers/AriaLiveRegionProvider/index.js b/client/components/providers/AriaLiveRegionProvider/index.js new file mode 100644 index 000000000..33b8277e2 --- /dev/null +++ b/client/components/providers/AriaLiveRegionProvider/index.js @@ -0,0 +1,56 @@ +import React, { useState, useContext, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; + +const AriaLiveRegionContext = React.createContext(); + +const VisuallyHiddenAriaLiveRegion = styled.span` + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +`; + +export const useAriaLiveRegion = () => { + const context = useContext(AriaLiveRegionContext); + if (!context) { + console.warn( + 'useAriaLiveRegion must be used within an AriaLiveRegionProvider' + ); + } + return context; +}; + +export const AriaLiveRegionProvider = ({ baseMessage = '', children }) => { + const [message, setMessage] = useState(baseMessage); + + useEffect(() => { + return () => { + setMessage(''); + }; + }, []); + + const updateMessage = newMessage => { + setMessage(baseMessage + newMessage); + }; + + return ( + + {children} + + {message} + + + ); +}; + +AriaLiveRegionProvider.propTypes = { + baseMessage: PropTypes.string, + children: PropTypes.node.isRequired +}; diff --git a/client/package.json b/client/package.json index 4e21d6280..9f15e073c 100644 --- a/client/package.json +++ b/client/package.json @@ -30,7 +30,6 @@ "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", - "@react-hook/resize-observer": "^1.2.6", "bootstrap": "^5.2.3", "core-js": "^3.8.0", "graphql": "^16.6.0", @@ -59,13 +58,13 @@ "@babel/preset-env": "^7.8.7", "@babel/preset-react": "^7.8.3", "@lhci/cli": "^0.11.0", - "@storybook/builder-webpack5": "^6.5.16", - "@storybook/manager-webpack5": "^6.5.16", "@storybook/addon-a11y": "^6.5.16", "@storybook/addon-actions": "^6.5.16", "@storybook/addon-controls": "^6.5.16", "@storybook/addon-links": "^6.5.16", "@storybook/addons": "^6.5.16", + "@storybook/builder-webpack5": "^6.5.16", + "@storybook/manager-webpack5": "^6.5.16", "@storybook/react": "^6.5.16", "@testing-library/dom": "^9.0.0", "@testing-library/jest-dom": "^5.16.5", @@ -112,9 +111,6 @@ "setupFilesAfterEnv": [ "./jest.setup.js" ], - "transformIgnorePatterns": [ - "/node_modules/(?!(@juggle/resize-observer|@react-hook/resize-observer)/)" - ], "transform": { "^.+\\.(js|jsx|mjs)$": "babel-jest" }, diff --git a/client/routes/index.js b/client/routes/index.js index 0da144954..8da183e31 100644 --- a/client/routes/index.js +++ b/client/routes/index.js @@ -5,13 +5,14 @@ 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'; +import TestPlanVersionsPage from '../components/TestPlanVersionsPage'; export default () => ( @@ -58,21 +59,18 @@ export default () => ( } /> - + } /> + } /> - - - } + path="/data-management/:testPlanDirectory/" + element={} /> } /> } /> 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/AddTestToQueueWithConfirmation.test.jsx b/client/tests/AddTestToQueueWithConfirmation.test.jsx new file mode 100644 index 000000000..8fe312668 --- /dev/null +++ b/client/tests/AddTestToQueueWithConfirmation.test.jsx @@ -0,0 +1,101 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, waitFor, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { MockedProvider } from '@apollo/client/testing'; + +// eslint-disable-next-line jest/no-mocks-import +import { TEST_QUEUE_MUTATION_MOCK } from './__mocks__/GraphQLMocks'; +import AddTestToQueueWithConfirmation from '../components/AddTestToQueueWithConfirmation'; +import { BrowserRouter } from 'react-router-dom'; +import { InMemoryCache, useMutation } from '@apollo/client'; + +jest.mock('@apollo/client'); + +let mutationMock; +let mockTestPlanVersion, + mockBrowser, + mockAt, + mockButtonText, + getByTestId, + findByRole; + +const setup = (props, mockMutation) => { + useMutation.mockReturnValue([mockMutation, {}]); + return render( + + + + + + ); +}; + +const commonSetup = mockMutation => { + mockTestPlanVersion = { id: 5 }; + mockBrowser = { id: 2 }; + mockAt = { id: 3 }; + mockButtonText = 'Add to Test Queue'; + + const renderResult = setup( + { + testPlanVersion: mockTestPlanVersion, + browser: mockBrowser, + at: mockAt, + buttonText: mockButtonText + }, + mockMutation + ); + + getByTestId = renderResult.getByTestId; + findByRole = renderResult.findByRole; +}; + +describe('AddTestToQueueWithConfirmation', () => { + beforeEach(() => { + mutationMock = jest.fn().mockResolvedValue(TEST_QUEUE_MUTATION_MOCK); + commonSetup(mutationMock); + }); + + test('renders Button without error', async () => { + await waitFor(() => + expect(getByTestId('add-button')).toBeInTheDocument() + ); + }); + + test('Button has correct text', async () => { + await waitFor(() => + expect(getByTestId('add-button')).toHaveTextContent(mockButtonText) + ); + }); + + test('renders BasicModal without error', async () => { + fireEvent.click(getByTestId('add-button')); + + await waitFor(async () => { + const modal = await findByRole('dialog'); + expect(modal).toBeInTheDocument(); + }); + }); + + test('calls mutation on button click with correct variables', async () => { + fireEvent.click(getByTestId('add-button')); + + await waitFor(() => { + expect(mutationMock).toHaveBeenCalled(); + expect(mutationMock).toHaveBeenCalledWith({ + variables: { + testPlanVersionId: mockTestPlanVersion.id, + atId: mockAt.id, + browserId: mockBrowser.id + } + }); + }); + }); +}); diff --git a/client/tests/DataManagement.test.jsx b/client/tests/DataManagement.test.jsx new file mode 100644 index 000000000..bec23e584 --- /dev/null +++ b/client/tests/DataManagement.test.jsx @@ -0,0 +1,290 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, renderHook, 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_MOCK_DATA } from './__mocks__/GraphQLMocks'; +import { act } from 'react-dom/test-utils'; +import { + useDataManagementTableFiltering, + useDataManagementTableSorting, + useDerivedOverallPhaseByTestPlanId, + useTestPlanVersionsByPhase, + useTestPlansByPhase +} from '../components/DataManagement/filterSortHooks'; +import { + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS, + DATA_MANAGEMENT_TABLE_SORT_OPTIONS +} from '../components/DataManagement/utils'; +import { TABLE_SORT_ORDERS } from '../components/common/SortableTableHeader'; + +const setup = (mocks = []) => { + return render( + + + + + + ); +}; + +describe('Data Management page', () => { + let wrapper; + + beforeEach(() => { + wrapper = setup(DATA_MANAGEMENT_PAGE_POPULATED_MOCK_DATA); + }); + + 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); + }); +}); + +const testPlans = [ + { title: 'Test A', directory: 'dirA', id: '1' }, + { title: 'Test B', directory: 'dirB', id: '2' }, + { title: 'Test C', directory: 'dirC', id: '3' }, + { title: 'Test D', directory: 'dirD', id: '4' } +]; + +const testPlanVersions = [ + { + phase: 'RD', + id: '101', + testPlan: { directory: 'dirA' }, + updatedAt: '2022-03-17T18:34:51.000Z' + }, + { + phase: 'DRAFT', + id: '102', + testPlan: { directory: 'dirB' }, + draftStatusReachedAt: '2022-05-18T20:51:40.000Z' + }, + { + phase: 'CANDIDATE', + id: '103', + testPlan: { directory: 'dirC' }, + candidatePhaseReachedAt: '2022-04-10T00:00:00.000Z' + }, + { + phase: 'RD', + id: '104', + testPlan: { directory: 'dirD' }, + updatedAt: '2022-03-18T18:34:51.000Z' + }, + { + phase: 'RECOMMENDED', + id: '105', + testPlan: { directory: 'dirD' }, + recommendedPhaseReachedAt: '2022-05-18T20:51:40.000Z' + } +]; + +const ats = []; // ATs are stubbed until this model is defined + +describe('useDataManagementTableSorting hook', () => { + it('sorts by phase by default', () => { + const { result } = renderHook(() => + useDataManagementTableSorting( + testPlans, + testPlanVersions, + ats, + TABLE_SORT_ORDERS.DESC + ) + ); + expect(result.current.sortedTestPlans).toEqual([ + testPlans[3], // RECOMMENDED + testPlans[2], // CANDIDATE + testPlans[1], // DRAFT + testPlans[0] // RD + ]); + }); + + it('can sort by name', () => { + const { result } = renderHook(() => + useDataManagementTableSorting(testPlans, testPlanVersions, ats) + ); + act(() => + result.current.updateSort({ + key: DATA_MANAGEMENT_TABLE_SORT_OPTIONS.NAME, + direction: TABLE_SORT_ORDERS.ASC + }) + ); + expect(result.current.sortedTestPlans).toEqual(testPlans); + }); +}); + +describe('useDataManagementTableFiltering hook', () => { + it('shows all plans by default', () => { + const { result } = renderHook(() => + useDataManagementTableFiltering( + testPlans, + testPlanVersions, + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL + ) + ); + expect(result.current.filteredTestPlans).toEqual(testPlans); + expect( + result.current.filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL + ] + ).toEqual(`All Plans (${testPlans.length})`); + }); + + it('can filter by RD phase', () => { + const { result } = renderHook(() => + useDataManagementTableFiltering( + testPlans, + testPlanVersions, + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RD + ) + ); + expect(result.current.filteredTestPlans).toEqual([ + testPlans[0] // RD + ]); + expect( + result.current.filterLabels[DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RD] + ).toEqual(`R&D Complete (1)`); + }); + + it('can filter by DRAFT phase', () => { + const { result } = renderHook(() => + useDataManagementTableFiltering( + testPlans, + testPlanVersions, + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.DRAFT + ) + ); + expect(result.current.filteredTestPlans).toEqual([ + testPlans[1] // DRAFT + ]); + expect( + result.current.filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.DRAFT + ] + ).toEqual(`In Draft Review (1)`); + }); + + it('can filter by CANDIDATE phase', () => { + const { result } = renderHook(() => + useDataManagementTableFiltering( + testPlans, + testPlanVersions, + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.CANDIDATE + ) + ); + expect(result.current.filteredTestPlans).toEqual([ + testPlans[2] // CANDIDATE + ]); + expect( + result.current.filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.CANDIDATE + ] + ).toEqual(`In Candidate Review (1)`); + }); + + it('can filter by RECOMMENDED phase', () => { + const { result } = renderHook(() => + useDataManagementTableFiltering( + testPlans, + testPlanVersions, + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED + ) + ); + expect(result.current.filteredTestPlans).toEqual([ + testPlans[3] // RECOMMENDED + ]); + expect( + result.current.filterLabels[ + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED + ] + ).toEqual(`Recommended Plans (1)`); + }); +}); + +describe('useTestPlanVersionsByPhase hook', () => { + it('returns an object with test plan versions grouped by phase', () => { + const { result } = renderHook(() => + useTestPlanVersionsByPhase(testPlanVersions) + ); + const { testPlanVersionsByPhase } = result.current; + expect(testPlanVersionsByPhase).toEqual({ + RD: [testPlanVersions[0], testPlanVersions[3]], + DRAFT: [testPlanVersions[1]], + CANDIDATE: [testPlanVersions[2]], + RECOMMENDED: [testPlanVersions[4]] + }); + }); +}); + +describe('useDerivedTestPlanOverallPhase hook', () => { + it('returns an object with the overall phase mapped to each test plan id', () => { + const { result } = renderHook(() => + useDerivedOverallPhaseByTestPlanId(testPlans, testPlanVersions) + ); + const { derivedOverallPhaseByTestPlanId } = result.current; + expect(derivedOverallPhaseByTestPlanId).toEqual({ + 1: 'RD', + 2: 'DRAFT', + 3: 'CANDIDATE', + 4: 'RECOMMENDED' + }); + }); +}); + +describe('useTestPlansByPhase hook', () => { + it('returns an object with test plans grouped by overall phase', () => { + const { result } = renderHook(() => + useTestPlansByPhase(testPlans, testPlanVersions) + ); + const { testPlansByPhase } = result.current; + expect(testPlansByPhase).toEqual({ + RD: [testPlans[0]], + DRAFT: [testPlans[1]], + CANDIDATE: [testPlans[2]], + RECOMMENDED: [testPlans[3]] + }); + }); +}); diff --git a/client/tests/FilterButtons.test.jsx b/client/tests/FilterButtons.test.jsx new file mode 100644 index 000000000..797f54e1b --- /dev/null +++ b/client/tests/FilterButtons.test.jsx @@ -0,0 +1,63 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import FilterButtons from '../components/common/FilterButtons'; +import { DATA_MANAGEMENT_TABLE_FILTER_OPTIONS } from '../components/DataManagement/utils'; + +describe('FilterButtons', () => { + const filterOptions = DATA_MANAGEMENT_TABLE_FILTER_OPTIONS; + const optionLabels = { + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RD]: `R&D Complete`, + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.DRAFT]: `In Draft Review`, + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.CANDIDATE]: `In Candidate Review`, + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED]: `Recommended Plans`, + [DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL]: `All Plans` + }; + const defaultProps = { + filterOptions, + optionLabels, + activeFilter: DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL, + onFilterChange: () => {} + }; + + it('should render without crashing', () => { + render(); + expect(screen.getByRole('group')).toBeInTheDocument(); + }); + + it('should render the correct filter labels', () => { + render(); + Object.values(optionLabels).forEach(label => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + }); + + it('should render the active filter with correct styles', () => { + render(); + const activeButton = screen + .getByText(optionLabels[DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.ALL]) + .closest('button'); + expect(activeButton).toHaveAttribute('aria-pressed', 'true'); + expect(activeButton).toHaveClass('active'); + }); + + it('should change filter on button click', () => { + const onFilterChange = jest.fn(); + render( + + ); + const button = screen + .getByText( + optionLabels[DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED] + ) + .closest('button'); + fireEvent.click(button); + expect(onFilterChange).toHaveBeenCalledWith( + DATA_MANAGEMENT_TABLE_FILTER_OPTIONS.RECOMMENDED + ); + }); +}); diff --git a/client/tests/SortableTableHeader.test.jsx b/client/tests/SortableTableHeader.test.jsx new file mode 100644 index 000000000..84829dcef --- /dev/null +++ b/client/tests/SortableTableHeader.test.jsx @@ -0,0 +1,75 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import SortableTableHeader, { + TABLE_SORT_ORDERS +} from '../components/common/SortableTableHeader'; +import '@testing-library/jest-dom'; +import { AriaLiveRegionProvider } from '../components/providers/AriaLiveRegionProvider'; + +const renderComponent = props => + render( + + + + + + + +
    +
    + ); + +const getAriaSort = (active, sortOrder) => + active + ? sortOrder === TABLE_SORT_ORDERS.ASC + ? 'ascending' + : 'descending' + : 'none'; + +describe('SortableTableHeader component', () => { + const defaultProps = { title: 'Header', active: false, onSort: () => {} }; + + it('should render without crashing', () => { + renderComponent(defaultProps); + expect(screen.getByRole('columnheader')).toBeInTheDocument(); + }); + + it('should render the correct title', () => { + renderComponent(defaultProps); + expect(screen.getByText('Header')).toBeInTheDocument(); + }); + + it('should render the inactive icon when active is false', () => { + renderComponent(defaultProps); + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); + }); + + it('should handle sorting order and aria-sort attribute when active is true and clicked', () => { + const onSort = jest.fn(); + renderComponent({ ...defaultProps, active: true, onSort }); + const button = screen.getByRole('button'); + + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + getAriaSort(true, TABLE_SORT_ORDERS.ASC) + ); + + fireEvent.click(button); + expect(onSort).toHaveBeenCalledWith(TABLE_SORT_ORDERS.DESC); + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + getAriaSort(true, TABLE_SORT_ORDERS.DESC) + ); + + fireEvent.click(button); + expect(onSort).toHaveBeenCalledWith(TABLE_SORT_ORDERS.ASC); + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + getAriaSort(true, TABLE_SORT_ORDERS.ASC) + ); + }); +}); 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/TestPlanReportStatusDialog.test.jsx b/client/tests/TestPlanReportStatusDialog.test.jsx new file mode 100644 index 000000000..ee16efe9a --- /dev/null +++ b/client/tests/TestPlanReportStatusDialog.test.jsx @@ -0,0 +1,68 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { BrowserRouter } from 'react-router-dom'; +import { InMemoryCache } from '@apollo/client'; +import TestPlanReportStatusDialog from '../components/TestPlanReportStatusDialog'; + +// eslint-disable-next-line jest/no-mocks-import +import { TEST_PLAN_REPORT_STATUS_DIALOG_MOCK_DATA } from './__mocks__/GraphQLMocks'; +// eslint-disable-next-line jest/no-mocks-import +import { mockedTestPlanVersion } from './__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock'; + +const setup = (props, mocks = []) => { + return render( + + + + + + ); +}; + +describe('TestPlanReportStatusDialog', () => { + let getByRole, getByText; + + beforeEach(() => { + const show = true; + const handleHide = jest.fn(); + const testPlanVersion = mockedTestPlanVersion; + + const result = setup( + { testPlanVersion, show, handleHide }, + TEST_PLAN_REPORT_STATUS_DIALOG_MOCK_DATA + ); + + getByRole = result.getByRole; + getByText = result.getByText; + }); + + test('renders without error', async () => { + await waitFor(() => expect(getByRole('dialog')).toBeInTheDocument()); + }); + + test('displays the dialog title', async () => { + await waitFor(() => { + expect( + getByText('Report Status for the Test Plan') + ).toBeInTheDocument(); + }); + }); + + test('displays the table headers', async () => { + await waitFor(() => { + expect(getByText('Required')).toBeInTheDocument(); + expect(getByText('AT')).toBeInTheDocument(); + expect(getByText('Browser')).toBeInTheDocument(); + expect(getByText('Report Status')).toBeInTheDocument(); + }); + }); +}); diff --git a/client/tests/TestQueue.test.jsx b/client/tests/TestQueue.test.jsx index 271fd6622..6382d4ff6 100644 --- a/client/tests/TestQueue.test.jsx +++ b/client/tests/TestQueue.test.jsx @@ -13,10 +13,10 @@ import TestQueue from '../components/TestQueue'; // eslint-disable-next-line jest/no-mocks-import import { - TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_ADMIN, - TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_TESTER, - TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN, - TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER + TEST_QUEUE_PAGE_ADMIN_NOT_POPULATED_MOCK_DATA, + TEST_QUEUE_PAGE_TESTER_NOT_POPULATED_MOCK_DATA, + TEST_QUEUE_PAGE_ADMIN_POPULATED_MOCK_DATA, + TEST_QUEUE_PAGE_TESTER_POPULATED_MOCK_DATA } from './__mocks__/GraphQLMocks'; const setup = (mocks = []) => { @@ -37,7 +37,7 @@ describe('Render TestQueue/index.jsx', () => { describe('[NOT ADMIN] when no test plan reports exist', () => { beforeEach(() => { - wrapper = setup(TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_TESTER); + wrapper = setup(TEST_QUEUE_PAGE_TESTER_NOT_POPULATED_MOCK_DATA); }); it('renders loading state on initialization', async () => { @@ -94,7 +94,7 @@ describe('Render TestQueue/index.jsx', () => { describe('[NOT ADMIN] when test plan reports exist', () => { beforeEach(() => { - wrapper = setup(TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER); + wrapper = setup(TEST_QUEUE_PAGE_TESTER_POPULATED_MOCK_DATA); }); it('renders loading state on initialization', async () => { @@ -167,7 +167,7 @@ describe('Render TestQueue/index.jsx', () => { describe('[IS ADMIN] when no test plan reports exist', () => { beforeEach(() => { - wrapper = setup(TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_ADMIN); + wrapper = setup(TEST_QUEUE_PAGE_ADMIN_NOT_POPULATED_MOCK_DATA); }); it('renders loading state on initialization', async () => { @@ -233,7 +233,7 @@ describe('Render TestQueue/index.jsx', () => { describe('[IS ADMIN] when test plan reports exist', () => { beforeEach(() => { - wrapper = setup(TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN); + wrapper = setup(TEST_QUEUE_PAGE_ADMIN_POPULATED_MOCK_DATA); }); it('renders loading state on initialization', async () => { diff --git a/client/tests/__mocks__/GraphQLMocks.js b/client/tests/__mocks__/GraphQLMocks.js deleted file mode 100644 index 29fe9cff8..000000000 --- a/client/tests/__mocks__/GraphQLMocks.js +++ /dev/null @@ -1,1488 +0,0 @@ -import { TEST_QUEUE_PAGE_QUERY } from '../../components/TestQueue/queries'; -import { TEST_MANAGEMENT_PAGE_QUERY } from '../../components/TestManagement/queries'; - -export const TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_ADMIN = [ - { - request: { - query: TEST_QUEUE_PAGE_QUERY - }, - result: { - data: { - me: { - id: '1', - username: 'foo-bar', - roles: ['ADMIN', 'TESTER'], - __typename: 'User' - }, - ats: [], - browsers: [], - users: [ - { - id: '1', - username: 'foo-bar', - roles: ['ADMIN', 'TESTER'] - }, - { - id: '4', - username: 'bar-foo', - roles: ['TESTER'] - }, - { - id: '5', - username: 'boo-far', - roles: ['TESTER'] - } - ], - testPlanVersions: [], - testPlanReports: [], - testPlans: [] - } - } - } -]; - -export const TEST_QUEUE_PAGE_NOT_POPULATED_MOCK_TESTER = [ - { - request: { - query: TEST_QUEUE_PAGE_QUERY - }, - result: { - data: { - me: { - id: '4', - username: 'bar-foo', - roles: ['TESTER'], - __typename: 'User' - }, - ats: [], - browsers: [], - users: [ - { - id: '1', - username: 'foo-bar', - roles: ['ADMIN', 'TESTER'], - __typename: 'User' - }, - { - id: '4', - username: 'bar-foo', - roles: ['TESTER'], - __typename: 'User' - }, - { - id: '5', - username: 'boo-far', - roles: ['TESTER'], - __typename: 'User' - } - ], - testPlanVersions: [], - testPlanReports: [], - testPlans: [] - } - } - } -]; - -export const TEST_QUEUE_PAGE_POPULATED_MOCK_ADMIN = [ - { - request: { - query: TEST_QUEUE_PAGE_QUERY - }, - result: { - data: { - me: { - id: '101', - username: 'alflennik', - roles: ['ADMIN', 'TESTER'] - }, - ats: [ - { - id: '1', - name: 'JAWS', - atVersions: [ - { - id: '6', - name: '2021.2103.174', - releasedAt: '2022-08-02T14:36:02.659Z' - } - ] - }, - { - id: '2', - name: 'NVDA', - atVersions: [ - { - id: '5', - name: '2020.4', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '4', - name: '2020.3', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '3', - name: '2020.2', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '2', - name: '2020.1', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '1', - name: '2019.3', - releasedAt: '2022-01-01T12:00:00.000Z' - } - ] - }, - { - id: '3', - name: 'VoiceOver for macOS', - atVersions: [ - { - id: '7', - name: '11.5.2', - releasedAt: '2022-01-01T12:00:00.000Z' - } - ] - } - ], - browsers: [ - { - id: '2', - name: 'Chrome' - }, - { - id: '1', - name: 'Firefox' - }, - { - id: '3', - name: 'Safari' - } - ], - users: [ - { - id: '1', - username: 'esmeralda-baggins', - roles: ['TESTER', 'ADMIN'] - }, - { id: '2', username: 'tom-proudfeet', roles: ['TESTER'] }, - { - id: '101', - username: 'alflennik', - roles: ['TESTER', 'ADMIN'] - } - ], - testPlanVersions: [ - { - id: '1', - title: 'Alert Example', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'alert' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - }, - { - id: '2', - title: 'Banner Landmark', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'banner' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - }, - { - id: '3', - title: 'Breadcrumb Example', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'breadcrumb' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - } - ], - testPlanReports: [ - { - id: '1', - status: 'DRAFT', - conflictsLength: 0, - runnableTestsLength: 17, - at: { id: '1', name: 'JAWS' }, - browser: { id: '2', name: 'Chrome' }, - testPlanVersion: { - id: '1', - title: 'Checkbox Example (Two State)', - gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', - gitMessage: 'The message for this SHA', - testPlan: { directory: 'checkbox' }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [ - { - id: '1', - tester: { - id: '1', - username: 'esmeralda-baggins' - }, - testResultsLength: 0 - } - ] - }, - { - id: '2', - status: 'DRAFT', - conflictsLength: 0, - runnableTestsLength: 17, - at: { id: '3', name: 'VoiceOver for macOS' }, - browser: { id: '3', name: 'Safari' }, - testPlanVersion: { - id: '1', - title: 'Checkbox Example (Two State)', - gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', - gitMessage: 'The message for this SHA', - testPlan: { directory: 'checkbox' }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [ - { - id: '1', - tester: { - id: '1', - username: 'esmeralda-baggins' - }, - testResultsLength: 0 - } - ] - }, - { - id: '3', - status: 'DRAFT', - conflictsLength: 3, - runnableTestsLength: 17, - at: { id: '2', name: 'NVDA' }, - browser: { id: '1', name: 'Firefox' }, - testPlanVersion: { - id: '1', - title: 'Checkbox Example (Two State)', - gitSha: 'b7078039f789c125e269cb8f8632f57a03d4c50b', - gitMessage: 'The message for this SHA', - testPlan: { directory: 'checkbox' }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [ - { - id: '3', - tester: { id: '2', username: 'tom-proudfeet' }, - testResultsLength: 3 - }, - { - id: '101', - tester: { id: '101', username: 'alflennik' }, - testResultsLength: 1 - }, - { - id: '2', - tester: { - id: '1', - username: 'esmeralda-baggins' - }, - testResultsLength: 3 - } - ] - } - ], - testPlans: [] - } - } - } -]; - -export const TEST_QUEUE_PAGE_POPULATED_MOCK_TESTER = [ - { - request: { - query: TEST_QUEUE_PAGE_QUERY - }, - result: { - data: { - me: { - id: '4', - username: 'bar-foo', - roles: ['TESTER'], - __typename: 'User' - }, - ats: [ - { - id: '1', - name: 'JAWS', - atVersions: [ - { - id: '6', - name: '2021.2103.174', - releasedAt: '2022-08-02T14:36:02.659Z' - } - ] - }, - { - id: '2', - name: 'NVDA', - atVersions: [ - { - id: '5', - name: '2020.4', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '4', - name: '2020.3', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '3', - name: '2020.2', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '2', - name: '2020.1', - releasedAt: '2022-01-01T12:00:00.000Z' - }, - { - id: '1', - name: '2019.3', - releasedAt: '2022-01-01T12:00:00.000Z' - } - ] - }, - { - id: '3', - name: 'VoiceOver for macOS', - atVersions: [ - { - id: '7', - name: '11.5.2', - releasedAt: '2022-01-01T12:00:00.000Z' - } - ] - } - ], - browsers: [ - { - id: '2', - name: 'Chrome' - }, - { - id: '1', - name: 'Firefox' - }, - { - id: '3', - name: 'Safari' - } - ], - users: [ - { - id: '1', - username: 'foo-bar', - roles: ['ADMIN', 'TESTER'] - }, - { - id: '4', - username: 'bar-foo', - roles: ['TESTER'] - }, - { - id: '5', - username: 'boo-far', - roles: ['TESTER'] - } - ], - testPlanVersions: [ - { - id: '1', - title: 'Alert Example', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'alert' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - }, - { - id: '2', - title: 'Banner Landmark', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'banner' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - }, - { - id: '3', - title: 'Breadcrumb Example', - gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', - gitMessage: - 'Create tests for APG design pattern example: Navigation Menu Button (#524)', - testPlan: { - directory: 'breadcrumb' - }, - updatedAt: '2022-04-15T19:09:53.000Z' - } - ], - testPlanReports: [ - { - id: '10', - status: 'DRAFT', - conflictsLength: 0, - runnableTestsLength: 17, - at: { - id: '2', - name: 'NVDA' - }, - browser: { - id: '1', - name: 'Firefox' - }, - testPlanVersion: { - id: '65', - title: 'Checkbox Example (Two State)', - gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', - gitMessage: 'The message for this SHA', - testPlan: { - directory: 'checkbox' - }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [ - { - id: '18', - tester: { - id: '1', - username: 'foo-bar' - }, - testResultsLength: 0 - }, - { - id: '19', - tester: { - id: '4', - username: 'bar-foo' - }, - testResultsLength: 0 - } - ] - }, - { - id: '11', - status: 'DRAFT', - conflictsLength: 0, - runnableTestsLength: 17, - at: { - id: '2', - name: 'JAWS' - }, - browser: { - id: '1', - name: 'Firefox' - }, - testPlanVersion: { - id: '65', - title: 'Checkbox Example (Two State)', - gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', - gitMessage: 'The message for this SHA', - testPlan: { - directory: 'checkbox' - }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [ - { - id: '20', - tester: { - id: '5', - username: 'boo-far' - }, - testResultsLength: 0 - } - ] - }, - { - id: '12', - status: 'DRAFT', - conflictsLength: 0, - runnableTestsLength: 15, - at: { - id: '3', - name: 'VoiceOver for macOS' - }, - browser: { - id: '1', - name: 'Firefox' - }, - testPlanVersion: { - id: '74', - title: 'Editor Menubar Example', - gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', - gitMessage: 'The message for this SHA', - testPlan: { - directory: 'menubar-editor' - }, - updatedAt: '2021-11-30T14:51:28.000Z' - }, - draftTestPlanRuns: [] - } - ], - testPlans: [] - } - } - } -]; - -export const TEST_MANAGEMENT_PAGE_POPULATED = [ - { - request: { - query: TEST_MANAGEMENT_PAGE_QUERY - }, - result: { - data: { - ats: [ - { - id: '1', - name: 'JAWS', - atVersions: [ - { - id: '1', - name: '2021.2111.13', - releasedAt: '2021-11-01T04:00:00.000Z' - } - ] - }, - { - id: '2', - name: 'NVDA', - atVersions: [ - { - id: '2', - name: '2020.4', - releasedAt: '2021-02-19T05:00:00.000Z' - } - ] - }, - { - id: '3', - name: 'VoiceOver for macOS', - atVersions: [ - { - id: '3', - name: '11.6 (20G165)', - releasedAt: '2019-09-01T04:00:00.000Z' - } - ] - } - ], - browsers: [ - { - id: '2', - name: 'Chrome' - }, - { - id: '1', - name: 'Firefox' - }, - { - id: '3', - name: 'Safari' - } - ], - testPlans: [ - { - directory: 'alert', - id: 'alert', - title: 'Alert Example', - latestTestPlanVersion: { - id: '1', - title: 'Alert Example' - } - }, - { - directory: 'banner', - id: 'banner', - title: 'Banner Landmark', - latestTestPlanVersion: { - id: '2', - title: 'Banner Landmark' - } - }, - { - directory: 'breadcrumb', - id: 'breadcrumb', - title: 'Breadcrumb Example', - latestTestPlanVersion: { - id: '3', - title: 'Breadcrumb Example' - } - }, - { - directory: 'checkbox', - id: 'checkbox', - title: 'Checkbox Example (Two State)', - latestTestPlanVersion: { - id: '4', - title: 'Checkbox Example (Two State)' - } - }, - { - directory: 'checkbox-tri-state', - id: 'checkbox-tri-state', - title: 'Checkbox Example (Mixed-State)', - latestTestPlanVersion: { - id: '5', - title: 'Checkbox Example (Mixed-State)' - } - }, - { - 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' - } - }, - { - directory: 'combobox-select-only', - id: 'combobox-select-only', - title: 'Select Only Combobox Example', - latestTestPlanVersion: { - id: '7', - title: 'Select Only Combobox Example' - } - }, - { - directory: 'command-button', - id: 'command-button', - title: 'Command Button Example', - latestTestPlanVersion: { - id: '8', - title: 'Command Button Example' - } - }, - { - directory: 'complementary', - id: 'complementary', - title: 'Complementary Landmark', - latestTestPlanVersion: { - id: '9', - title: 'Complementary Landmark' - } - }, - { - directory: 'contentinfo', - id: 'contentinfo', - title: 'Contentinfo Landmark', - latestTestPlanVersion: { - id: '10', - title: 'Contentinfo Landmark' - } - }, - { - directory: 'datepicker-spin-button', - id: 'datepicker-spin-button', - title: 'Date Picker Spin Button Example', - latestTestPlanVersion: { - id: '11', - title: 'Date Picker Spin Button Example' - } - }, - { - 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' - } - }, - { - directory: 'disclosure-navigation', - id: 'disclosure-navigation', - title: 'Disclosure Navigation Menu Example', - latestTestPlanVersion: { - id: '13', - title: 'Disclosure Navigation Menu Example' - } - }, - { - directory: 'form', - id: 'form', - title: 'Form Landmark', - latestTestPlanVersion: { - id: '14', - title: 'Form Landmark' - } - }, - { - directory: 'horizontal-slider', - id: 'horizontal-slider', - title: 'Color Viewer Slider', - latestTestPlanVersion: { - id: '15', - title: 'Color Viewer Slider' - } - }, - { - 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)' - } - }, - { - 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)' - } - }, - { - 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)' - } - }, - { - directory: 'main', - id: 'main', - title: 'Main Landmark', - latestTestPlanVersion: { - id: '19', - title: 'Main Landmark' - } - }, - { - 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()' - } - }, - { - 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' - } - }, - { - directory: 'menu-button-navigation', - id: 'menu-button-navigation', - title: 'Navigation Menu Button', - latestTestPlanVersion: { - id: '22', - title: 'Navigation Menu Button' - } - }, - { - directory: 'menubar-editor', - id: 'menubar-editor', - title: 'Editor Menubar Example', - latestTestPlanVersion: { - id: '23', - title: 'Editor Menubar Example' - } - }, - { - directory: 'meter', - id: 'meter', - title: 'Meter', - latestTestPlanVersion: { - id: '24', - title: 'Meter' - } - }, - { - 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' - } - }, - { - directory: 'modal-dialog', - id: 'modal-dialog', - title: 'Modal Dialog Example', - latestTestPlanVersion: { - id: '26', - title: 'Modal Dialog Example' - } - }, - { - 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' - } - }, - { - 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' - } - }, - { - directory: 'rating-slider', - id: 'rating-slider', - title: 'Rating Slider', - latestTestPlanVersion: { - id: '29', - title: 'Rating Slider' - } - }, - { - directory: 'seek-slider', - id: 'seek-slider', - title: 'Media Seek Slider', - latestTestPlanVersion: { - id: '30', - title: 'Media Seek Slider' - } - }, - { - directory: 'slider-multithumb', - id: 'slider-multithumb', - title: 'Horizontal Multi-Thumb Slider', - latestTestPlanVersion: { - id: '31', - title: 'Horizontal Multi-Thumb Slider' - } - }, - { - directory: 'switch', - id: 'switch', - title: 'Switch Example', - latestTestPlanVersion: { - id: '32', - title: 'Switch Example' - } - }, - { - directory: 'tabs-manual-activation', - id: 'tabs-manual-activation', - title: 'Tabs with Manual Activation', - latestTestPlanVersion: { - id: '33', - title: 'Tabs with Manual Activation' - } - }, - { - directory: 'toggle-button', - id: 'toggle-button', - title: 'Toggle Button', - latestTestPlanVersion: { - id: '34', - title: 'Toggle Button' - } - }, - { - directory: 'vertical-temperature-slider', - id: 'vertical-temperature-slider', - title: 'Vertical Temperature Slider', - latestTestPlanVersion: { - id: '35', - title: 'Vertical Temperature Slider' - } - } - ], - testPlanVersions: [ - { - id: '21', - title: 'Action Menu Button Example Using aria-activedescendant', - 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: 'menu-button-actions-active-descendant' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '20', - title: 'Action Menu Button Example Using element.focus()', - 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: 'menu-button-actions' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '1', - title: 'Alert 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: 'alert' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '2', - title: 'Banner Landmark', - 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: 'banner' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '3', - title: 'Breadcrumb 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: 'breadcrumb' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - 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' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '4', - title: 'Checkbox Example (Two 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' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '15', - title: 'Color Viewer Slider', - 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: 'horizontal-slider' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '6', - title: 'Combobox with Both List and Inline Autocomplete 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-autocomplete-both-updated' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '8', - title: 'Command Button 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: 'command-button' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '9', - title: 'Complementary Landmark', - 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: 'complementary' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '10', - title: 'Contentinfo Landmark', - 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: 'contentinfo' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '25', - title: 'Data Grid Example 1: Minimal Data Grid', - 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: 'minimal-data-grid' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '11', - title: 'Date Picker Spin Button 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: 'datepicker-spin-button' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '13', - title: 'Disclosure Navigation Menu 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: 'disclosure-navigation' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '12', - title: 'Disclosure of Answers to Frequently Asked Questions 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: 'disclosure-faq' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - 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)', - testPlan: { - directory: 'menubar-editor' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '14', - title: 'Form Landmark', - 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: 'form' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '31', - title: 'Horizontal Multi-Thumb Slider', - 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: 'slider-multithumb' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '18', - title: 'Link Example 1 (span element with text content)', - 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: 'link-span-text' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '17', - title: 'Link Example 2 (img element with alt attribute)', - 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: 'link-img-alt' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '16', - title: 'Link Example 3 (CSS :before content property on a span element)', - 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: 'link-css' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '19', - title: 'Main Landmark', - 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: 'main' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '30', - title: 'Media Seek Slider', - 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: 'seek-slider' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '24', - title: 'Meter', - 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: 'meter' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - 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' - }, - { - id: '22', - title: 'Navigation Menu 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: 'menu-button-navigation' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '28', - title: 'Radio Group Example Using Roving tabindex', - 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: 'radiogroup-roving-tabindex' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '27', - title: 'Radio Group Example Using aria-activedescendant', - 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: 'radiogroup-aria-activedescendant' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - }, - { - id: '29', - title: 'Rating Slider', - 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: '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' - }, - { - id: '33', - title: 'Tabs with Manual Activation', - 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: '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' - }, - { - id: '35', - title: 'Vertical Temperature Slider', - 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: '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' - } - }, - { - 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' - }, - 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' - } - }, - { - 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' - }, - 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' - } - }, - { - 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' - }, - 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' - }, - 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' - }, - updatedAt: '2023-04-10T18:22:22.000Z' - } - } - ] - } - } - } -]; diff --git a/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js new file mode 100644 index 000000000..1715e7526 --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js @@ -0,0 +1,4052 @@ +export default ( + meQuery, + dataManagementPageQuery, + testPlanReportStatusDialogQuery +) => [ + { + request: { + query: meQuery + }, + result: { + data: { + me: { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + } + } + } + }, + { + request: { + query: dataManagementPageQuery + }, + result: { + data: { + me: { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + }, + ats: [ + { + id: '1', + name: 'JAWS', + atVersions: [ + { + id: '1', + name: '2021.2111.13', + releasedAt: '2021-11-01T04:00:00.000Z' + } + ] + }, + { + id: '2', + name: 'NVDA', + atVersions: [ + { + id: '2', + name: '2020.4', + releasedAt: '2021-02-19T05:00:00.000Z' + } + ] + }, + { + id: '3', + name: 'VoiceOver for macOS', + atVersions: [ + { + id: '3', + name: '11.6 (20G165)', + releasedAt: '2019-09-01T04:00:00.000Z' + } + ] + } + ], + browsers: [ + { + id: '2', + name: 'Chrome' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '3', + name: 'Safari' + } + ], + testPlans: [ + { + id: '27', + directory: 'radiogroup-aria-activedescendant', + title: 'Radio Group Example Using aria-activedescendant' + }, + { + id: '28', + directory: 'radiogroup-roving-tabindex', + title: 'Radio Group Example Using Roving tabindex' + }, + { + id: '31', + directory: 'slider-multithumb', + title: 'Horizontal Multi-Thumb Slider' + }, + { + id: '16', + directory: 'link-css', + title: 'Link Example 3 (CSS :before content property on a span element)' + }, + { + id: '17', + directory: 'link-img-alt', + title: 'Link Example 2 (img element with alt attribute)' + }, + { + id: '1', + directory: 'alert', + title: 'Alert Example' + }, + { + id: '13', + directory: 'disclosure-navigation', + title: 'Disclosure Navigation Menu Example' + }, + { + id: '5', + directory: 'checkbox-tri-state', + title: 'Checkbox Example (Mixed-State)' + }, + { + id: '3', + directory: 'breadcrumb', + title: 'Breadcrumb Example' + }, + { + id: '19', + directory: 'main', + title: 'Main Landmark' + }, + { + id: '24', + directory: 'meter', + title: 'Meter' + }, + { + id: '32', + directory: 'switch', + title: 'Switch Example' + }, + { + id: '26', + directory: 'modal-dialog', + title: 'Modal Dialog Example' + }, + { + id: '22', + directory: 'menu-button-navigation', + title: 'Navigation Menu Button' + }, + { + id: '34', + directory: 'toggle-button', + title: 'Toggle Button' + }, + { + id: '18', + directory: 'link-span-text', + title: 'Link Example 1 (span element with text content)' + }, + { + id: '8', + directory: 'command-button', + title: 'Command Button Example' + }, + { + id: '15', + directory: 'horizontal-slider', + title: 'Color Viewer Slider' + }, + { + id: '6', + directory: 'combobox-autocomplete-both-updated', + title: 'Combobox with Both List and Inline Autocomplete Example' + }, + { + id: '7', + directory: 'combobox-select-only', + title: 'Select Only Combobox Example' + }, + { + id: '4', + directory: 'checkbox', + title: 'Checkbox Example (Two State)' + }, + { + id: '9', + directory: 'complementary', + title: 'Complementary Landmark' + }, + { + id: '10', + directory: 'contentinfo', + title: 'Contentinfo Landmark' + }, + { + id: '11', + directory: 'datepicker-spin-button', + title: 'Date Picker Spin Button Example' + }, + { + id: '12', + directory: 'disclosure-faq', + title: 'Disclosure of Answers to Frequently Asked Questions Example' + }, + { + id: '14', + directory: 'form', + title: 'Form Landmark' + }, + { + id: '20', + directory: 'menu-button-actions', + title: 'Action Menu Button Example Using element.focus()' + }, + { + id: '21', + directory: 'menu-button-actions-active-descendant', + title: 'Action Menu Button Example Using aria-activedescendant' + }, + { + id: '23', + directory: 'menubar-editor', + title: 'Editor Menubar Example' + }, + { + id: '25', + directory: 'minimal-data-grid', + title: 'Data Grid Example 1: Minimal Data Grid' + }, + { + id: '29', + directory: 'rating-slider', + title: 'Rating Slider' + }, + { + id: '30', + directory: 'seek-slider', + title: 'Media Seek Slider' + }, + { + id: '33', + directory: 'tabs-manual-activation', + title: 'Tabs with Manual Activation' + }, + { + id: '35', + directory: 'vertical-temperature-slider', + title: 'Vertical Temperature Slider' + }, + { + id: '2', + directory: 'banner', + title: 'Banner Landmark' + } + ], + testPlanVersions: [ + { + 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: 'radiogroup-roving-tabindex' + }, + testPlanReports: [] + }, + { + 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: 'radiogroup-aria-activedescendant' + }, + testPlanReports: [] + }, + { + id: '31', + title: 'Horizontal Multi-Thumb Slider', + phase: 'RD', + gitSha: 'b5fe3efd569518e449ef9a0978b0dec1f2a08bd6', + gitMessage: + '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: 'slider-multithumb' + }, + testPlanReports: [] + }, + { + id: '16', + title: 'Link Example 3 (CSS :before content property on a span element)', + phase: 'RD', + gitSha: '7a8454bca6de980199868101431817cea03cce35', + gitMessage: + '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: 'link-css' + }, + testPlanReports: [] + }, + { + id: '17', + title: 'Link Example 2 (img element with alt attribute)', + phase: 'RD', + gitSha: 'dc637636cff74b51f5c468ff3b81bd1f38aefbb2', + gitMessage: + '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: 'link-img-alt' + }, + testPlanReports: [] + }, + { + id: '1', + title: 'Alert Example', + phase: 'DRAFT', + gitSha: '0928bcf530efcf4faa677285439701537674e014', + gitMessage: + '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: 'alert' + }, + testPlanReports: [ + { + id: '7', + metrics: {}, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [] + } + ] + }, + { + id: '13', + title: 'Disclosure Navigation Menu Example', + phase: 'RD', + gitSha: '179ba0f438aaa5781b3ec8a4033d6bf9f757360b', + gitMessage: + '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: 'disclosure-navigation' + }, + testPlanReports: [] + }, + { + id: '3', + title: 'Breadcrumb Example', + phase: 'RD', + gitSha: '1aa3b74d24d340362e9f511eae33788d55487d12', + gitMessage: + '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: 'breadcrumb' + }, + testPlanReports: [] + }, + { + id: '19', + title: 'Main Landmark', + phase: 'RD', + gitSha: 'c87a66ea13a2b6fac6d79fe1fb0b7a2f721dcd22', + gitMessage: + '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: 'main' + }, + testPlanReports: [] + }, + { + id: '24', + title: 'Meter', + phase: 'RD', + gitSha: '32d2d9db48becfc008fc566b569ac1563576ceb9', + gitMessage: + '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: 'meter' + }, + testPlanReports: [] + }, + { + id: '32', + title: 'Switch Example', + phase: 'RD', + gitSha: '9d0e4e3d1040d64d9db69647e615c4ec0be723c2', + gitMessage: + '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: 'switch' + }, + testPlanReports: [] + }, + { + id: '22', + title: 'Navigation Menu Button', + phase: 'RD', + gitSha: 'ecf05f484292189789f4db8b1ec41b19db38e567', + gitMessage: + '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: 'menu-button-navigation' + }, + testPlanReports: [] + }, + { + id: '34', + title: 'Toggle Button', + phase: 'DRAFT', + gitSha: '022340081280b8cafb8ae0716a5b67e9ab942ef4', + gitMessage: + '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: 'toggle-button' + }, + testPlanReports: [ + { + id: '1', + metrics: { + testsCount: 16, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 93, + testsFailedCount: 14, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '28 of 30 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 30, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 28 + }, + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '1' + }, + testResults: [ + { + test: { + id: 'OWY5NeyIyIjoiMzQifQTRmOD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.154Z' + }, + { + test: { + id: 'NGFjMeyIyIjoiMzQifQjQxY2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'NTAwOeyIyIjoiMzQifQWI5YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'YThjMeyIyIjoiMzQifQzIyYT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'YTgxMeyIyIjoiMzQifQzExOW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.381Z' + }, + { + test: { + id: 'NGMwNeyIyIjoiMzQifQ2IwN2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.464Z' + }, + { + test: { + id: 'YzQxNeyIyIjoiMzQifQjY5ND' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.537Z' + }, + { + test: { + id: 'MjgwNeyIyIjoiMzQifQzk3YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.610Z' + } + ] + } + ] + } + ] + }, + { + id: '8', + title: 'Command Button Example', + phase: 'RD', + gitSha: '0c466eec96c8cafc9961232c85e14758c4589525', + gitMessage: + '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: 'command-button' + }, + testPlanReports: [] + }, + { + id: '18', + title: 'Link Example 1 (span element with text content)', + phase: 'RD', + gitSha: '0c466eec96c8cafc9961232c85e14758c4589525', + gitMessage: + '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: 'link-span-text' + }, + testPlanReports: [] + }, + { + id: '15', + title: 'Color Viewer Slider', + phase: 'RD', + gitSha: '1c6ef2fbef5fc056c622c802bebedaa14f2c8d40', + gitMessage: + '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: 'horizontal-slider' + }, + testPlanReports: [] + }, + { + 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: 'combobox-autocomplete-both-updated' + }, + testPlanReports: [] + }, + { + id: '21', + title: 'Action Menu Button Example Using aria-activedescendant', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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-actions-active-descendant' + }, + testPlanReports: [] + }, + { + id: '20', + title: 'Action Menu Button Example Using element.focus()', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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-actions' + }, + testPlanReports: [] + }, + { + id: '2', + title: 'Banner Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'banner' + }, + testPlanReports: [] + }, + { + id: '4', + title: 'Checkbox Example (Two State)', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'checkbox' + }, + testPlanReports: [] + }, + { + id: '9', + title: 'Complementary Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'complementary' + }, + testPlanReports: [] + }, + { + id: '10', + title: 'Contentinfo Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'contentinfo' + }, + testPlanReports: [] + }, + { + id: '25', + title: 'Data Grid Example 1: Minimal Data Grid', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'minimal-data-grid' + }, + testPlanReports: [] + }, + { + id: '11', + title: 'Date Picker Spin Button Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'datepicker-spin-button' + }, + testPlanReports: [] + }, + { + id: '12', + title: 'Disclosure of Answers to Frequently Asked Questions Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'disclosure-faq' + }, + testPlanReports: [] + }, + { + id: '23', + title: 'Editor Menubar Example', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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: 'menubar-editor' + }, + testPlanReports: [] + }, + { + id: '14', + title: 'Form Landmark', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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' + }, + testPlanReports: [] + }, + { + id: '30', + title: 'Media Seek Slider', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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' + }, + testPlanReports: [] + }, + { + id: '29', + title: 'Rating Slider', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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' + }, + testPlanReports: [] + }, + { + id: '7', + title: 'Select Only Combobox Example', + phase: 'DRAFT', + 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: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'combobox-select-only' + }, + testPlanReports: [ + { + id: '2', + metrics: { + testsCount: 21, + supportLevel: 'FAILING', + conflictsCount: 5, + supportPercent: 96, + testsFailedCount: 16, + testsPassedCount: 5, + optionalFormatted: '3 of 3 passed', + requiredFormatted: '48 of 50 passed', + optionalAssertionsCount: 3, + requiredAssertionsCount: 50, + unexpectedBehaviorCount: 3, + unexpectedBehaviorsFormatted: '3 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 3, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 48 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.240Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.332Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.412Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.501Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.593Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + }, + { + test: { + id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.811Z' + }, + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.902Z' + }, + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.996Z' + } + ] + }, + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.718Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.813Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.914Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.988Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.074Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + } + ] + } + ] + } + ] + }, + { + id: '33', + title: 'Tabs with Manual Activation', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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' + }, + testPlanReports: [] + }, + { + id: '35', + title: 'Vertical Temperature Slider', + phase: 'RD', + gitSha: '7c4b5dce23c74fcf280ed164bdb903e02e0e7726', + gitMessage: + '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' + }, + testPlanReports: [] + }, + { + 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' + }, + testPlanReports: [ + { + id: '6', + metrics: { + testsCount: 7, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 96, + testsFailedCount: 3, + testsPassedCount: 4, + optionalFormatted: '4 of 4 passed', + requiredFormatted: '44 of 46 passed', + optionalAssertionsCount: 4, + requiredAssertionsCount: 46, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 4, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 44 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '6' + }, + testResults: [ + { + test: { + id: 'YTE3NeyIyIjoiNSJ9WJlMj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.341Z' + }, + { + test: { + id: 'YWJiOeyIyIjoiNSJ9GQ5Zm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.405Z' + }, + { + test: { + id: 'ZGFlYeyIyIjoiNSJ9TJlMW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.474Z' + }, + { + test: { + id: 'YjI2MeyIyIjoiNSJ9WE1OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.537Z' + }, + { + test: { + id: 'ZjAwZeyIyIjoiNSJ9TZmZj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.605Z' + }, + { + test: { + id: 'MGRjZeyIyIjoiNSJ9WNiZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.670Z' + }, + { + test: { + id: 'OTZmYeyIyIjoiNSJ9TU5Ym' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.739Z' + } + ] + } + ] + }, + { + id: '12', + metrics: { + testsCount: 14, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 12, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '25 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 25 + }, + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '12' + }, + testResults: [ + { + test: { + id: 'MTVlZeyIyIjoiNSJ9DUzMz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.764Z' + }, + { + test: { + id: 'OThhMeyIyIjoiNSJ9WMxM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.828Z' + }, + { + test: { + id: 'YWNhNeyIyIjoiNSJ9TliN2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.892Z' + } + ] + } + ] + }, + { + id: '13', + metrics: { + testsCount: 14, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 12, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '25 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 25 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '13' + }, + testResults: [ + { + test: { + id: 'MTVlZeyIyIjoiNSJ9DUzMz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.955Z' + }, + { + test: { + id: 'OThhMeyIyIjoiNSJ9WMxM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:12.017Z' + }, + { + test: { + id: 'YWNhNeyIyIjoiNSJ9TliN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:12.083Z' + } + ] + } + ] + } + ] + }, + { + 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: '2022-07-06T00:00:00.000Z', + candidatePhaseReachedAt: '2022-07-06T00:00:00.000Z', + recommendedPhaseTargetDate: '2023-01-02T00:00:00.000Z', + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'modal-dialog' + }, + testPlanReports: [ + { + id: '10', + metrics: { + testsCount: 11, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 9, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 14 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 14, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '10' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.295Z' + }, + { + test: { + id: 'N2FkZeyIyIjoiMjYifQDQ5NT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.369Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.450Z' + } + ] + } + ] + }, + { + id: '9', + metrics: { + testsCount: 18, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '16 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 16 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '9' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.059Z' + }, + { + test: { + id: 'ODY5MeyIyIjoiMjYifQzhmNW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.137Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.218Z' + } + ] + } + ] + }, + { + id: '3', + metrics: { + testsCount: 18, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 88, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '3' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.074Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.134Z' + }, + { + test: { + id: 'NWM4NeyIyIjoiMjYifQDEwM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.202Z' + }, + { + test: { + id: 'NGFiZeyIyIjoiMjYifQWZiYW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.268Z' + }, + { + test: { + id: 'MzQzYeyIyIjoiMjYifQzU5Zm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.336Z' + } + ] + } + ] + }, + { + id: '11', + metrics: { + testsCount: 11, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 9, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 14 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 14, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '11' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.532Z' + }, + { + test: { + id: 'N2FkZeyIyIjoiMjYifQDQ5NT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.611Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.696Z' + } + ] + } + ] + }, + { + id: '4', + metrics: { + testsCount: 18, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 91, + testsFailedCount: 14, + testsPassedCount: 4, + optionalFormatted: false, + requiredFormatted: '20 of 22 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 22, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 20 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '4' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.409Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.478Z' + }, + { + test: { + id: 'NWM4NeyIyIjoiMjYifQDEwM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.551Z' + }, + { + test: { + id: 'NGFiZeyIyIjoiMjYifQWZiYW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.629Z' + }, + { + test: { + id: 'MzQzYeyIyIjoiMjYifQzU5Zm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.704Z' + }, + { + test: { + id: 'MmI1MeyIyIjoiMjYifQmU3Yz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.777Z' + }, + { + test: { + id: 'YmRmYeyIyIjoiMjYifQjEyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.852Z' + } + ] + } + ] + }, + { + id: '5', + metrics: { + testsCount: 11, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 92, + testsFailedCount: 8, + testsPassedCount: 3, + optionalFormatted: false, + requiredFormatted: '23 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 23 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '5' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:09.923Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:09.991Z' + }, + { + test: { + id: 'ZmQyNeyIyIjoiMjYifQ2M2ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.059Z' + }, + { + test: { + id: 'OGE3YeyIyIjoiMjYifQjU1ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.129Z' + }, + { + test: { + id: 'YWI3OeyIyIjoiMjYifQWJlNW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.198Z' + }, + { + test: { + id: 'M2RiOeyIyIjoiMjYifQGY1Nj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.272Z' + } + ] + } + ] + }, + { + id: '8', + metrics: { + testsCount: 18, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '16 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 16 + }, + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '8' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.817Z' + }, + { + test: { + id: 'ODY5MeyIyIjoiMjYifQzhmNW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.894Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.979Z' + } + ] + } + ] + } + ] + } + ] + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '1' } + }, + result: { + data: { + testPlanVersion: { + id: '1', + title: 'Alert Example', + phase: 'DRAFT', + gitSha: '0928bcf530efcf4faa677285439701537674e014', + gitMessage: + '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: 'alert' + }, + testPlanReports: [ + { + id: '7', + metrics: {}, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [] + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '5' } + }, + result: { + data: { + testPlanVersion: { + id: '5', + title: 'Checkbox Example (Mixed-State)', + phase: 'RECOMMENDED', + gitSha: '836fb2a997f5b2844035b8c934f8fda9833cd5b2', + gitMessage: 'Validation for test csv formats (#980)', + updatedAt: '2023-08-23T20:30:34.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' + }, + testPlanReports: [ + { + id: '6', + metrics: { + testsCount: 7, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 96, + testsFailedCount: 3, + testsPassedCount: 4, + optionalFormatted: '4 of 4 passed', + requiredFormatted: '44 of 46 passed', + optionalAssertionsCount: 4, + requiredAssertionsCount: 46, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 4, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 44 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '6' + }, + testResults: [ + { + test: { + id: 'YTE3NeyIyIjoiNSJ9WJlMj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.070Z' + }, + { + test: { + id: 'YWJiOeyIyIjoiNSJ9GQ5Zm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.142Z' + }, + { + test: { + id: 'ZGFlYeyIyIjoiNSJ9TJlMW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.204Z' + }, + { + test: { + id: 'YjI2MeyIyIjoiNSJ9WE1OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.275Z' + }, + { + test: { + id: 'ZjAwZeyIyIjoiNSJ9TZmZj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.330Z' + }, + { + test: { + id: 'MGRjZeyIyIjoiNSJ9WNiZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.382Z' + }, + { + test: { + id: 'OTZmYeyIyIjoiNSJ9TU5Ym' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-30T13:23:57.439Z' + } + ] + } + ] + }, + { + id: '12', + metrics: { + testsCount: 14, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 12, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '25 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 25 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '12' + }, + testResults: [ + { + test: { + id: 'MTVlZeyIyIjoiNSJ9DUzMz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.343Z' + }, + { + test: { + id: 'OThhMeyIyIjoiNSJ9WMxM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.404Z' + }, + { + test: { + id: 'YWNhNeyIyIjoiNSJ9TliN2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.472Z' + } + ] + } + ] + }, + { + id: '13', + metrics: { + testsCount: 14, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 12, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '25 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 25 + }, + markedFinalAt: '2022-07-07T00:00:00.000Z', + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '13' + }, + testResults: [ + { + test: { + id: 'MTVlZeyIyIjoiNSJ9DUzMz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.531Z' + }, + { + test: { + id: 'OThhMeyIyIjoiNSJ9WMxM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.593Z' + }, + { + test: { + id: 'YWNhNeyIyIjoiNSJ9TliN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-30T13:23:58.655Z' + } + ] + } + ] + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '7' } + }, + result: { + data: { + testPlanVersion: { + id: '7', + title: 'Select Only Combobox Example', + phase: 'DRAFT', + 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: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'combobox-select-only' + }, + testPlanReports: [ + { + id: '2', + metrics: { + testsCount: 21, + supportLevel: 'FAILING', + conflictsCount: 5, + supportPercent: 96, + testsFailedCount: 16, + testsPassedCount: 5, + optionalFormatted: '3 of 3 passed', + requiredFormatted: '48 of 50 passed', + optionalAssertionsCount: 3, + requiredAssertionsCount: 50, + unexpectedBehaviorCount: 3, + unexpectedBehaviorsFormatted: '3 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 3, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 48 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.240Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.332Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.412Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.501Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.593Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + }, + { + test: { + id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.811Z' + }, + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.902Z' + }, + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.996Z' + } + ] + }, + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.718Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.813Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.914Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:07.988Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:08.074Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + } + ] + } + ] + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '26' } + }, + result: { + data: { + testPlanVersion: { + 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: '2022-07-06T00:00:00.000Z', + candidatePhaseReachedAt: '2022-07-06T00:00:00.000Z', + recommendedPhaseTargetDate: '2023-01-02T00:00:00.000Z', + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'modal-dialog' + }, + testPlanReports: [ + { + id: '10', + metrics: { + testsCount: 11, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 9, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 14 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 14, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '10' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.295Z' + }, + { + test: { + id: 'N2FkZeyIyIjoiMjYifQDQ5NT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.369Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.450Z' + } + ] + } + ] + }, + { + id: '9', + metrics: { + testsCount: 18, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '16 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 16 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '9' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.059Z' + }, + { + test: { + id: 'ODY5MeyIyIjoiMjYifQzhmNW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.137Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:11.218Z' + } + ] + } + ] + }, + { + id: '3', + metrics: { + testsCount: 18, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 88, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '3' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.074Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.134Z' + }, + { + test: { + id: 'NWM4NeyIyIjoiMjYifQDEwM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.202Z' + }, + { + test: { + id: 'NGFiZeyIyIjoiMjYifQWZiYW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.268Z' + }, + { + test: { + id: 'MzQzYeyIyIjoiMjYifQzU5Zm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.336Z' + } + ] + } + ] + }, + { + id: '11', + metrics: { + testsCount: 11, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 9, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '14 of 14 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 14, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 14 + }, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '11' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.532Z' + }, + { + test: { + id: 'N2FkZeyIyIjoiMjYifQDQ5NT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.611Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:11.696Z' + } + ] + } + ] + }, + { + id: '4', + metrics: { + testsCount: 18, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 91, + testsFailedCount: 14, + testsPassedCount: 4, + optionalFormatted: false, + requiredFormatted: '20 of 22 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 22, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 20 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '4' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.409Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.478Z' + }, + { + test: { + id: 'NWM4NeyIyIjoiMjYifQDEwM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.551Z' + }, + { + test: { + id: 'NGFiZeyIyIjoiMjYifQWZiYW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.629Z' + }, + { + test: { + id: 'MzQzYeyIyIjoiMjYifQzU5Zm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.704Z' + }, + { + test: { + id: 'MmI1MeyIyIjoiMjYifQmU3Yz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.777Z' + }, + { + test: { + id: 'YmRmYeyIyIjoiMjYifQjEyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:09.852Z' + } + ] + } + ] + }, + { + id: '5', + metrics: { + testsCount: 11, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 92, + testsFailedCount: 8, + testsPassedCount: 3, + optionalFormatted: false, + requiredFormatted: '23 of 25 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 25, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 23 + }, + markedFinalAt: '2022-07-06T00:00:00.000Z', + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + name: 'Safari' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '5' + }, + testResults: [ + { + test: { + id: 'MzlmYeyIyIjoiMjYifQzIxY2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:09.923Z' + }, + { + test: { + id: 'ZDJkYeyIyIjoiMjYifQzRkYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:09.991Z' + }, + { + test: { + id: 'ZmQyNeyIyIjoiMjYifQ2M2ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.059Z' + }, + { + test: { + id: 'OGE3YeyIyIjoiMjYifQjU1ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.129Z' + }, + { + test: { + id: 'YWI3OeyIyIjoiMjYifQWJlNW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.198Z' + }, + { + test: { + id: 'M2RiOeyIyIjoiMjYifQGY1Nj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2023-08-18T03:17:10.272Z' + } + ] + } + ] + }, + { + id: '8', + metrics: { + testsCount: 18, + supportLevel: 'FULL', + conflictsCount: 0, + supportPercent: 100, + testsFailedCount: 16, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '16 of 16 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 16, + unexpectedBehaviorCount: 0, + unexpectedBehaviorsFormatted: false, + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 0, + requiredAssertionsPassedCount: 16 + }, + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '8' + }, + testResults: [ + { + test: { + id: 'MThhNeyIyIjoiMjYifQmEyMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.817Z' + }, + { + test: { + id: 'ODY5MeyIyIjoiMjYifQzhmNW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.894Z' + }, + { + test: { + id: 'NWVkNeyIyIjoiMjYifQTZkOT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2023-08-18T03:17:10.979Z' + } + ] + } + ] + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '34' } + }, + result: { + data: { + testPlanVersion: { + id: '34', + title: 'Toggle Button', + phase: 'DRAFT', + gitSha: '022340081280b8cafb8ae0716a5b67e9ab942ef4', + gitMessage: + '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: 'toggle-button' + }, + testPlanReports: [ + { + id: '1', + metrics: { + testsCount: 16, + supportLevel: 'FAILING', + conflictsCount: 0, + supportPercent: 93, + testsFailedCount: 14, + testsPassedCount: 2, + optionalFormatted: false, + requiredFormatted: '28 of 30 passed', + optionalAssertionsCount: 0, + requiredAssertionsCount: 30, + unexpectedBehaviorCount: 1, + unexpectedBehaviorsFormatted: '1 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 0, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 28 + }, + markedFinalAt: null, + at: { + id: '1', + name: 'JAWS' + }, + browser: { + id: '2', + name: 'Chrome' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '1' + }, + testResults: [ + { + test: { + id: 'OWY5NeyIyIjoiMzQifQTRmOD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.154Z' + }, + { + test: { + id: 'NGFjMeyIyIjoiMzQifQjQxY2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'NTAwOeyIyIjoiMzQifQWI5YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'YThjMeyIyIjoiMzQifQzIyYT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'YTgxMeyIyIjoiMzQifQzExOW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.381Z' + }, + { + test: { + id: 'NGMwNeyIyIjoiMzQifQ2IwN2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.464Z' + }, + { + test: { + id: 'YzQxNeyIyIjoiMzQifQjY5ND' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.537Z' + }, + { + test: { + id: 'MjgwNeyIyIjoiMzQifQzk3YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2023-08-18T03:17:07.610Z' + } + ] + } + ] + } + ] + } + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js b/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js new file mode 100644 index 000000000..8d0898e36 --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js @@ -0,0 +1,308 @@ +export const mockedTestPlanVersion = { + id: '7', + title: 'Select Only Combobox Example', + phase: 'DRAFT', + 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: null, + recommendedPhaseTargetDate: null, + recommendedPhaseReachedAt: null, + testPlan: { + directory: 'combobox-select-only' + }, + testPlanReports: [ + { + id: '2', + metrics: { + testsCount: 21, + supportLevel: 'FAILING', + conflictsCount: 5, + supportPercent: 96, + testsFailedCount: 16, + testsPassedCount: 5, + optionalFormatted: '3 of 3 passed', + requiredFormatted: '48 of 50 passed', + optionalAssertionsCount: 3, + requiredAssertionsCount: 50, + unexpectedBehaviorCount: 3, + unexpectedBehaviorsFormatted: '3 found', + optionalAssertionsFailedCount: 0, + optionalAssertionsPassedCount: 3, + requiredAssertionsFailedCount: 2, + requiredAssertionsPassedCount: 48 + }, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.240Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.332Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.412Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.501Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.593Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + }, + { + test: { + id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.811Z' + }, + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.902Z' + }, + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.996Z' + } + ] + }, + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '2' + }, + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:07.718Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:07.813Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:07.914Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:07.988Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2023-08-18T03:17:08.074Z' + }, + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null + } + ] + } + ] + } + ] +}; + +export default (meQuery, testPlanReportStatusDialogQuery) => [ + { + request: { + query: meQuery + }, + result: { + data: { + me: { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '7' } + }, + result: { + data: { + testPlanVersion: mockedTestPlanVersion + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js new file mode 100644 index 000000000..f5243b0df --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js @@ -0,0 +1,39 @@ +export default testQueuePageQuery => [ + { + request: { + query: testQueuePageQuery + }, + result: { + data: { + me: { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'], + __typename: 'User' + }, + ats: [], + browsers: [], + users: [ + { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + }, + { + id: '4', + username: 'bar-foo', + roles: ['TESTER'] + }, + { + id: '5', + username: 'boo-far', + roles: ['TESTER'] + } + ], + testPlanVersions: [], + testPlanReports: [], + testPlans: [] + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js new file mode 100644 index 000000000..2cfcee9b1 --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js @@ -0,0 +1,233 @@ +export default testQueuePageQuery => [ + { + request: { + query: testQueuePageQuery + }, + result: { + data: { + me: { + id: '101', + username: 'alflennik', + roles: ['ADMIN', 'TESTER'] + }, + ats: [ + { + id: '1', + name: 'JAWS', + atVersions: [ + { + id: '6', + name: '2021.2103.174', + releasedAt: '2022-08-02T14:36:02.659Z' + } + ] + }, + { + id: '2', + name: 'NVDA', + atVersions: [ + { + id: '5', + name: '2020.4', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '4', + name: '2020.3', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '3', + name: '2020.2', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '2', + name: '2020.1', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '1', + name: '2019.3', + releasedAt: '2022-01-01T12:00:00.000Z' + } + ] + }, + { + id: '3', + name: 'VoiceOver for macOS', + atVersions: [ + { + id: '7', + name: '11.5.2', + releasedAt: '2022-01-01T12:00:00.000Z' + } + ] + } + ], + browsers: [ + { + id: '2', + name: 'Chrome' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '3', + name: 'Safari' + } + ], + users: [ + { + id: '1', + username: 'esmeralda-baggins', + roles: ['TESTER', 'ADMIN'] + }, + { id: '2', username: 'tom-proudfeet', roles: ['TESTER'] }, + { + id: '101', + username: 'alflennik', + roles: ['TESTER', 'ADMIN'] + } + ], + testPlanVersions: [ + { + id: '1', + title: 'Alert Example', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'alert' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + }, + { + id: '2', + title: 'Banner Landmark', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'banner' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + }, + { + id: '3', + title: 'Breadcrumb Example', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'breadcrumb' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + } + ], + testPlanReports: [ + { + id: '1', + 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' }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [ + { + id: '1', + tester: { + id: '1', + username: 'esmeralda-baggins' + }, + testResultsLength: 0 + } + ] + }, + { + id: '2', + 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' }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [ + { + id: '1', + tester: { + id: '1', + username: 'esmeralda-baggins' + }, + testResultsLength: 0 + } + ] + }, + { + id: '3', + 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' }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [ + { + id: '3', + tester: { id: '2', username: 'tom-proudfeet' }, + testResultsLength: 3 + }, + { + id: '101', + tester: { id: '101', username: 'alflennik' }, + testResultsLength: 1 + }, + { + id: '2', + tester: { + id: '1', + username: 'esmeralda-baggins' + }, + testResultsLength: 3 + } + ] + } + ], + testPlans: [] + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js new file mode 100644 index 000000000..fe03d4e0e --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js @@ -0,0 +1,42 @@ +export default testQueuePageQuery => [ + { + request: { + query: testQueuePageQuery + }, + result: { + data: { + me: { + id: '4', + username: 'bar-foo', + roles: ['TESTER'], + __typename: 'User' + }, + ats: [], + browsers: [], + users: [ + { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'], + __typename: 'User' + }, + { + id: '4', + username: 'bar-foo', + roles: ['TESTER'], + __typename: 'User' + }, + { + id: '5', + username: 'boo-far', + roles: ['TESTER'], + __typename: 'User' + } + ], + testPlanVersions: [], + testPlanReports: [], + testPlans: [] + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js new file mode 100644 index 000000000..95949a0bc --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js @@ -0,0 +1,251 @@ +export default testQueuePageQuery => [ + { + request: { + query: testQueuePageQuery + }, + result: { + data: { + me: { + id: '4', + username: 'bar-foo', + roles: ['TESTER'], + __typename: 'User' + }, + ats: [ + { + id: '1', + name: 'JAWS', + atVersions: [ + { + id: '6', + name: '2021.2103.174', + releasedAt: '2022-08-02T14:36:02.659Z' + } + ] + }, + { + id: '2', + name: 'NVDA', + atVersions: [ + { + id: '5', + name: '2020.4', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '4', + name: '2020.3', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '3', + name: '2020.2', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '2', + name: '2020.1', + releasedAt: '2022-01-01T12:00:00.000Z' + }, + { + id: '1', + name: '2019.3', + releasedAt: '2022-01-01T12:00:00.000Z' + } + ] + }, + { + id: '3', + name: 'VoiceOver for macOS', + atVersions: [ + { + id: '7', + name: '11.5.2', + releasedAt: '2022-01-01T12:00:00.000Z' + } + ] + } + ], + browsers: [ + { + id: '2', + name: 'Chrome' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '3', + name: 'Safari' + } + ], + users: [ + { + id: '1', + username: 'foo-bar', + roles: ['ADMIN', 'TESTER'] + }, + { + id: '4', + username: 'bar-foo', + roles: ['TESTER'] + }, + { + id: '5', + username: 'boo-far', + roles: ['TESTER'] + } + ], + testPlanVersions: [ + { + id: '1', + title: 'Alert Example', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'alert' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + }, + { + id: '2', + title: 'Banner Landmark', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'banner' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + }, + { + id: '3', + title: 'Breadcrumb Example', + phase: 'DRAFT', + gitSha: '97d4bd6c2078849ad4ee01eeeb3667767ca6f992', + gitMessage: + 'Create tests for APG design pattern example: Navigation Menu Button (#524)', + testPlan: { + directory: 'breadcrumb' + }, + updatedAt: '2022-04-15T19:09:53.000Z' + } + ], + testPlanReports: [ + { + id: '10', + status: 'DRAFT', + conflictsLength: 0, + runnableTestsLength: 17, + markedFinalAt: null, + at: { + id: '2', + name: 'NVDA' + }, + browser: { + id: '1', + name: 'Firefox' + }, + testPlanVersion: { + id: '65', + title: 'Checkbox Example (Two State)', + phase: 'DRAFT', + gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', + gitMessage: 'The message for this SHA', + testPlan: { + directory: 'checkbox' + }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [ + { + id: '18', + tester: { + id: '1', + username: 'foo-bar' + }, + testResultsLength: 0 + }, + { + id: '19', + tester: { + id: '4', + username: 'bar-foo' + }, + testResultsLength: 0 + } + ] + }, + { + id: '11', + status: 'DRAFT', + conflictsLength: 0, + runnableTestsLength: 17, + markedFinalAt: null, + at: { + id: '2', + name: 'JAWS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + testPlanVersion: { + id: '65', + title: 'Checkbox Example (Two State)', + phase: 'DRAFT', + gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', + gitMessage: 'The message for this SHA', + testPlan: { + directory: 'checkbox' + }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [ + { + id: '20', + tester: { + id: '5', + username: 'boo-far' + }, + testResultsLength: 0 + } + ] + }, + { + id: '12', + status: 'DRAFT', + conflictsLength: 0, + runnableTestsLength: 15, + markedFinalAt: null, + at: { + id: '3', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + name: 'Firefox' + }, + testPlanVersion: { + id: '74', + title: 'Editor Menubar Example', + phase: 'DRAFT', + gitSha: 'aea64f84b8fa8b21e94f5d9afd7035570bc1bed3', + gitMessage: 'The message for this SHA', + testPlan: { + directory: 'menubar-editor' + }, + updatedAt: '2021-11-30T14:51:28.000Z' + }, + draftTestPlanRuns: [] + } + ], + testPlans: [] + } + } + } +]; diff --git a/client/tests/__mocks__/GraphQLMocks/index.js b/client/tests/__mocks__/GraphQLMocks/index.js new file mode 100644 index 000000000..f237b96e0 --- /dev/null +++ b/client/tests/__mocks__/GraphQLMocks/index.js @@ -0,0 +1,36 @@ +import { TEST_QUEUE_PAGE_QUERY } from '@components/TestQueue/queries'; +import { DATA_MANAGEMENT_PAGE_QUERY } from '@components/DataManagement/queries'; +import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '@components/TestPlanReportStatusDialog/queries'; +import { ME_QUERY } from '@components/App/queries'; + +import TestQueuePageAdminNotPopulatedMock from './TestQueuePageAdminNotPopulatedMock'; +import TestQueuePageAdminPopulatedMock from './TestQueuePageAdminPopulatedMock'; +import TestQueuePageTesterNotPopulatedMock from './TestQueuePageTesterNotPopulatedMock'; +import TestQueuePageTesterPopulatedMock from './TestQueuePageTesterPopulatedMock'; +import DataManagementPagePopulatedMock from './DataManagementPagePopulatedMock'; +import TestPlanReportStatusDialogMock from './TestPlanReportStatusDialogMock'; + +export const TEST_QUEUE_PAGE_ADMIN_NOT_POPULATED_MOCK_DATA = + TestQueuePageAdminNotPopulatedMock(TEST_QUEUE_PAGE_QUERY); + +export const TEST_QUEUE_PAGE_ADMIN_POPULATED_MOCK_DATA = + TestQueuePageAdminPopulatedMock(TEST_QUEUE_PAGE_QUERY); + +export const TEST_QUEUE_PAGE_TESTER_NOT_POPULATED_MOCK_DATA = + TestQueuePageTesterNotPopulatedMock(TEST_QUEUE_PAGE_QUERY); + +export const TEST_QUEUE_PAGE_TESTER_POPULATED_MOCK_DATA = + TestQueuePageTesterPopulatedMock(TEST_QUEUE_PAGE_QUERY); + +export const DATA_MANAGEMENT_PAGE_POPULATED_MOCK_DATA = + DataManagementPagePopulatedMock( + ME_QUERY, + DATA_MANAGEMENT_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ); + +export const TEST_PLAN_REPORT_STATUS_DIALOG_MOCK_DATA = + TestPlanReportStatusDialogMock( + ME_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ); diff --git a/client/tests/calculateTestPlanReportCompletionPercentage.test.js b/client/tests/calculateTestPlanReportCompletionPercentage.test.js new file mode 100644 index 000000000..f03b7593f --- /dev/null +++ b/client/tests/calculateTestPlanReportCompletionPercentage.test.js @@ -0,0 +1,57 @@ +import { calculateTestPlanReportCompletionPercentage } from '../components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage'; + +describe('calculateTestPlanReportCompletionPercentage', () => { + test('returns 0 when metrics or draftTestPlanRuns is not defined', () => { + expect(calculateTestPlanReportCompletionPercentage({})).toBe(0); + expect( + calculateTestPlanReportCompletionPercentage({ metrics: {} }) + ).toBe(0); + expect( + calculateTestPlanReportCompletionPercentage({ + draftTestPlanRuns: [] + }) + ).toBe(0); + }); + + test('returns 0 when draftTestPlanRuns is empty', () => { + expect( + calculateTestPlanReportCompletionPercentage({ + metrics: { testsCount: 5 }, + draftTestPlanRuns: [] + }) + ).toBe(0); + }); + + test('returns 0 and not Infinity when total tests possible is 0', () => { + const metrics = { testsCount: 0 }; + const draftTestPlanRuns = [ + { testResults: [1, 2, 3] }, + { testResults: [1, 2, 3, 4, 5] } + ]; + + expect( + calculateTestPlanReportCompletionPercentage({ + metrics, + draftTestPlanRuns + }) + ).toBe(0); + }); + + test('calculates and returns the correct percentage when draftTestPlanRuns has testResults', () => { + const metrics = { testsCount: 5 }; + const draftTestPlanRuns = [ + { testResults: [1, 2] }, + { testResults: [1, 2, 3] } + ]; + + // Output should follow this formula: + // (NUMBER_COMPLETED_TESTS_BY_ALL_TESTERS / (NUMBER_ASSIGNED_TESTERS * NUMBER_TESTS_IN_PLAN)) * 100 + // (5 / (2 * 5)) * 100 = 50 + expect( + calculateTestPlanReportCompletionPercentage({ + metrics, + draftTestPlanRuns + }) + ).toBe(50); + }); +}); diff --git a/client/utils/aria.js b/client/utils/aria.js index eaf6450de..6118c86f5 100644 --- a/client/utils/aria.js +++ b/client/utils/aria.js @@ -8,3 +8,18 @@ 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'; + case 'DEPRECATED': + return 'Deprecated'; + } +}; diff --git a/client/utils/constants.js b/client/utils/constants.js new file mode 100644 index 000000000..8f496c7cf --- /dev/null +++ b/client/utils/constants.js @@ -0,0 +1,6 @@ +export const TEST_PLAN_VERSION_PHASES = { + RD: 'RD', + DRAFT: 'DRAFT', + CANDIDATE: 'CANDIDATE', + RECOMMENDED: 'RECOMMENDED' +}; diff --git a/client/utils/createIssueLink.js b/client/utils/createIssueLink.js new file mode 100644 index 000000000..253d17517 --- /dev/null +++ b/client/utils/createIssueLink.js @@ -0,0 +1,158 @@ +const GITHUB_ISSUES_URL = + process.env.ENVIRONMENT === 'production' + ? 'https://github.com/w3c/aria-at' + : 'https://github.com/bocoup/aria-at'; + +const atLabelMap = { + 'VoiceOver for macOS': 'vo', + JAWS: 'jaws', + NVDA: 'nvda' +}; + +const createIssueLink = ({ + isCandidateReview = false, + isCandidateReviewChangesRequested = false, + testPlanDirectory, + testPlanTitle, + versionString, + testTitle = null, + testRowNumber = null, + testRenderedUrl = null, + atName, + atVersionName = null, + browserName = null, + browserVersionName = null, + conflictMarkdown = null, + reportLink = null +}) => { + if (!(testPlanDirectory || testPlanTitle || versionString || atName)) { + throw new Error('Cannot create issue link due to missing parameters'); + } + + const hasTest = !!(testTitle && testRowNumber && testRenderedUrl); + + let title; + if (hasTest) { + let titleStart; + if (isCandidateReview) { + titleStart = isCandidateReviewChangesRequested + ? `${atName} Changes Requested` + : `${atName} Feedback`; + } else { + titleStart = 'Feedback'; + } + + title = + `${titleStart}: "${testTitle}" (${testPlanTitle}, ` + + `Test ${testRowNumber}, ${versionString})`; + } else { + title = `${atName} General Feedback: ${testPlanTitle} ${versionString}`; + } + + const labels = + (isCandidateReview ? 'candidate-review,' : '') + + `${atLabelMap[atName]},` + + (isCandidateReviewChangesRequested ? 'changes-requested' : 'feedback'); + + let reportLinkFormatted = ''; + if (reportLink) { + reportLinkFormatted = `- Report Page: [Link](${reportLink})\n`; + } + + let testSetupFormatted = ''; + if (hasTest) { + // TODO: fix renderedUrl + let modifiedRenderedUrl = testRenderedUrl.replace( + /.+(?=\/tests)/, + 'https://aria-at.netlify.app' + ); + + const shortenedUrl = modifiedRenderedUrl?.match(/[^/]+$/)[0]; + + let atFormatted; + if (atVersionName) { + atFormatted = `- AT: ${atName} (Version ${atVersionName})\n`; + } else { + atFormatted = `- AT: ${atName}\n`; + } + + let browserFormatted = ''; + if (browserName && browserVersionName) { + browserFormatted = `- Browser: ${browserName} (Version ${browserVersionName})\n`; + } else if (browserName) { + browserFormatted = `- Browser: ${browserName}\n`; + } + + testSetupFormatted = + `## Test Setup\n\n` + + `- Test File: ` + + `[${shortenedUrl}](${modifiedRenderedUrl})\n` + + reportLinkFormatted + + atFormatted + + browserFormatted + + '\n'; + } + + const hiddenIssueMetadata = JSON.stringify({ + testPlanDirectory, + versionString, + atName, + browserName, + testRowNumber, + isCandidateReview, + isCandidateReviewChangesRequested + }); + + let body = + `## Description of Behavior\n\n` + + `\n\n` + + testSetupFormatted + + `\n` + + ``; + + if (conflictMarkdown) { + body += `\n${conflictMarkdown}`; + } + + return ( + `${GITHUB_ISSUES_URL}/issues/new?title=${encodeURI(title)}&` + + `labels=${labels}&body=${encodeURIComponent(body)}` + ); +}; + +export const getIssueSearchLink = ({ + isCandidateReview = false, + isCandidateReviewChangesRequested = false, + username = null, + atName, + testPlanTitle, + versionString, + testRowNumber = null +}) => { + let atKey; + if (atName === 'JAWS' || atName === 'NVDA') { + atKey = atName.toLowerCase(); + } else { + atKey = 'vo'; + } + + const query = [ + isCandidateReview ? `label:candidate-review` : '', + isCandidateReviewChangesRequested + ? `label:changes-requested` + : 'label:feedback', + `label:${atLabelMap[atName]}`, + username ? `author:${username}` : '', + `label:${atKey}`, + `"${testPlanTitle}"`, + testRowNumber ? `Test ${testRowNumber}` : '', + versionString + ] + .filter(str => str) + .join(' '); + + return `${GITHUB_ISSUES_URL}/issues?q=${encodeURI(query)}`; +}; + +export default createIssueLink; 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/config/test.env b/config/test.env index 22c5a305f..5556e6327 100644 --- a/config/test.env +++ b/config/test.env @@ -10,7 +10,8 @@ ENVIRONMENT=test ALLOW_FAKE_ROLE=true IMPORT_CONFIG=../config/test.env -IMPORT_ARIA_AT_TESTS_COMMIT_1=1aa3b74d24d340362e9f511eae33788d55487d12 +IMPORT_ARIA_AT_TESTS_COMMIT_1=5fe7afd82fe51c185b8661276105190a59d47322 +IMPORT_ARIA_AT_TESTS_COMMIT_2=1aa3b74d24d340362e9f511eae33788d55487d12 GITHUB_OAUTH_SERVER=http://localhost:4466 GITHUB_GRAPHQL_SERVER=http://localhost:4466 diff --git a/docs/database.md b/docs/database.md index 2804e1e00..b90318157 100644 --- a/docs/database.md +++ b/docs/database.md @@ -3,8 +3,9 @@ The database migrations are managed by [Sequelize](https://sequelize.org/). To read and understand the schema, see the Sequelize models that represent the data in `server/models`. Each model represents a table in the database. ## Setting up a local database for development + 0. Install PostgreSQL - - Mac + - Mac ``` brew install postgresql@14 brew services start postgresql@14 @@ -30,7 +31,9 @@ The database migrations are managed by [Sequelize](https://sequelize.org/). To r ``` 4. Import the most recent tests from the [aria-at repository](https://github.com/w3c/aria-at): ``` - yarn db-import-tests:dev + yarn db-import-tests:dev -c 5fe7afd82fe51c185b8661276105190a59d47322; + yarn db-import-tests:dev -c 1aa3b74d24d340362e9f511eae33788d55487d12; + yarn db-import-tests:dev; ``` All at once: @@ -45,6 +48,8 @@ fi; yarn sequelize db:migrate; yarn sequelize db:seed:all; +yarn db-import-tests:dev -c 5fe7afd82fe51c185b8661276105190a59d47322; +yarn db-import-tests:dev -c 1aa3b74d24d340362e9f511eae33788d55487d12; yarn db-import-tests:dev; ``` @@ -73,6 +78,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 @@ -82,6 +92,8 @@ The instructions are similar for the test database, with one extra step: yarn db-init:test; yarn sequelize:test db:migrate; yarn sequelize:test db:seed:all; +yarn workspace server db-import-tests:test -c 5fe7afd82fe51c185b8661276105190a59d47322; +yarn workspace server db-import-tests:test -c 1aa3b74d24d340362e9f511eae33788d55487d12; yarn workspace server db-import-tests:test; yarn workspace server db-populate-sample-data:test; ``` @@ -89,9 +101,10 @@ yarn workspace server db-populate-sample-data:test; ### Inspecting the database To connect to the Postgres table locally: - ``` - yarn run dotenv -e config/dev.env psql - ``` + +``` +yarn run dotenv -e config/dev.env psql +``` ## Application development: modifications to the schema 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..df073d6bb 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,12 @@ const queryReports = async () => { name } } - testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) { + testPlanReports( + testPlanVersionPhases: [CANDIDATE, RECOMMENDED] + isFinal: true + ) { id metrics - status at { id name @@ -64,6 +66,7 @@ const queryReports = async () => { testPlanVersion { id title + phase updatedAt testPlan { id @@ -185,16 +188,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 +215,7 @@ const renderEmbed = ({ allBrowsers, allAtVersionsByAt, testPlanVersionIds, - status, + phase, reportsByAt } = getLatestReportsForPattern({ pattern, allTestPlanReports }); const allAtBrowserCombinations = Object.fromEntries( @@ -232,7 +235,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 e65def618..528b93829 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,44 @@ 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]! + """ + A list of all issues which have filed through "Raise an Issue" buttons + in the app. Note that results will be cached for at least ten seconds. + """ + issues: [Issue]! + } + + """ + The life-cycle of a TestPlanVersion from the point it is imported automatically + or by an admin until it is saved an available to the public on the reports page. + """ + enum TestPlanVersionPhase { + """ + Accepting new TestPlanRuns from testers. + """ + RD + """ + 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 + """ + The TestPlanVersion is now outdated and replaced by another version. + """ + DEPRECATED } """ @@ -271,16 +284,35 @@ 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 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. + """ + candidatePhaseReachedAt: Timestamp + """ + Date of when the TestPlanVersion was last updated to the 'Recommended' + phase. + """ + recommendedPhaseReachedAt: Timestamp + """ + The intended target date for the final TestPlanVersion phase promotion. + Based on the ARIA-AT Working Mode. + https://github.com/w3c/aria-at/wiki/Working-Mode + """ + recommendedPhaseTargetDate: Timestamp + """ + The date when the TestPlanVersion was deprecated. + """ + deprecatedAt: Timestamp """ The TestPlan this TestPlanVersion is a snapshot of. """ @@ -297,6 +329,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 @@ -315,6 +348,17 @@ const graphqlSchema = gql` The tests as they stand at this point in time. """ tests: [Test]! + """ + The TestPlanReports attached to the TestPlanVersion. There will always + 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(isFinal: Boolean): [TestPlanReport]! } """ @@ -714,27 +758,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 @@ -772,10 +795,20 @@ const graphqlSchema = gql` """ author: String! """ + The issue title in GitHub. + """ + title: String! + """ Link to the GitHub issue's first comment. """ link: String! """ + Will be true if the issue was raised on the Candidate Review page + of the app (as opposed to other places with "raise an issue" buttons like + the test queue or the reports page.) + """ + isCandidateReview: Boolean! + """ Indicates the type of issue. 'CHANGES_REQUESTED' or 'FEEDBACK'. 'FEEDBACK' is the default type. """ @@ -788,6 +821,25 @@ const graphqlSchema = gql` Test Number the issue was raised for. """ testNumberFilteredByAt: Int + """ + The time the issue was created, according to GitHub. + """ + createdAt: Timestamp! + """ + The time the issue was closed, if it was closed. + """ + closedAt: Timestamp + """ + The AT associated with the issue. Although there are not currently any + cases where we generate GitHub issues without an associated AT, that + may not remain true forever and we do support this field being + undefined. + """ + at: At + """ + The browser associated with the issue, which may not be present. + """ + browser: Browser } """ @@ -800,26 +852,6 @@ const graphqlSchema = gql` """ id: ID! """ - See TestPlanReportStatus type for more information. - """ - status: TestPlanReportStatus! - """ - Date of when the TestPlanReport was last updated to the 'Candidate' - status. - """ - candidateStatusReachedAt: Timestamp - """ - Date of when the TestPlanReport was last updated to the 'Recommended' - status. - """ - recommendedStatusReachedAt: Timestamp - """ - The intended target date for the final TestPlanReport status promotion. - Based on the ARIA-AT Working Mode. - https://github.com/w3c/aria-at/wiki/Working-Mode - """ - recommendedStatusTargetDate: Timestamp - """ The snapshot of a TestPlan to use. """ testPlanVersion: TestPlanVersion! @@ -855,7 +887,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]! @@ -872,8 +904,8 @@ const graphqlSchema = gql` """ finalizedTestResults: [TestResult] """ - These are the different feedback and requested change items created for - the TestPlanReport and retrieved from GitHub. + A list of all issues which have filed through "Raise an Issue" buttons + in the app. Note that results will be cached for at least ten seconds. """ issues: [Issue]! """ @@ -894,6 +926,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! } """ @@ -988,21 +1030,22 @@ const graphqlSchema = gql` """ Get all TestPlanVersions. """ - testPlanVersions: [TestPlanVersion]! + testPlanVersions(phases: [TestPlanVersionPhase]): [TestPlanVersion]! """ Get a particular TestPlanVersion by ID. """ - testPlanVersion(id: ID!): TestPlanVersion + 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. @@ -1088,26 +1131,24 @@ 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. """ - updateStatus( - status: TestPlanReportStatus! - candidateStatusReachedAt: Timestamp - recommendedStatusTargetDate: Timestamp - ): PopulatedData! + markAsFinal: 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. + 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. """ - bulkUpdateStatus(status: TestPlanReportStatus!): [PopulatedData]! + unmarkAsFinal: PopulatedData! """ - Update the report recommended status target date. - Only available to admins. + Update the report to a specific TestPlanVersion id. """ - updateRecommendedStatusTargetDate( - recommendedStatusTargetDate: Timestamp! + updateTestPlanReportTestPlanVersion( + """ + The TestPlanReport to update. + """ + input: TestPlanReportInput! ): PopulatedData! """ Move the vendor review status from READY to IN PROGRESS @@ -1121,6 +1162,29 @@ const graphqlSchema = gql` deleteTestPlanReport: NoResponse } + """ + Mutations scoped to a previously-created TestPlanVersion. + """ + type TestPlanVersionOperations { + """ + Update the test plan version phase. Remember that all conflicts must be resolved + 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. + Only available to admins. + """ + updateRecommendedPhaseTargetDate( + recommendedPhaseTargetDate: Timestamp! + ): PopulatedData! + } + """ Mutations scoped to a previously-created TestPlanRun. """ @@ -1202,10 +1266,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( """ @@ -1226,6 +1287,10 @@ const graphqlSchema = gql` """ testResult(id: ID!): TestResultOperations! """ + Get the available mutations for the given TestPlanVersion. + """ + testPlanVersion(id: ID!): TestPlanVersionOperations! + """ Update the currently-logged-in User. """ updateMe(input: UserInput): User! 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/20211116172219-commandSequences.js b/server/migrations/20211116172219-commandSequences.js index e4acf4d48..85d6243ac 100644 --- a/server/migrations/20211116172219-commandSequences.js +++ b/server/migrations/20211116172219-commandSequences.js @@ -11,7 +11,15 @@ module.exports = { if (!Number(testPlanVersionCount)) return; const testPlanVersions = await TestPlanVersion.findAll({ - attributes: { exclude: ['testPlanId'] } + attributes: { + exclude: [ + 'testPlanId', + 'phase', + 'candidatePhaseReachedAt', + 'recommendedPhaseReachedAt', + 'recommendedPhaseTargetDate' + ] + } }); await Promise.all( testPlanVersions.map(testPlanVersion => { diff --git a/server/migrations/20211118143508-testRowNumber.js b/server/migrations/20211118143508-testRowNumber.js index b8b894086..a52618f71 100644 --- a/server/migrations/20211118143508-testRowNumber.js +++ b/server/migrations/20211118143508-testRowNumber.js @@ -10,7 +10,15 @@ module.exports = { if (!Number(testPlanVersionCount)) return; const testPlanVersions = await TestPlanVersion.findAll({ - attributes: { exclude: ['testPlanId'] } + attributes: { + exclude: [ + 'testPlanId', + 'phase', + 'candidatePhaseReachedAt', + 'recommendedPhaseReachedAt', + 'recommendedPhaseTargetDate' + ] + } }); await Promise.all( testPlanVersions.map(testPlanVersion => { @@ -32,7 +40,15 @@ module.exports = { down: async () => { const testPlanVersions = await TestPlanVersion.findAll({ - attributes: { exclude: ['testPlanId'] } + attributes: { + exclude: [ + 'testPlanId', + 'phase', + 'candidatePhaseReachedAt', + 'recommendedPhaseReachedAt', + 'recommendedPhaseTargetDate' + ] + } }); await Promise.all( testPlanVersions.map(testPlanVersion => { 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 new file mode 100644 index 000000000..7b1fa8d88 --- /dev/null +++ b/server/migrations/20230608170853-addDateColumnsToTestPlanVersion.js @@ -0,0 +1,117 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'TestPlanVersion', + 'phase', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'DRAFT' + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanVersion', + 'draftPhaseReachedAt', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanVersion', + 'candidatePhaseReachedAt', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanVersion', + 'recommendedPhaseReachedAt', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanVersion', + 'recommendedPhaseTargetDate', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanVersion', + 'deprecatedAt', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + }); + }, + + async down(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn('TestPlanVersion', 'phase', { + transaction + }); + await queryInterface.removeColumn( + 'TestPlanVersion', + 'draftPhaseReachedAt', + { + transaction + } + ); + await queryInterface.removeColumn( + 'TestPlanVersion', + 'candidatePhaseReachedAt', + { + transaction + } + ); + await queryInterface.removeColumn( + 'TestPlanVersion', + 'recommendedPhaseReachedAt', + { + transaction + } + ); + await queryInterface.removeColumn( + 'TestPlanVersion', + 'recommendedPhaseTargetDate', + { + transaction + } + ); + await queryInterface.removeColumn( + 'TestPlanVersion', + 'deprecatedAt', + { + transaction + } + ); + }); + } +}; diff --git a/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js b/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js new file mode 100644 index 000000000..34a58ebd2 --- /dev/null +++ b/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js @@ -0,0 +1,645 @@ +'use strict'; + +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, 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", + "TestPlanVersion".id as "testPlanVersionId", + directory, + status, + "testPlanVersionId", + "atId", + "browserId", + "updatedAt" as "gitShaDate", + "TestPlanReport"."candidateStatusReachedAt", + "TestPlanReport"."recommendedStatusReachedAt", + "TestPlanReport"."recommendedStatusTargetDate" + from "TestPlanReport" + join "TestPlanVersion" on "TestPlanReport"."testPlanVersionId" = "TestPlanVersion".id + where status in ('CANDIDATE', 'RECOMMENDED') + order by title, "gitShaDate" desc`, + { + transaction + } + ); + const testPlanReportsData = testPlanReportsQuery[0]; + + const testPlanReportLatestReleasedQuery = + async testPlanReportId => { + return await queryInterface.sequelize.query( + `select "atVersionId", name, "releasedAt", "testPlanReportId", "testerUserId", "testPlanRunId" + from ( select distinct "TestPlanReport".id as "testPlanReportId", + "TestPlanRun".id as "testPlanRunId", + "TestPlanRun"."testerUserId", + (jsonb_array_elements("testResults") ->> 'atVersionId')::integer as "atVersionId" + from "TestPlanReport" + left outer join "TestPlanRun" on "TestPlanRun"."testPlanReportId" = "TestPlanReport".id + where "testPlanReportId" = ${testPlanReportId} + group by "TestPlanReport".id, "TestPlanRun".id ) as atVersionResults + join "AtVersion" on "AtVersion".id = atVersionResults."atVersionId" + order by "releasedAt" desc + limit 1;`, + { transaction } + ); + }; + + const testPlanReportsByDirectory = {}; + for (let i = 0; i < testPlanReportsData.length; i++) { + let testPlanReport = testPlanReportsData[i]; + const testPlanReportLatestReleasedData = ( + await testPlanReportLatestReleasedQuery( + testPlanReport.testPlanReportId + ) + )[0]; + testPlanReport.latestAtVersionReleasedAt = + testPlanReportLatestReleasedData[0].releasedAt; + + if (!testPlanReportsByDirectory[testPlanReport.directory]) + testPlanReportsByDirectory[testPlanReport.directory] = [ + testPlanReport + ]; + else + testPlanReportsByDirectory[testPlanReport.directory].push( + testPlanReport + ); + } + + // We now need to rely on a single TestPlanVersion now, rather than having consolidated + // TestPlanReports, we need to do the following: + + // Determine which TestPlanReport is the latest TestPlanVersion for a report group + // AND determine which TestPlanReports need to be updated to that latest version + // (without losing data, but there may need to be some manual updates that will have to + // happen) + + const findHighestTestPlanVersion = testPlanReportsByDirectory => { + const result = {}; + + for (const directory in testPlanReportsByDirectory) { + const reports = testPlanReportsByDirectory[directory]; + + let highestTestPlanVersion = 0; + let highestCollectiveStatus = 'RECOMMENDED'; + let latestAtVersionReleasedAtOverall = ''; + let latestCandidateStatusReachedAt = ''; + let latestRecommendedStatusReachedAt = ''; + let latestRecommendedStatusTargetDate = ''; + let latestAtBrowserMatchings = {}; + + for (const report of reports) { + const { + testPlanVersionId, + status, + atId, + browserId, + latestAtVersionReleasedAt, + candidateStatusReachedAt, + recommendedStatusReachedAt, + recommendedStatusTargetDate + } = report; + + // Determine which of the AT+Browser pairs should be updated (these are + // what's being currently displayed on the reports page for each column) + const uniqueAtBrowserKey = `${atId}-${browserId}`; + if ( + !latestAtBrowserMatchings[uniqueAtBrowserKey] || + latestAtVersionReleasedAt > + latestAtBrowserMatchings[uniqueAtBrowserKey] + .latestAtVersionReleasedAt + ) { + latestAtBrowserMatchings[uniqueAtBrowserKey] = + report; + + if (status === 'CANDIDATE') + highestCollectiveStatus = 'CANDIDATE'; + } + + if ( + testPlanVersionId > highestTestPlanVersion || + (testPlanVersionId === highestTestPlanVersion && + latestAtVersionReleasedAt > + latestAtVersionReleasedAtOverall) + ) { + highestTestPlanVersion = testPlanVersionId; + latestAtVersionReleasedAtOverall = + latestAtVersionReleasedAt; + latestCandidateStatusReachedAt = + candidateStatusReachedAt; + latestRecommendedStatusReachedAt = + recommendedStatusReachedAt; + latestRecommendedStatusTargetDate = + recommendedStatusTargetDate; + } + } + + result[directory] = { + directory, + highestTestPlanVersion, + highestCollectiveStatus, + latestAtVersionReleasedAtOverall, + latestCandidateStatusReachedAt, + latestRecommendedStatusReachedAt, + latestRecommendedStatusTargetDate, + latestAtBrowserMatchings + }; + } + + return result; + }; + + // Find the latest testPlanVersion for each directory + const highestVersions = findHighestTestPlanVersion( + testPlanReportsByDirectory + ); + + 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]; + + // Update the noted test plan versions to use the dates of the currently defined + // "latest" test plan reports' phases + const { + highestTestPlanVersion: highestTestPlanVersionId, + highestCollectiveStatus: phase, + latestCandidateStatusReachedAt: candidatePhaseReachedAt, + latestRecommendedStatusReachedAt: recommendedPhaseReachedAt, + latestRecommendedStatusTargetDate: + recommendedPhaseTargetDate, + latestAtBrowserMatchings + } = highestTestPlanVersion; + + await queryInterface.sequelize.query( + `UPDATE "TestPlanVersion" + SET phase = ?, + "candidatePhaseReachedAt" = ?, + "recommendedPhaseReachedAt" = ?, + "recommendedPhaseTargetDate" = ? + WHERE id = ?`, + { + replacements: [ + phase, + candidatePhaseReachedAt, + recommendedPhaseReachedAt, + recommendedPhaseTargetDate, + highestTestPlanVersionId + ], + transaction + } + ); + + // Update the individual reports, so they can be included as part of the same phase + // by being a part of the same test plan version + for (const uniqueMatchKey in latestAtBrowserMatchings) { + const uniqueMatch = + latestAtBrowserMatchings[uniqueMatchKey]; + if ( + uniqueMatch.testPlanVersionId !== + highestTestPlanVersionId + ) { + // eslint-disable-next-line no-console + console.info( + `=== Updating testPlanReportId ${uniqueMatch.testPlanReportId} to testPlanVersionId ${highestTestPlanVersionId} for atId ${uniqueMatch.atId} and browserId ${uniqueMatch.browserId} ===` + ); + 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 new file mode 100644 index 000000000..713e51bb3 --- /dev/null +++ b/server/migrations/20230614004831-removeDateColumnsFromTestPlanReportAndRenameCandidateStatusReachedAtToApprovedAt.js @@ -0,0 +1,62 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.renameColumn( + 'TestPlanReport', + 'candidateStatusReachedAt', + 'markedFinalAt', + { transaction } + ); + await queryInterface.removeColumn( + 'TestPlanReport', + 'recommendedStatusReachedAt', + { + transaction + } + ); + await queryInterface.removeColumn( + 'TestPlanReport', + 'recommendedStatusTargetDate', + { + transaction + } + ); + }); + }, + + async down(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.renameColumn( + 'TestPlanReport', + 'markedFinalAt', + 'candidateStatusReachedAt', + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanReport', + 'recommendedStatusReachedAt', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanReport', + 'recommendedStatusTargetDate', + { + type: Sequelize.DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + { transaction } + ); + }); + } +}; diff --git a/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js b/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js new file mode 100644 index 000000000..1de8cbf57 --- /dev/null +++ b/server/migrations/20230626203205-updatePhaseAndDraftPhaseReachedAtColumnsForTestPlanVersion.js @@ -0,0 +1,80 @@ +'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) { + const draftPhaseReachedAt = new Date(updatedAt); + draftPhaseReachedAt.setSeconds( + // Set draftPhaseReachedAt to happen 60 seconds after updatedAt for general + // 'correctness' and to help with any app sorts + draftPhaseReachedAt.getSeconds() + 60 + ); + await queryInterface.sequelize.query( + `UPDATE "TestPlanVersion" SET "draftPhaseReachedAt" = ? WHERE id = ?`, + { + replacements: [draftPhaseReachedAt, 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/migrations/20230830225248-addMissingDeprecatedAtAndReorderPhaseChangeDates.js b/server/migrations/20230830225248-addMissingDeprecatedAtAndReorderPhaseChangeDates.js new file mode 100644 index 000000000..77d36372a --- /dev/null +++ b/server/migrations/20230830225248-addMissingDeprecatedAtAndReorderPhaseChangeDates.js @@ -0,0 +1,165 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + // Check for all instances of TestPlanVersions that have "markedAsFinal" reports + // and in the DRAFT phase, and set those to CANDIDATE (so they can be shown as having + // reports been generated for them even though they are now deprecated) + // + // Eg. 281 (Disclosure Navigation) and + // 1178 (Radio Group Example Using aria-activedescendant) TestPlanVersions + const testPlanVersionsToSetToCandidate = + await queryInterface.sequelize.query( + `select "TestPlanVersion".id, phase, "draftPhaseReachedAt", "markedFinalAt" + from "TestPlanVersion" + join "TestPlanReport" on "TestPlanVersion".id = "TestPlanReport"."testPlanVersionId" + where "markedFinalAt" is not null + and phase = 'DRAFT';`, + { + type: Sequelize.QueryTypes.SELECT, + transaction + } + ); + + for (const testPlanVersion of testPlanVersionsToSetToCandidate) { + const candidatePhaseReachedAt = new Date( + testPlanVersion.draftPhaseReachedAt + ); + + // Set candidatePhaseReachedAt to draftPhaseReachedAt date (+1 day) + candidatePhaseReachedAt.setDate( + candidatePhaseReachedAt.getDate() + 1 + ); + + const recommendedPhaseTargetDate = new Date( + candidatePhaseReachedAt + ); + recommendedPhaseTargetDate.setDate( + candidatePhaseReachedAt.getDate() + 180 + ); + + await queryInterface.sequelize.query( + `update "TestPlanVersion" + set "candidatePhaseReachedAt" = ?, + "recommendedPhaseTargetDate" = ?, + phase = 'CANDIDATE' + where id = ?`, + { + replacements: [ + candidatePhaseReachedAt, + recommendedPhaseTargetDate, + testPlanVersion.id + ], + transaction + } + ); + } + + // Check for instances of all older TestPlanVersions and deprecate them + const testPlanVersions = await queryInterface.sequelize.query( + `select id, directory, "updatedAt", "draftPhaseReachedAt", "candidatePhaseReachedAt", "recommendedPhaseReachedAt", "deprecatedAt" + from "TestPlanVersion" + order by directory, "updatedAt";`, + { + type: Sequelize.QueryTypes.SELECT, + transaction + } + ); + + // Group objects by directory + const groupedData = {}; + for (const testPlanVersion of testPlanVersions) { + if (!groupedData[testPlanVersion.directory]) { + groupedData[testPlanVersion.directory] = []; + } + groupedData[testPlanVersion.directory].push(testPlanVersion); + } + + // Update "deprecatedAt" based on next object's "updatedAt" + for (const directory in groupedData) { + const objects = groupedData[directory]; + for (let i = 0; i < objects.length - 1; i++) { + objects[i].deprecatedAt = objects[i + 1].updatedAt; + } + } + + // Flatten the grouped data back into a single array + const flattenedTestPlanVersions = Object.values(groupedData).reduce( + (acc, objects) => acc.concat(objects), + [] + ); + + for (let testPlanVersion of flattenedTestPlanVersions) { + // Check for the instances where candidatePhaseReachedAt is shown as happening + // before draftPhaseReachedAt + if ( + testPlanVersion.draftPhaseReachedAt && + testPlanVersion.candidatePhaseReachedAt + ) { + let draftPhaseReachedAt = new Date( + testPlanVersion.draftPhaseReachedAt + ); + let candidatePhaseReachedAt = new Date( + testPlanVersion.candidatePhaseReachedAt + ); + + // Update candidatePhaseReachedAt to be the draftPhaseReachedAt date (+1 day) + // (because that phase happening before shouldn't be possible) + if (candidatePhaseReachedAt < draftPhaseReachedAt) { + const newCandidatePhaseReachedAt = new Date( + draftPhaseReachedAt + ); + newCandidatePhaseReachedAt.setDate( + newCandidatePhaseReachedAt.getDate() + 1 + ); + + testPlanVersion.candidatePhaseReachedAt = + newCandidatePhaseReachedAt; + + await queryInterface.sequelize.query( + `update "TestPlanVersion" + set "candidatePhaseReachedAt" = ? + where id = ?`, + { + replacements: [ + testPlanVersion.candidatePhaseReachedAt, + testPlanVersion.id + ], + transaction + } + ); + } + } + + if (testPlanVersion.deprecatedAt) { + const deprecatedAt = new Date(testPlanVersion.deprecatedAt); + deprecatedAt.setSeconds(deprecatedAt.getSeconds() - 1); + + // Add deprecatedAt for applicable testPlanVersions + await queryInterface.sequelize.query( + `update "TestPlanVersion" + set "deprecatedAt" = ?, + phase = 'DEPRECATED' + where id = ?`, + { + replacements: [deprecatedAt, testPlanVersion.id], + transaction + } + ); + } + } + }); + }, + + async down(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.sequelize.query( + `update "TestPlanVersion" + set "deprecatedAt" = null;`, + { 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 230c775f3..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 }, @@ -28,21 +18,6 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, - candidateStatusReachedAt: { - type: DataTypes.DATE, - defaultValue: null, - allowNull: true - }, - recommendedStatusReachedAt: { - type: DataTypes.DATE, - defaultValue: null, - allowNull: true - }, - recommendedStatusTargetDate: { - type: DataTypes.DATE, - defaultValue: null, - allowNull: true - }, vendorReviewStatus: { type: DataTypes.TEXT, // 'READY', 'IN_PROGRESS', 'APPROVED' defaultValue: null, @@ -52,6 +27,11 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.JSONB, defaultValue: {}, allowNull: false + }, + markedFinalAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true } }, { @@ -60,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 c69470bca..6e2a3dcef 100644 --- a/server/models/TestPlanVersion.js +++ b/server/models/TestPlanVersion.js @@ -1,4 +1,11 @@ const MODEL_NAME = 'TestPlanVersion'; +const PHASE = { + RD: 'RD', + DRAFT: 'DRAFT', + CANDIDATE: 'CANDIDATE', + RECOMMENDED: 'RECOMMENDED', + DEPRECATED: 'DEPRECATED' +}; module.exports = function (sequelize, DataTypes) { const Model = sequelize.define( @@ -10,6 +17,11 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, autoIncrement: true }, + phase: { + type: DataTypes.TEXT, + allowNull: false, + defaultValue: PHASE.RD + }, title: { type: DataTypes.TEXT }, directory: { type: DataTypes.TEXT }, gitSha: { type: DataTypes.TEXT }, @@ -26,7 +38,32 @@ module.exports = function (sequelize, DataTypes) { }, tests: { type: DataTypes.JSONB }, testPlanId: { type: DataTypes.INTEGER }, - metadata: { type: DataTypes.JSONB } + metadata: { type: DataTypes.JSONB }, + draftPhaseReachedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + candidatePhaseReachedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + recommendedPhaseReachedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + recommendedPhaseTargetDate: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + }, + deprecatedAt: { + type: DataTypes.DATE, + defaultValue: null, + allowNull: true + } }, { timestamps: false, 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 e8e8af0a3..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,14 +248,11 @@ const createTestPlanReport = async ( const updateTestPlanReport = async ( id, { - status, metrics, testPlanTargetId, testPlanVersionId, - candidateStatusReachedAt, - recommendedStatusReachedAt, - recommendedStatusTargetDate, - vendorReviewStatus + vendorReviewStatus, + markedFinalAt }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, @@ -270,14 +266,11 @@ const updateTestPlanReport = async ( TestPlanReport, { id }, { - status, metrics, testPlanTargetId, testPlanVersionId, - candidateStatusReachedAt, - recommendedStatusReachedAt, - recommendedStatusTargetDate, - vendorReviewStatus + vendorReviewStatus, + markedFinalAt }, options ); @@ -322,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, @@ -338,8 +330,16 @@ const getOrCreateTestPlanReport = async ( create: createTestPlanReport, update: updateTestPlanReport, values: { testPlanVersionId, atId, browserId }, - updateValues: { status }, - returnAttributes: [null, [], [], [], [], [], [], []] + returnAttributes: [ + testPlanReportAttributes, + [], + [], + [], + [], + [], + [], + [] + ] } ], { transaction: options.transaction } @@ -359,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 e29c0930c..e9ac56854 100644 --- a/server/models/services/TestPlanVersionService.js +++ b/server/models/services/TestPlanVersionService.js @@ -187,11 +187,12 @@ const getTestPlanVersions = async ( */ const createTestPlanVersion = async ( { - // ID must be provided so it matches the ID which is baked into the Test + // ID must be provided, so it matches the ID which is baked into the Test // IDs (see LocationOfDataId.js for more information). id, title, directory, + phase, gitSha, gitMessage, testPageUrl, @@ -213,6 +214,7 @@ const createTestPlanVersion = async ( id, title, directory, + phase, gitSha, gitMessage, testPageUrl, @@ -253,13 +255,19 @@ const updateTestPlanVersion = async ( { title, directory, + phase, gitSha, gitMessage, testPageUrl, hashedTests, updatedAt, metadata, - tests + tests, + draftPhaseReachedAt, + candidatePhaseReachedAt, + recommendedPhaseReachedAt, + recommendedPhaseTargetDate, + deprecatedAt }, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, @@ -275,13 +283,19 @@ const updateTestPlanVersion = async ( { title, directory, + phase, gitSha, gitMessage, testPageUrl, hashedTests, updatedAt, metadata, - tests + tests, + draftPhaseReachedAt, + candidatePhaseReachedAt, + recommendedPhaseReachedAt, + recommendedPhaseTargetDate, + deprecatedAt }, options ); diff --git a/server/resolvers/TestPlan/index.js b/server/resolvers/TestPlan/index.js new file mode 100644 index 000000000..60c147e0f --- /dev/null +++ b/server/resolvers/TestPlan/index.js @@ -0,0 +1,5 @@ +const issues = require('./issuesResolver'); + +module.exports = { + issues +}; diff --git a/server/resolvers/TestPlan/issuesResolver.js b/server/resolvers/TestPlan/issuesResolver.js new file mode 100644 index 000000000..be59aa8f8 --- /dev/null +++ b/server/resolvers/TestPlan/issuesResolver.js @@ -0,0 +1,6 @@ +const { getIssues } = require('../TestPlanReport/issuesResolver'); + +const issuesResolver = (testPlan, _, context) => + getIssues({ testPlan, context }); + +module.exports = issuesResolver; 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 7c5cbaa2c..7f714a3bf 100644 --- a/server/resolvers/TestPlanReport/index.js +++ b/server/resolvers/TestPlanReport/index.js @@ -5,11 +5,11 @@ const finalizedTestResults = require('./finalizedTestResultsResolver'); const conflicts = require('./conflictsResolver'); const conflictsLength = require('./conflictsLengthResolver'); const issues = require('./issuesResolver'); -const recommendedStatusTargetDate = require('./recommendedStatusTargetDateResolver'); const atVersions = require('./atVersionsResolver'); const at = require('./atResolver'); const browser = require('./browserResolver'); const latestAtVersionReleasedAt = require('./latestAtVersionReleasedAtResolver'); +const isFinal = require('./isFinalResolver'); module.exports = { runnableTests, @@ -19,9 +19,9 @@ module.exports = { conflicts, conflictsLength, issues, - recommendedStatusTargetDate, 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/TestPlanReport/issuesResolver.js b/server/resolvers/TestPlanReport/issuesResolver.js index b097c911d..8194fc250 100644 --- a/server/resolvers/TestPlanReport/issuesResolver.js +++ b/server/resolvers/TestPlanReport/issuesResolver.js @@ -1,64 +1,92 @@ const { GithubService } = require('../../services'); -const { Base64 } = require('js-base64'); -const moment = require('moment'); +const convertDateToString = require('../../util/convertDateToString'); -const issuesResolver = async testPlanReport => { - if (!testPlanReport.candidateStatusReachedAt) return []; +const issuesResolver = (testPlanReport, _, context) => + getIssues({ testPlanReport, context }); - const searchPrefix = `${testPlanReport.at.name} Feedback: "`; - const searchTestPlanVersionTitle = - testPlanReport.testPlanVersion.dataValues.title; - const searchTestPlanVersionDate = moment( - testPlanReport.testPlanVersion.updatedAt - ).format('DD-MM-YYYY'); - const cacheId = Base64.encode( - `${testPlanReport.id}${searchPrefix}${searchTestPlanVersionTitle}${searchTestPlanVersionDate}` - ); +const getIssues = async ({ testPlanReport, testPlan, context }) => { + const [ats, browsers] = await Promise.all([ + context.atLoader.getAll(), + context.browserLoader.getAll() + ]); - const issues = await GithubService.getCandidateReviewIssuesByAt({ - cacheId, - atName: testPlanReport.at.name - }); + const issues = await GithubService.getAllIssues(); - if (issues.length) { - const filteredIssues = issues.filter( - ({ title }) => - title.includes(searchPrefix) && - title.includes(searchTestPlanVersionTitle) && - title.includes(searchTestPlanVersionDate) + const getHiddenIssueMetadata = issue => { + return JSON.parse( + issue.body.match( + // Since this is human editable it should be okay for the + // JSON part to be multiline, and for additional comments + // to follow this metadata comment + // + )?.[1] ?? 'null' ); - return filteredIssues.map(issue => { - const { - title, - user, - labels, - state, - html_url, - id: topCommentId - } = issue; - const testNumberMatch = title.match(/\sTest \d+,/g); - const testNumberSubstring = testNumberMatch - ? testNumberMatch[0] - : ''; - const testNumberFilteredByAt = testNumberSubstring - ? testNumberSubstring.match(/\d+/g)[0] - : null; + }; + + return issues + .filter(issue => { + const hiddenIssueMetadata = getHiddenIssueMetadata(issue); + + if (testPlanReport) { + const { at, browser, testPlanVersion } = testPlanReport; + + const versionString = `V${convertDateToString( + testPlanVersion.updatedAt, + 'YY.MM.DD' + )}`; + + return ( + hiddenIssueMetadata && + hiddenIssueMetadata.testPlanDirectory === + testPlanVersion.directory && + hiddenIssueMetadata.versionString === versionString && + hiddenIssueMetadata.atName === at.name && + (!hiddenIssueMetadata.browserName || + hiddenIssueMetadata.browserName === browser.name) + ); + } else if (testPlan) { + return ( + hiddenIssueMetadata && + hiddenIssueMetadata.testPlanDirectory === testPlan.directory + ); + } + }) + .map(issue => { + const hiddenIssueMetadata = getHiddenIssueMetadata(issue); + + const { title, user, state, html_url, id: topCommentId } = issue; + + const feedbackType = + hiddenIssueMetadata.isCandidateReviewChangesRequested + ? 'CHANGES_REQUESTED' + : 'FEEDBACK'; + + const at = ats.find( + at => + at.name.toLowerCase() === + hiddenIssueMetadata.atName?.toLowerCase() + ); + const browser = browsers.find( + browser => + browser.name.toLowerCase() === + hiddenIssueMetadata.browserName?.toLowerCase() + ); return { author: user.login, + title, + createdAt: issue.created_at, + closedAt: issue.closed_at, link: `${html_url}#issue-${topCommentId}`, - feedbackType: labels - .map(label => label.name) - .includes('changes-requested') - ? 'CHANGES_REQUESTED' - : 'FEEDBACK', + isCandidateReview: hiddenIssueMetadata.isCandidateReview, + feedbackType, isOpen: state === 'open', - testNumberFilteredByAt + testNumberFilteredByAt: hiddenIssueMetadata.testRowNumber, + at, + browser }; }); - } - - return []; }; module.exports = issuesResolver; +module.exports.getIssues = getIssues; diff --git a/server/resolvers/TestPlanReport/recommendedStatusTargetDateResolver.js b/server/resolvers/TestPlanReport/recommendedStatusTargetDateResolver.js deleted file mode 100644 index 4ebde8f97..000000000 --- a/server/resolvers/TestPlanReport/recommendedStatusTargetDateResolver.js +++ /dev/null @@ -1,15 +0,0 @@ -const recommendedStatusTargetDateResolver = ({ - candidateStatusReachedAt, - recommendedStatusTargetDate -}) => { - // Dependent on working mode and role of user as outlined at: - // https://github.com/w3c/aria-at/wiki/Working-Mode - if (!candidateStatusReachedAt) return null; - if (recommendedStatusTargetDate) return recommendedStatusTargetDate; - - const targetDate = new Date(candidateStatusReachedAt); - targetDate.setDate(candidateStatusReachedAt.getDate() + 180); - return targetDate; -}; - -module.exports = recommendedStatusTargetDateResolver; 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 58b10c6f3..bb33b0145 100644 --- a/server/resolvers/TestPlanReportOperations/index.js +++ b/server/resolvers/TestPlanReportOperations/index.js @@ -1,17 +1,17 @@ const assignTester = require('./assignTesterResolver'); const deleteTestPlanRun = require('./deleteTestPlanRunResolver'); -const updateStatus = require('./updateStatusResolver'); -const bulkUpdateStatus = require('./bulkUpdateStatusResolver'); -const updateRecommendedStatusTargetDate = require('./updateRecommendedStatusTargetDateResolver'); +const markAsFinal = require('./markAsFinalResolver'); +const unmarkAsFinal = require('./unmarkAsFinalResolver'); const deleteTestPlanReport = require('./deleteTestPlanReportResolver'); const promoteVendorReviewStatus = require('./promoteVendorReviewStatusResolver'); +const updateTestPlanReportTestPlanVersion = require('./updateTestPlanReportTestPlanVersionResolver'); module.exports = { assignTester, deleteTestPlanRun, - updateStatus, - bulkUpdateStatus, - updateRecommendedStatusTargetDate, + markAsFinal, + unmarkAsFinal, deleteTestPlanReport, - promoteVendorReviewStatus + 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/updateRecommendedStatusTargetDateResolver.js b/server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js similarity index 66% rename from server/resolvers/TestPlanReportOperations/updateRecommendedStatusTargetDateResolver.js rename to server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js index a281e20df..6fcf0ebbe 100644 --- a/server/resolvers/TestPlanReportOperations/updateRecommendedStatusTargetDateResolver.js +++ b/server/resolvers/TestPlanReportOperations/unmarkAsFinalResolver.js @@ -4,21 +4,20 @@ const { } = require('../../models/services/TestPlanReportService'); const populateData = require('../../services/PopulatedData/populateData'); -const updateRecommendedStatusTargetDateResolver = async ( +const unmarkAsFinalResolver = async ( { parentContext: { id: testPlanReportId } }, - { recommendedStatusTargetDate }, + _, context ) => { const { user } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { throw new AuthenticationError(); } - await updateTestPlanReport(testPlanReportId, { - recommendedStatusTargetDate - }); + await updateTestPlanReport(testPlanReportId, { markedFinalAt: null }); return populateData({ testPlanReportId }, { context }); }; -module.exports = updateRecommendedStatusTargetDateResolver; +module.exports = unmarkAsFinalResolver; diff --git a/server/resolvers/TestPlanReportOperations/updateStatusResolver.js b/server/resolvers/TestPlanReportOperations/updateStatusResolver.js deleted file mode 100644 index 745f3aed2..000000000 --- a/server/resolvers/TestPlanReportOperations/updateStatusResolver.js +++ /dev/null @@ -1,95 +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 recommendedStatusTargetDateResolver = require('../TestPlanReport/recommendedStatusTargetDateResolver'); -const populateData = require('../../services/PopulatedData/populateData'); -const getMetrics = require('../../util/getMetrics'); - -const updateStatusResolver = async ( - { parentContext: { id: testPlanReportId } }, - { status, candidateStatusReachedAt, recommendedStatusTargetDate }, - 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') { - const candidateStatusReachedAtValue = candidateStatusReachedAt - ? candidateStatusReachedAt - : new Date(); - const recommendedStatusTargetDateValue = recommendedStatusTargetDate - ? recommendedStatusTargetDate - : recommendedStatusTargetDateResolver({ - candidateStatusReachedAt - }); - - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics }, - candidateStatusReachedAt: candidateStatusReachedAtValue, - recommendedStatusTargetDate: recommendedStatusTargetDateValue, - vendorReviewStatus: 'READY' - }; - } else if (status === 'RECOMMENDED') { - updateParams = { - ...updateParams, - metrics: { ...testPlanReport.metrics, ...metrics }, - recommendedStatusReachedAt: new Date() - }; - } - } - await updateTestPlanReport(testPlanReportId, updateParams); - - return populateData({ testPlanReportId }, { context }); -}; - -module.exports = updateStatusResolver; diff --git a/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js b/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js new file mode 100644 index 000000000..883325846 --- /dev/null +++ b/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js @@ -0,0 +1,335 @@ +const hash = require('object-hash'); +const { omit } = require('lodash'); +const { AuthenticationError } = require('apollo-server-express'); +const { + getTestPlanReportById, + getOrCreateTestPlanReport, + updateTestPlanReport +} = require('../../models/services/TestPlanReportService'); +const { + getTestPlanVersionById +} = require('../../models/services/TestPlanVersionService'); +const { testResults } = require('../TestPlanRun'); +const populateData = require('../../services/PopulatedData/populateData'); +const scenariosResolver = require('../Test/scenariosResolver'); +const { + createTestPlanRun +} = require('../../models/services/TestPlanRunService'); +const { findOrCreateTestResult } = require('../TestPlanRunOperations'); +const { submitTestResult, saveTestResult } = require('../TestResultOperations'); + +const compareTestContent = (currentTests, newTests) => { + const hashTest = test => hash(omit(test, ['id'])); + const hashTests = tests => { + return Object.fromEntries(tests.map(test => [hashTest(test), test])); + }; + + 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 + }; + } + ) + }; +}; + +const updateTestPlanReportTestPlanVersionResolver = async ( + { parentContext: { id: testPlanReportId } }, + { input }, // { testPlanVersionId, atId, browserId } + context +) => { + const { user } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + const { testPlanVersionId: newTestPlanVersionId, atId } = input; + + // [SECTION START]: Preparing data to be worked with in a similar way to TestPlanUpdaterModal + const newTestPlanVersionData = ( + await getTestPlanVersionById(newTestPlanVersionId) + ).toJSON(); + 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) + ).toJSON(); + + for (let i = 0; i < currentTestPlanReport.testPlanRuns.length; i++) { + const testPlanRun = currentTestPlanReport.testPlanRuns[i]; + const { testPlanRun: populatedTestPlanRun } = await populateData( + { testPlanRunId: testPlanRun.id }, + { context } + ); + + testPlanRun.testResults = await testResults( + populatedTestPlanRun.toJSON(), + 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(input); + + const candidatePhaseReachedAt = + currentTestPlanReport.candidatePhaseReachedAt; + const recommendedPhaseReachedAt = + currentTestPlanReport.recommendedPhaseReachedAt; + const recommendedPhaseTargetDate = + currentTestPlanReport.recommendedPhaseTargetDate; + const vendorReviewStatus = currentTestPlanReport.vendorReviewStatus; + + await updateTestPlanReport(foundOrCreatedTestPlanReport.id, { + candidatePhaseReachedAt, + recommendedPhaseReachedAt, + recommendedPhaseTargetDate, + vendorReviewStatus + }); + + 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 + }); + + 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, user } + ); + + const copiedTestResultInput = copyTestResult( + testResultSkeleton, + testResult + ); + + let savedData; + if (testResult.completedAt) { + savedData = await submitTestResult( + { parentContext: { id: copiedTestResultInput.id } }, + { input: copiedTestResultInput }, + { ...context, user } + ); + } else { + savedData = await saveTestResult( + { parentContext: { id: copiedTestResultInput.id } }, + { input: copiedTestResultInput }, + { ...context, user } + ); + } + 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 }); +}; + +module.exports = updateTestPlanReportTestPlanVersionResolver; diff --git a/server/resolvers/TestPlanVersion/index.js b/server/resolvers/TestPlanVersion/index.js index 9a0afa71f..d281e24c0 100644 --- a/server/resolvers/TestPlanVersion/index.js +++ b/server/resolvers/TestPlanVersion/index.js @@ -1,11 +1,15 @@ -const testPlan = require('./testPlanVersionTestPlanResolver'); +const testPlan = require('./testPlanResolver'); const gitMessage = require('./gitMessageResolver'); const tests = require('./testsResolver'); +const testPlanReports = require('./testPlanReportsResolver'); +const recommendedPhaseTargetDate = require('./recommendedPhaseTargetDateResolver'); const TestPlanVersion = { testPlan, gitMessage, - tests + tests, + testPlanReports, + recommendedPhaseTargetDate }; module.exports = TestPlanVersion; diff --git a/server/resolvers/TestPlanVersion/recommendedPhaseTargetDateResolver.js b/server/resolvers/TestPlanVersion/recommendedPhaseTargetDateResolver.js new file mode 100644 index 000000000..cea1b92fb --- /dev/null +++ b/server/resolvers/TestPlanVersion/recommendedPhaseTargetDateResolver.js @@ -0,0 +1,15 @@ +const recommendedPhaseTargetDateResolver = ({ + candidatePhaseReachedAt, + recommendedPhaseTargetDate +}) => { + // Dependent on working mode and role of user as outlined at: + // https://github.com/w3c/aria-at/wiki/Working-Mode + if (!candidatePhaseReachedAt) return null; + if (recommendedPhaseTargetDate) return recommendedPhaseTargetDate; + + const targetDate = new Date(candidatePhaseReachedAt); + targetDate.setDate(candidatePhaseReachedAt.getDate() + 180); + return targetDate; +}; + +module.exports = recommendedPhaseTargetDateResolver; diff --git a/server/resolvers/TestPlanVersion/testPlanReportsResolver.js b/server/resolvers/TestPlanVersion/testPlanReportsResolver.js new file mode 100644 index 000000000..810cbbf18 --- /dev/null +++ b/server/resolvers/TestPlanVersion/testPlanReportsResolver.js @@ -0,0 +1,32 @@ +const { + getTestPlanReports +} = require('../../models/services/TestPlanReportService'); + +const testPlanReportsResolver = async ( + { id: testPlanVersionId }, + { isFinal } +) => { + const where = { + testPlanVersionId + }; + + 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/TestPlanVersion/testPlanVersionTestPlanResolver.js b/server/resolvers/TestPlanVersion/testPlanResolver.js similarity index 83% rename from server/resolvers/TestPlanVersion/testPlanVersionTestPlanResolver.js rename to server/resolvers/TestPlanVersion/testPlanResolver.js index ec1f81112..528ef440b 100644 --- a/server/resolvers/TestPlanVersion/testPlanVersionTestPlanResolver.js +++ b/server/resolvers/TestPlanVersion/testPlanResolver.js @@ -2,12 +2,7 @@ const { getTestPlanById } = require('../../models/services/TestPlanVersionService'); -const testPlanVersionTestPlanResolver = async ( - testPlanVersion, - args, - cotext, - info -) => { +const testPlanResolver = async (testPlanVersion, args, context, info) => { const requestedFields = info?.fieldNodes[0] && info.fieldNodes[0].selectionSet.selections.map( @@ -33,4 +28,4 @@ const testPlanVersionTestPlanResolver = async ( }; }; -module.exports = testPlanVersionTestPlanResolver; +module.exports = testPlanResolver; diff --git a/server/resolvers/TestPlanVersionOperations/index.js b/server/resolvers/TestPlanVersionOperations/index.js new file mode 100644 index 000000000..50bb64508 --- /dev/null +++ b/server/resolvers/TestPlanVersionOperations/index.js @@ -0,0 +1,7 @@ +const updatePhase = require('./updatePhaseResolver'); +const updateRecommendedPhaseTargetDate = require('./updateRecommendedPhaseTargetDateResolver'); + +module.exports = { + updatePhase, + updateRecommendedPhaseTargetDate +}; diff --git a/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js new file mode 100644 index 000000000..564850198 --- /dev/null +++ b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js @@ -0,0 +1,577 @@ +const { AuthenticationError } = require('apollo-server'); +const { + updateTestPlanReport, + getTestPlanReports, + getOrCreateTestPlanReport, + removeTestPlanReport +} = require('../../models/services/TestPlanReportService'); +const conflictsResolver = require('../TestPlanReport/conflictsResolver'); +const finalizedTestResultsResolver = require('../TestPlanReport/finalizedTestResultsResolver'); +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 } }, + { + phase, + candidatePhaseReachedAt, + recommendedPhaseTargetDate, + testPlanVersionDataToIncludeId + }, + context +) => { + const { user } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + // Immediately deprecate version without further checks + if (phase === 'DEPRECATED') { + await updateTestPlanVersion(testPlanVersionId, { + phase, + deprecatedAt: new Date() + }); + return populateData({ testPlanVersionId }, { context }); + } + + let testPlanVersionDataToInclude; + let testPlanReportsDataToIncludeId = []; + let createdTestPlanReportIdsFromOldResults = []; + + // The testPlanVersion being updated + const testPlanVersion = await getTestPlanVersionById(testPlanVersionId); + + // 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 + }; + testPlanReports = await getTestPlanReports( + null, + whereTestPlanVersion, + null, + null, + null, + null, + null, + null, + { + order: [['createdAt', 'desc']] + } + ); + + // 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) { + // Found odd instances of rowNumber being an int instead of being how it + // currently is; imported as a string + // Ensuring proper hashes are done here + const testHash = hashTest({ + ...testPlanVersionTest, + rowNumber: String(testPlanVersionTest.rowNumber) + }); + + if (keptTestIds[testHash]) continue; + + for (const testPlanVersionDataToIncludeTest of testPlanVersionDataToInclude.tests) { + const testDataToIncludeHash = hashTest({ + ...testPlanVersionDataToIncludeTest, + rowNumber: String( + testPlanVersionDataToIncludeTest.rowNumber + ) + }); + + 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: Return information on which tests cannot be preserved + } + }); + } + + if (Object.keys(testResultsToSave).length) { + const [createdTestPlanReport] = + await getOrCreateTestPlanReport({ + testPlanVersionId, + atId: testPlanReportDataToInclude.atId, + browserId: testPlanReportDataToInclude.browserId + }); + + createdTestPlanReportIdsFromOldResults.push( + createdTestPlanReport.id + ); + + 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); + } + + // Update TestPlanRun test results to be used in metrics evaluation + // afterward + await updateTestPlanRun(createdTestPlanRun.id, { + testResults + }); + + // Update metrics for TestPlanReport + const { testPlanReport: populatedTestPlanReport } = + await populateData( + { testPlanReportId: createdTestPlanReport.id }, + { context } + ); + + const runnableTests = runnableTestsResolver( + populatedTestPlanReport + ); + let updateParams = {}; + + // Mark the report as final if previously was on the TestPlanVersion being + // deprecated + if (testPlanReportDataToInclude.markedFinalAt) + updateParams = { markedFinalAt: new Date() }; + + // Calculate the metrics (happens if updating to DRAFT) + const conflicts = await conflictsResolver( + populatedTestPlanReport, + null, + context + ); + + if (conflicts.length > 0) { + // Then no chance to have finalized reports, and means it hasn't been + // marked as final yet + updateParams = { + ...updateParams, + metrics: { + ...populatedTestPlanReport.metrics, + conflictsCount: conflicts.length + } + }; + } else { + const finalizedTestResults = + await finalizedTestResultsResolver( + populatedTestPlanReport, + null, + context + ); + + if ( + !finalizedTestResults || + !finalizedTestResults.length + ) { + // Just update with current { markedFinalAt } if available + updateParams = { + ...updateParams, + metrics: { + ...populatedTestPlanReport.metrics + } + }; + } else { + const metrics = getMetrics({ + testPlanReport: { + ...populatedTestPlanReport, + finalizedTestResults, + runnableTests + } + }); + + updateParams = { + ...updateParams, + metrics: { + ...populatedTestPlanReport.metrics, + ...metrics + } + }; + } + } + + await updateTestPlanReport( + populatedTestPlanReport.id, + updateParams + ); + } + } + } + } + + 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 there is at least one report that wasn't created by the old reports then do the exception + // check + if ( + testPlanReports.some( + ({ id }) => !createdTestPlanReportIdsFromOldResults.includes(id) + ) + ) { + if ( + !testPlanReports.some(({ markedFinalAt }) => markedFinalAt) && + (phase === 'CANDIDATE' || phase === 'RECOMMENDED') + ) { + // Throw away newly created test plan reports if exception was hit + if (createdTestPlanReportIdsFromOldResults.length) + for (const createdTestPlanReportId of createdTestPlanReportIdsFromOldResults) { + await removeTestPlanReport(createdTestPlanReportId); + } + + // 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') { + const reportsByAtAndBrowser = {}; + + testPlanReports + // Only check for reports which have been marked as final + .filter(testPlanReport => !!testPlanReport.markedFinalAt) + .forEach(testPlanReport => { + const { at, browser } = testPlanReport; + if (!reportsByAtAndBrowser[at.id]) { + reportsByAtAndBrowser[at.id] = {}; + } + + 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}` + ); + } + }); + }); + + if (missingAtBrowserCombinations.length) { + // Throw away newly created test plan reports if exception was hit + if (createdTestPlanReportIdsFromOldResults.length) + for (const createdTestPlanReportId of createdTestPlanReportIdsFromOldResults) { + await removeTestPlanReport(createdTestPlanReportId); + } + + throw new Error( + `Cannot set phase to ${phase.toLowerCase()} because the following` + + ` required reports have not been collected or finalized:` + + ` ${missingAtBrowserCombinations.join(', ')}.` + ); + } + } + + for (const testPlanReport of testPlanReports) { + const runnableTests = runnableTestsResolver(testPlanReport); + let updateParams = {}; + + const isReportCreatedFromOldResults = + createdTestPlanReportIdsFromOldResults.includes(testPlanReport.id); + + if (phase === 'DRAFT') { + const conflicts = await conflictsResolver( + testPlanReport, + null, + context + ); + + updateParams = { + metrics: { + ...testPlanReport.metrics, + conflictsCount: conflicts.length + } + }; + + // Nullify markedFinalAt if not using old result + if (!isReportCreatedFromOldResults) + updateParams = { ...updateParams, markedFinalAt: null }; + + await updateTestPlanReport(testPlanReport.id, updateParams); + } + + const shouldThrowErrorIfFound = + (phase === 'CANDIDATE' || phase === 'RECOMMENDED') && + isReportCreatedFromOldResults + ? false + : testPlanReport.markedFinalAt; + + if (shouldThrowErrorIfFound) { + const conflicts = await conflictsResolver( + testPlanReport, + null, + context + ); + if (conflicts.length > 0) { + // Throw away newly created test plan reports if exception was hit + if (createdTestPlanReportIdsFromOldResults.length) + for (const createdTestPlanReportId of createdTestPlanReportIdsFromOldResults) { + await removeTestPlanReport(createdTestPlanReportId); + } + + throw new Error( + 'Cannot update test plan report due to conflicts' + ); + } + + const finalizedTestResults = await finalizedTestResultsResolver( + testPlanReport, + null, + context + ); + + if (!finalizedTestResults || !finalizedTestResults.length) { + // Throw away newly created test plan reports if exception was hit + if (createdTestPlanReportIdsFromOldResults.length) + for (const createdTestPlanReportId of createdTestPlanReportIdsFromOldResults) { + await removeTestPlanReport(createdTestPlanReportId); + } + + 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(testPlanReport.id, 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, { + phase: 'DEPRECATED', + deprecatedAt: new Date() + }); + + await updateTestPlanVersion(testPlanVersionId, updateParams); + return populateData({ testPlanVersionId }, { context }); +}; + +module.exports = updatePhaseResolver; diff --git a/server/resolvers/TestPlanVersionOperations/updateRecommendedPhaseTargetDateResolver.js b/server/resolvers/TestPlanVersionOperations/updateRecommendedPhaseTargetDateResolver.js new file mode 100644 index 000000000..87557ab20 --- /dev/null +++ b/server/resolvers/TestPlanVersionOperations/updateRecommendedPhaseTargetDateResolver.js @@ -0,0 +1,24 @@ +const { AuthenticationError } = require('apollo-server'); +const populateData = require('../../services/PopulatedData/populateData'); +const { + updateTestPlanVersion +} = require('../../models/services/TestPlanVersionService'); + +const updateRecommendedPhaseTargetDateResolver = async ( + { parentContext: { id: testPlanVersionId } }, + { recommendedPhaseTargetDate }, + context +) => { + const { user } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + await updateTestPlanVersion(testPlanVersionId, { + recommendedPhaseTargetDate + }); + + return populateData({ testPlanVersionId }, { context }); +}; + +module.exports = updateRecommendedPhaseTargetDateResolver; diff --git a/server/resolvers/TestResultOperations/saveTestResultCommon.js b/server/resolvers/TestResultOperations/saveTestResultCommon.js index a469c2c16..6f61be9a4 100644 --- a/server/resolvers/TestResultOperations/saveTestResultCommon.js +++ b/server/resolvers/TestResultOperations/saveTestResultCommon.js @@ -48,8 +48,8 @@ const saveTestResultCommon = async ({ // Some clients might send an otherUnexpectedBehaviorText for unexpectedBehaviors // that are not "OTHER". As long as the otherUnexpectedBehaviorText is null or undefined, // the best course of action is probably to allow it, but not save it to the database. - newTestResult.scenarioResults.forEach(scenarioResult => { - scenarioResult.unexpectedBehaviors.forEach(unexpectedBehavior => { + newTestResult.scenarioResults?.forEach(scenarioResult => { + scenarioResult.unexpectedBehaviors?.forEach(unexpectedBehavior => { if ( unexpectedBehavior.id !== 'OTHER' && unexpectedBehavior.otherUnexpectedBehaviorText == null 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/index.js b/server/resolvers/index.js index 54fa5be62..a8fad8916 100644 --- a/server/resolvers/index.js +++ b/server/resolvers/index.js @@ -17,17 +17,20 @@ const mutateBrowser = require('./mutateBrowserResolver'); const mutateTestPlanReport = require('./mutateTestPlanReportResolver'); const mutateTestPlanRun = require('./mutateTestPlanRunResolver'); const mutateTestResult = require('./mutateTestResultResolver'); +const mutateTestPlanVersion = require('./mutateTestPlanVersionResolver'); const updateMe = require('./updateMe'); const populateData = require('./populateDataResolver'); const User = require('./User'); const AtOperations = require('./AtOperations'); const AtVersionOperations = require('./AtVersionOperations'); const BrowserOperations = require('./BrowserOperations'); +const TestPlan = require('./TestPlan'); const TestPlanVersion = require('./TestPlanVersion'); const TestPlanReport = require('./TestPlanReport'); const TestPlanReportOperations = require('./TestPlanReportOperations'); const TestPlanRunOperations = require('./TestPlanRunOperations'); const TestResultOperations = require('./TestResultOperations'); +const TestPlanVersionOperations = require('./TestPlanVersionOperations'); const TestPlanRun = require('./TestPlanRun'); const Test = require('./Test'); const ScenarioResult = require('./ScenarioResult'); @@ -54,6 +57,7 @@ const resolvers = { testPlanReport: mutateTestPlanReport, testPlanRun: mutateTestPlanRun, testResult: mutateTestResult, + testPlanVersion: mutateTestPlanVersion, findOrCreateTestPlanReport, updateMe, addViewer @@ -62,6 +66,7 @@ const resolvers = { AtVersionOperations, BrowserOperations, User, + TestPlan, TestPlanVersion, TestPlanReport, TestPlanRun, @@ -69,7 +74,8 @@ const resolvers = { ScenarioResult, TestPlanReportOperations, TestPlanRunOperations, - TestResultOperations + TestResultOperations, + TestPlanVersionOperations }; module.exports = resolvers; diff --git a/server/resolvers/mutateTestPlanVersionResolver.js b/server/resolvers/mutateTestPlanVersionResolver.js new file mode 100644 index 000000000..13ab8d883 --- /dev/null +++ b/server/resolvers/mutateTestPlanVersionResolver.js @@ -0,0 +1,5 @@ +const mutateTestPlanVersionResolver = (_, { id }) => { + return { parentContext: { id } }; +}; + +module.exports = mutateTestPlanVersionResolver; 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/resolvers/testPlanVersionsResolver.js b/server/resolvers/testPlanVersionsResolver.js index 0b9600190..8f385e8af 100644 --- a/server/resolvers/testPlanVersionsResolver.js +++ b/server/resolvers/testPlanVersionsResolver.js @@ -4,7 +4,10 @@ const { const retrieveAttributes = require('./helpers/retrieveAttributes'); const { TEST_PLAN_VERSION_ATTRIBUTES } = require('../models/services/helpers'); -const testPlanVersionsResolver = async (root, args, context, info) => { +const testPlanVersionsResolver = async (_, { phases }, context, info) => { + const where = {}; + if (phases) where.phase = phases; + const { attributes: testPlanVersionAttributes } = retrieveAttributes( 'testPlanVersion', TEST_PLAN_VERSION_ATTRIBUTES, @@ -13,7 +16,7 @@ const testPlanVersionsResolver = async (root, args, context, info) => { return getTestPlanVersions( null, - {}, + where, testPlanVersionAttributes, [], [], @@ -22,6 +25,7 @@ const testPlanVersionsResolver = async (root, args, context, info) => { [], { order: [ + ['candidatePhaseReachedAt', 'desc'], ['updatedAt', 'desc'], ['title', 'asc'], ['directory', 'asc'] diff --git a/server/scripts/import-tests/index.js b/server/scripts/import-tests/index.js index 8368c1a63..b36f9e7b7 100644 --- a/server/scripts/import-tests/index.js +++ b/server/scripts/import-tests/index.js @@ -7,7 +7,8 @@ const spawn = require('cross-spawn'); const { At } = require('../../models'); const { createTestPlanVersion, - getTestPlanVersions + getTestPlanVersions, + updateTestPlanVersion } = require('../../models/services/TestPlanVersionService'); const { getTestPlans, @@ -148,6 +149,30 @@ const importTestPlanVersions = async () => { testPlanId = newTestPlan.dataValues.id; } + // Check if any TestPlanVersions exist for the directory and is currently in RD, and set it + // to DEPRECATED + const testPlanVersionsToDeprecate = await getTestPlanVersions('', { + phase: 'RD', + directory + }); + if (testPlanVersionsToDeprecate.length) { + for (const testPlanVersionToDeprecate of testPlanVersionsToDeprecate) { + if ( + new Date(testPlanVersionToDeprecate.updatedAt) < updatedAt + ) { + // Set the deprecatedAt time to a couple seconds less than the updatedAt date. + // Deprecations happen slightly before update during normal app operations. + // This is to maintain correctness and any app sorts issues + const deprecatedAt = new Date(updatedAt); + deprecatedAt.setSeconds(deprecatedAt.getSeconds() - 60); + await updateTestPlanVersion(testPlanVersionToDeprecate.id, { + phase: 'DEPRECATED', + deprecatedAt + }); + } + } + } + await createTestPlanVersion({ id: testPlanVersionId, title, diff --git a/server/scripts/populate-test-data/index.js b/server/scripts/populate-test-data/index.js index 133280392..9c285042e 100644 --- a/server/scripts/populate-test-data/index.js +++ b/server/scripts/populate-test-data/index.js @@ -68,6 +68,11 @@ const populateTestDatabase = async () => { 'completeAndFailingDueToIncorrectAssertions', 'completeAndFailingDueToNoOutputAssertions', 'completeAndFailingDueToUnexpectedBehaviors', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', 'completeAndPassing' ]); @@ -102,6 +107,60 @@ const populateTestDatabase = async () => { 'completeAndPassing' ]); + await populateFakeTestResults(8, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(9, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(10, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing' + ]); + + await populateFakeTestResults(11, [ + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + 'completeAndPassing', + '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 512e0c849..a5147b4c9 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,13 +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", "candidateStatusReachedAt", "recommendedStatusReachedAt", "vendorReviewStatus") VALUES (3, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 1, 2, '2022-07-06', '2023-01-02', 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "candidateStatusReachedAt", "recommendedStatusReachedAt", "vendorReviewStatus") VALUES (4, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 2, 1, '2022-07-06', '2023-01-02', 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "candidateStatusReachedAt", "recommendedStatusReachedAt", "vendorReviewStatus") VALUES (5, 'CANDIDATE', get_test_plan_version_id(text 'Modal Dialog Example'), '2021-05-14 14:18:23.602-05', 3, 3, '2022-07-06', '2023-01-02', 'READY'); -INSERT INTO "TestPlanReport" (id, "status", "testPlanVersionId", "createdAt", "atId", "browserId", "candidateStatusReachedAt", "recommendedStatusReachedAt", "vendorReviewStatus") VALUES (6, 'CANDIDATE', get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', 3, 3, '2022-07-06', '2023-01-02', '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", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (12, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '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 (13, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)'), '2021-05-14 14:18:23.602-05', '2022-07-07', 2, 2, 'READY'); + +-- +-- Data for Name: TestPlanVersion; Type: TABLE DATA; Schema: public; Owner: atr +-- + +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 @@ -101,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/services/GithubService.js b/server/services/GithubService.js index a772e929f..84b04b351 100644 --- a/server/services/GithubService.js +++ b/server/services/GithubService.js @@ -1,7 +1,8 @@ const axios = require('axios'); -const NodeCache = require('node-cache'); +const staleWhileRevalidate = require('../util/staleWhileRevalidate'); const { + ENVIRONMENT, GITHUB_GRAPHQL_SERVER, GITHUB_OAUTH_SERVER, GITHUB_CLIENT_ID, @@ -11,6 +12,11 @@ const { GITHUB_TEAM_QUERY } = process.env; +const GITHUB_ISSUES_API_URL = + ENVIRONMENT === 'production' + ? 'https://api.github.com/repos/w3c/aria-at' + : 'https://api.github.com/repos/bocoup/aria-at'; + const permissionScopes = [ // Not currently used, but this permissions scope will allow us to query for // the user's private email address via the REST API in the future (Note @@ -23,74 +29,44 @@ const permissionScopes = [ ]; const permissionScopesURI = encodeURI(permissionScopes.join(' ')); const graphQLEndpoint = `${GITHUB_GRAPHQL_SERVER}/graphql`; -const nodeCache = new NodeCache(); -const CACHE_MINUTES = 1; -const constructIssuesRequest = async ({ - ats = ['jaws', 'nvda', 'vo'], - page = 1 -}) => { - const issuesEndpoint = `https://api.github.com/repos/w3c/aria-at/issues?labels=app,candidate-review&per_page=100`; - const url = `${issuesEndpoint}&page=${page}`; - const auth = { - username: GITHUB_CLIENT_ID, - password: GITHUB_CLIENT_SECRET - }; - const response = await axios.get(url, { auth }); - // https://docs.github.com/en/rest/issues/issues#list-repository-issues - // Filter out Pull Requests. GitHub's REST API v3 also considers every - // pull request an issue. - const issues = response.data.filter(data => !data.pull_request); - const headersLink = response.headers.link; +const getAllIssues = async () => { + let currentResults = []; + let page = 1; - let resultsByAt = { jaws: [], nvda: [], vo: [] }; - if (issues.length) { - resultsByAt = { - jaws: ats.includes('jaws') - ? [ - ...issues.filter(issue => - issue.labels.map(label => label.name).includes('jaws') - ) - ] - : [], - nvda: ats.includes('nvda') - ? [ - ...issues.filter(issue => - issue.labels.map(label => label.name).includes('nvda') - ) - ] - : [], - vo: ats.includes('vo') - ? [ - ...issues.filter(issue => - issue.labels.map(label => label.name).includes('vo') - ) - ] - : [] + // eslint-disable-next-line no-constant-condition + while (true) { + const issuesEndpoint = `${GITHUB_ISSUES_API_URL}/issues?state=all&per_page=100`; + const url = `${issuesEndpoint}&page=${page}`; + const auth = { + username: GITHUB_CLIENT_ID, + password: GITHUB_CLIENT_SECRET }; - } + const response = await axios.get(url, { auth }); - // Check if additional pages exist - if (headersLink && headersLink.includes('rel="next"')) { - // Get result from other pages - const additionalResultsByAt = await constructIssuesRequest({ - ats, - page: page + 1 - }); - resultsByAt = { - jaws: ats.includes('jaws') - ? [...resultsByAt.jaws, ...additionalResultsByAt.jaws] - : [], - nvda: ats.includes('nvda') - ? [...resultsByAt.nvda, ...additionalResultsByAt.nvda] - : [], - vo: ats.includes('vo') - ? [...resultsByAt.vo, ...additionalResultsByAt.vo] - : [] - }; + const issues = response.data + // https://docs.github.com/en/rest/issues/issues#list-repository-issues + // Filter out Pull Requests. GitHub's REST API v3 also considers every + // pull request an issue. + .filter(data => !data.pull_request) + // Our issue API should only return issues that were originally + // created by the app, indicated by the presence of metadata + // hidden in a comment + .filter(data => data.body.includes('ARIA_AT_APP_ISSUE_DATA')); + + currentResults = [...currentResults, ...issues]; + + const hasMoreResults = response.headers.link?.includes('rel="next"'); + + if (hasMoreResults) { + page += 1; + continue; + } + + break; } - return resultsByAt; + return currentResults; }; module.exports = { @@ -165,29 +141,7 @@ module.exports = { return isMember; }, - async getCandidateReviewIssuesByAt({ cacheId, atName }) { - const cacheResult = nodeCache.get(cacheId); - - let atKey = ''; - switch (atName) { - case 'JAWS': - atKey = 'jaws'; - break; - case 'NVDA': - atKey = 'nvda'; - break; - case 'VoiceOver for macOS': - atKey = 'vo'; - break; - } - - if (!cacheResult) { - const result = await constructIssuesRequest({ - ats: [atKey] - }); - nodeCache.set(cacheId, result, CACHE_MINUTES * 60); - return result[atKey]; - } - return cacheResult[atKey]; - } + getAllIssues: staleWhileRevalidate(getAllIssues, { + millisecondsUntilStale: 10000 /* 10 seconds */ + }) }; diff --git a/server/services/PopulatedData/populateData.js b/server/services/PopulatedData/populateData.js index 06e8dfe31..e03885e54 100644 --- a/server/services/PopulatedData/populateData.js +++ b/server/services/PopulatedData/populateData.js @@ -11,7 +11,7 @@ const { const { decodeLocationOfDataId } = require('./locationOfDataId'); const testsResolver = require('../../resolvers/TestPlanVersion/testsResolver'); const testResultsResolver = require('../../resolvers/TestPlanRun/testResultsResolver'); -const testPlanVersionTestPlanResolver = require('../../resolvers/TestPlanVersion/testPlanVersionTestPlanResolver'); +const testPlanVersionTestPlanResolver = require('../../resolvers/TestPlanVersion/testPlanResolver'); /** * diff --git a/server/tests/integration/dataManagement.test.js b/server/tests/integration/dataManagement.test.js new file mode 100644 index 000000000..7893a9cda --- /dev/null +++ b/server/tests/integration/dataManagement.test.js @@ -0,0 +1,709 @@ +const { gql } = require('apollo-server'); +const dbCleaner = require('../util/db-cleaner'); +const { query, mutate } = require('../util/graphql-test-utilities'); +const db = require('../../models'); + +beforeAll(() => { + jest.setTimeout(20000); +}); + +afterAll(async () => { + // Closing the DB connection allows Jest to exit successfully. + await db.sequelize.close(); +}); + +describe('data management', () => { + const testPlanVersionsQuery = () => { + return query(gql` + query { + testPlanVersions(phases: [RD, CANDIDATE]) { + id + phase + gitSha + testPlan { + directory + } + testPlanReports { + id + at { + id + } + browser { + id + } + draftTestPlanRuns { + testResults { + id + completedAt + test { + id + rowNumber + title + ats { + id + name + } + atMode + scenarios { + id + commands { + id + text + } + } + assertions { + id + priority + text + } + } + scenarioResults { + output + assertionResults { + id + assertion { + text + } + passed + } + scenario { + id + commands { + id + text + } + } + } + } + } + } + } + } + `); + }; + + const updateVersionToPhaseQuery = ( + testPlanVersionId, + testPlanVersionDataToIncludeId, + phase + ) => { + return mutate(gql` + mutation { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase( + phase: ${phase} + testPlanVersionDataToIncludeId: ${testPlanVersionDataToIncludeId} + ) { + testPlanVersion { + phase + testPlanReports { + id + at { + id + } + browser { + id + } + draftTestPlanRuns { + testResults { + id + completedAt + test { + id + rowNumber + title + ats { + id + name + } + atMode + scenarios { + id + commands { + id + text + } + } + assertions { + id + priority + text + } + } + scenarioResults { + output + assertionResults { + id + assertion { + text + } + passed + } + scenario { + id + commands { + id + text + } + } + } + } + } + } + } + } + } + } + `); + }; + + const countCompletedTests = testPlanReports => { + return testPlanReports.reduce((acc, testPlanReport) => { + return ( + acc + + testPlanReport.draftTestPlanRuns[0]?.testResults.reduce( + (acc, testResult) => + testResult.completedAt ? acc + 1 : acc, + 0 + ) || 0 + ); + }, 0); + }; + + it('can set test plan version to candidate and recommended', async () => { + await dbCleaner(async () => { + 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 { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: DRAFT) { + testPlanVersion { + phase + } + } + } + } + `); + + const previous = await query(gql` + query { + testPlanVersion(id: ${testPlanVersionId}) { + phase + testPlanReports { + id + } + } + } + `); + const previousPhase = previous.testPlanVersion.phase; + const previousPhaseTestPlanReportId = + previous.testPlanVersion.testPlanReports[0].id; + + // Need to approve at least one of the associated reports + await mutate(gql` + mutation { + testPlanReport(id: ${previousPhaseTestPlanReportId}) { + markAsFinal { + testPlanReport { + id + markedFinalAt + } + } + } + } + `); + + // Check to see that the testPlanVersion cannot be updated until the reports have been + // finalized + await expect(() => { + return mutate(gql` + mutation { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: CANDIDATE) { + testPlanVersion { + phase + } + } + } + } + `); + }).rejects.toThrow( + /Cannot set phase to candidate because the following required reports have not been collected or finalized:/i + ); + + const testPlanReportsToMarkAsFinalResult = await query(gql` + query { + testPlanReports(testPlanVersionId: ${testPlanVersionId}) { + id + } + } + `); + + for (const testPlanReport of testPlanReportsToMarkAsFinalResult.testPlanReports) { + await mutate(gql` + mutation { + testPlanReport(id: ${testPlanReport.id}) { + markAsFinal { + testPlanReport { + id + markedFinalAt + } + } + } + } + + `); + } + + const candidateResult = await mutate(gql` + mutation { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: CANDIDATE) { + testPlanVersion { + phase + } + } + } + } + `); + const candidateResultPhase = + candidateResult.testPlanVersion.updatePhase.testPlanVersion + .phase; + + const recommendedResult = await mutate(gql` + mutation { + testPlanVersion(id: ${testPlanVersionId}) { + updatePhase(phase: RECOMMENDED) { + testPlanVersion { + phase + } + } + } + } + `); + const recommendedResultPhase = + recommendedResult.testPlanVersion.updatePhase.testPlanVersion + .phase; + + expect(candidateTestPlanVersion.phase).toBe('CANDIDATE'); + expect(previousPhase).not.toBe('CANDIDATE'); + expect(candidateResultPhase).toBe('CANDIDATE'); + expect(recommendedResultPhase).toBe('RECOMMENDED'); + }); + }); + + it('updates test plan version and copies results from previous version reports', async () => { + await dbCleaner(async () => { + const testPlanVersions = await testPlanVersionsQuery(); + + // This has reports for JAWS + Chrome, NVDA + Chrome, VO + Safari + additional + // non-required reports + const [oldModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'CANDIDATE' + ); + const oldModalDialogVersionTestPlanReports = + oldModalDialogVersion.testPlanReports; + const oldModalDialogVersionTestPlanReportsCount = + oldModalDialogVersionTestPlanReports.length; + + // Get JAWS-specific tests count for old version + const oldModalDialogVersionJAWSCompletedTestsCount = + countCompletedTests( + oldModalDialogVersionTestPlanReports.filter( + el => el.at.id == 1 + ) + ); + + // Get NVDA-specific tests count for old version + const oldModalDialogVersionNVDACompletedTestsCount = + countCompletedTests( + oldModalDialogVersionTestPlanReports.filter( + el => el.at.id == 2 + ) + ); + + // Get VO-specific tests count for old version + const oldModalDialogVersionVOCompletedTestsCount = + countCompletedTests( + oldModalDialogVersionTestPlanReports.filter( + el => el.at.id == 3 + ) + ); + + const [newModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'RD' + ); + const newModalDialogVersionTestPlanReportsInRDCount = + newModalDialogVersion.testPlanReports.length; + + const { testPlanVersion: newModalDialogVersionInDraft } = + await mutate(gql` + mutation { + testPlanVersion(id: ${newModalDialogVersion.id}) { + updatePhase(phase: DRAFT) { + testPlanVersion { + phase + testPlanReports { + id + } + } + } + } + } + `); + const newModalDialogVersionTestPlanReportsInDraftCount = + newModalDialogVersionInDraft.updatePhase.testPlanVersion + .testPlanReports.length; + + const { testPlanVersion: newModalDialogVersionInCandidate } = + await updateVersionToPhaseQuery( + newModalDialogVersion.id, + oldModalDialogVersion.id, + 'CANDIDATE' + ); + const newModalDialogVersionTestPlanReportsInCandidate = + newModalDialogVersionInCandidate.updatePhase.testPlanVersion + .testPlanReports; + const newModalDialogVersionTestPlanReportsInCandidateCount = + newModalDialogVersionTestPlanReportsInCandidate.length; + + // Get JAWS-specific tests count for new version + const newModalDialogVersionJAWSCompletedTestsInCandidateCount = + countCompletedTests( + newModalDialogVersionTestPlanReportsInCandidate.filter( + el => el.at.id == 1 + ) + ); + + // Get NVDA-specific tests count for new version + const newModalDialogVersionNVDACompletedTestsInCandidateCount = + countCompletedTests( + newModalDialogVersionTestPlanReportsInCandidate.filter( + el => el.at.id == 2 + ) + ); + + // Get VO-specific tests count for new version + const newModalDialogVersionVOCompletedTestsInCandidateCount = + countCompletedTests( + newModalDialogVersionTestPlanReportsInCandidate.filter( + el => el.at.id == 3 + ) + ); + + // https://github.com/w3c/aria-at/compare/5fe7afd82fe51c185b8661276105190a59d47322..d0e16b42179de6f6c070da2310e99de837c71215 + // Modal Dialog was updated to show have differences between several NVDA and JAWS tests + // There are no changes for the VO tests + expect(oldModalDialogVersion.gitSha).toBe( + '5fe7afd82fe51c185b8661276105190a59d47322' + ); + expect(newModalDialogVersion.gitSha).toBe( + 'd0e16b42179de6f6c070da2310e99de837c71215' + ); + + expect(oldModalDialogVersionTestPlanReportsCount).toBeGreaterThan( + 0 + ); + expect(newModalDialogVersionTestPlanReportsInRDCount).toBe(0); + expect(newModalDialogVersionTestPlanReportsInDraftCount).toEqual(0); + expect( + newModalDialogVersionTestPlanReportsInCandidateCount + ).toEqual(oldModalDialogVersionTestPlanReportsCount); + + expect( + oldModalDialogVersionJAWSCompletedTestsCount + ).toBeGreaterThan( + newModalDialogVersionJAWSCompletedTestsInCandidateCount + ); + expect( + oldModalDialogVersionNVDACompletedTestsCount + ).toBeGreaterThan( + newModalDialogVersionNVDACompletedTestsInCandidateCount + ); + expect(oldModalDialogVersionVOCompletedTestsCount).toEqual( + newModalDialogVersionVOCompletedTestsInCandidateCount + ); + }); + }); + + it('updates test plan version and copies all but one report from previous version', async () => { + await dbCleaner(async () => { + const testPlanVersions = await testPlanVersionsQuery(); + + // This has reports for JAWS + Chrome, NVDA + Chrome, VO + Safari + additional + // non-required reports + const [oldModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'CANDIDATE' + ); + const oldModalDialogVersionTestPlanReports = + oldModalDialogVersion.testPlanReports; + const oldModalDialogVersionTestPlanReportsCount = + oldModalDialogVersionTestPlanReports.length; + + // Get VO+Firefox-specific tests count for old version + const oldModalDialogVersionVOFirefoxCompletedTestsCount = + countCompletedTests( + oldModalDialogVersionTestPlanReports.filter( + el => el.at.id == 3 && el.browser.id == 1 + ) + ); + + const [newModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'RD' + ); + const newModalDialogVersionTestPlanReportsInRDCount = + newModalDialogVersion.testPlanReports.length; + + await mutate(gql` + mutation { + testPlanVersion(id: ${newModalDialogVersion.id}) { + updatePhase(phase: DRAFT) { + testPlanVersion { + phase + testPlanReports { + id + } + } + } + } + } + `); + + const { + findOrCreateTestPlanReport: { + populatedData: { + testPlanVersion: newModalDialogVersionInDraft + } + } + } = await mutate(gql` + mutation { + findOrCreateTestPlanReport(input: { + testPlanVersionId: ${newModalDialogVersion.id} + atId: 3 + browserId: 1 + }) { + populatedData { + testPlanReport { + id + at { + id + } + browser { + id + } + } + testPlanVersion { + id + phase + testPlanReports { + id + } + } + } + created { + locationOfData + } + } + } + `); + + const newModalDialogVersionTestPlanReportsInDraftCount = + newModalDialogVersionInDraft.testPlanReports.length; + + const { testPlanVersion: newModalDialogVersionInCandidate } = + await updateVersionToPhaseQuery( + newModalDialogVersion.id, + oldModalDialogVersion.id, + 'CANDIDATE' + ); + const newModalDialogVersionTestPlanReportsInCandidate = + newModalDialogVersionInCandidate.updatePhase.testPlanVersion + .testPlanReports; + const newModalDialogVersionTestPlanReportsInCandidateCount = + newModalDialogVersionTestPlanReportsInCandidate.length; + + // Get VO+Firefox-specific tests count for new version + const newModalDialogVersionVOFirefoxCompletedTestsInCandidateCount = + countCompletedTests( + newModalDialogVersionTestPlanReportsInCandidate.filter( + el => el.at.id == 3 && el.browser.id == 1 + ) + ); + + // https://github.com/w3c/aria-at/compare/5fe7afd82fe51c185b8661276105190a59d47322..d0e16b42179de6f6c070da2310e99de837c71215 + // Modal Dialog was updated to show have differences between several NVDA and JAWS tests + // There are no changes for the VO tests + expect(oldModalDialogVersion.gitSha).toBe( + '5fe7afd82fe51c185b8661276105190a59d47322' + ); + expect(newModalDialogVersion.gitSha).toBe( + 'd0e16b42179de6f6c070da2310e99de837c71215' + ); + + expect(oldModalDialogVersionTestPlanReportsCount).toBeGreaterThan( + 0 + ); + expect(newModalDialogVersionTestPlanReportsInRDCount).toBe(0); + expect(newModalDialogVersionTestPlanReportsInDraftCount).toEqual(1); + expect( + newModalDialogVersionTestPlanReportsInCandidateCount + ).toEqual(oldModalDialogVersionTestPlanReportsCount); + + expect( + oldModalDialogVersionVOFirefoxCompletedTestsCount + ).toBeGreaterThan( + newModalDialogVersionVOFirefoxCompletedTestsInCandidateCount + ); + expect( + newModalDialogVersionVOFirefoxCompletedTestsInCandidateCount + ).toEqual(0); + }); + }); + + it('updates test plan version but has new reports that are required and not yet marked as final', async () => { + await dbCleaner(async () => { + const testPlanVersions = await testPlanVersionsQuery(); + + // This has reports for JAWS + Chrome, NVDA + Chrome, VO + Safari + additional + // non-required reports + const [oldModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'CANDIDATE' + ); + const oldModalDialogVersionTestPlanReports = + oldModalDialogVersion.testPlanReports; + const oldModalDialogVersionTestPlanReportsCount = + oldModalDialogVersionTestPlanReports.length; + + // Get VO+Safari-specific tests count for old version + const oldModalDialogVersionVOSafariCompletedTestsCount = + countCompletedTests( + oldModalDialogVersionTestPlanReports.filter( + el => el.at.id == 3 && el.browser.id == 3 + ) + ); + + const [newModalDialogVersion] = + testPlanVersions.testPlanVersions.filter( + e => + e.testPlan.directory === 'modal-dialog' && + e.phase === 'RD' + ); + const newModalDialogVersionTestPlanReportsInRDCount = + newModalDialogVersion.testPlanReports.length; + + await mutate(gql` + mutation { + testPlanVersion(id: ${newModalDialogVersion.id}) { + updatePhase(phase: DRAFT) { + testPlanVersion { + phase + testPlanReports { + id + } + } + } + } + } + `); + + const { + findOrCreateTestPlanReport: { + populatedData: { + testPlanVersion: newModalDialogVersionInDraft + } + } + } = await mutate(gql` + mutation { + findOrCreateTestPlanReport(input: { + testPlanVersionId: ${newModalDialogVersion.id} + atId: 3 + browserId: 3 + }) { + populatedData { + testPlanReport { + id + at { + id + } + browser { + id + } + } + testPlanVersion { + id + phase + testPlanReports { + id + } + } + } + created { + locationOfData + } + } + } + `); + + const newModalDialogVersionTestPlanReportsInDraftCount = + newModalDialogVersionInDraft.testPlanReports.length; + + // A required report isn't marked as final, VO + Safari + await expect(() => { + return updateVersionToPhaseQuery( + newModalDialogVersion.id, + oldModalDialogVersion.id, + 'CANDIDATE' + ); + }).rejects.toThrow( + /Cannot set phase to candidate because the following required reports have not been collected or finalized:/i + ); + + // https://github.com/w3c/aria-at/compare/5fe7afd82fe51c185b8661276105190a59d47322..d0e16b42179de6f6c070da2310e99de837c71215 + // Modal Dialog was updated to show have differences between several NVDA and JAWS tests + // There are no changes for the VO tests + expect(oldModalDialogVersion.gitSha).toBe( + '5fe7afd82fe51c185b8661276105190a59d47322' + ); + expect(newModalDialogVersion.gitSha).toBe( + 'd0e16b42179de6f6c070da2310e99de837c71215' + ); + + expect(oldModalDialogVersionTestPlanReportsCount).toBeGreaterThan( + 0 + ); + expect( + oldModalDialogVersionVOSafariCompletedTestsCount + ).toBeGreaterThan(0); + expect(newModalDialogVersionTestPlanReportsInRDCount).toBe(0); + expect(newModalDialogVersionTestPlanReportsInDraftCount).toEqual(1); + }); + }); +}); 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 b5a39d68b..9dee97ae5 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -137,7 +137,6 @@ describe('graphql', () => { const excludedTypeNames = [ // Items formatted like this: // 'TestResult' - 'Issue', 'Vendor' ]; const excludedTypeNameAndField = [ @@ -147,6 +146,11 @@ describe('graphql', () => { ['PopulatedData', 'browserVersion'], ['TestPlanReport', 'issues'], ['TestPlanReport', 'vendorReviewStatus'], + ['TestPlanReportOperations', 'updateTestPlanReportTestPlanVersion'], + ['TestPlanVersion', 'candidatePhaseReachedAt'], + ['TestPlanVersion', 'recommendedPhaseReachedAt'], + ['TestPlanVersion', 'recommendedPhaseTargetDate'], + ['TestPlanVersion', 'deprecatedAt'], ['Test', 'viewers'] ]; ({ @@ -182,6 +186,16 @@ describe('graphql', () => { id name } + candidateAts { + __typename + id + name + } + recommendedAts { + __typename + id + name + } browserVersions { __typename id @@ -197,6 +211,16 @@ describe('graphql', () => { id name } + candidateBrowsers { + __typename + id + name + } + recommendedBrowsers { + __typename + id + name + } atVersions { __typename id @@ -266,17 +290,45 @@ describe('graphql', () => { testPlanVersions { id } + issues { + __typename + author + title + link + isCandidateReview + feedbackType + isOpen + testNumberFilteredByAt + createdAt + closedAt + at { + name + } + browser { + name + } + } } testPlans { directory title } testPlanVersions { + __typename id + phase + draftPhaseReachedAt + candidatePhaseReachedAt + recommendedPhaseTargetDate + recommendedPhaseReachedAt + deprecatedAt } testPlanVersion(id: 1) { __typename id + testPlanReports { + id + } testPlan { id directory @@ -285,7 +337,7 @@ describe('graphql', () => { conflictTestPlanReport: testPlanReport(id: 2) { __typename id - status + isFinal createdAt vendorReviewStatus testPlanVersion { @@ -378,9 +430,6 @@ describe('graphql', () => { } metrics conflictsLength - candidateStatusReachedAt - recommendedStatusTargetDate - recommendedStatusReachedAt atVersions { id name @@ -391,6 +440,7 @@ describe('graphql', () => { name releasedAt } + markedFinalAt } testPlanReports { id @@ -510,24 +560,16 @@ describe('graphql', () => { locationOfData } } - reportStatus: testPlanReport(id: 1) { - __typename - updateStatus(status: CANDIDATE) { - locationOfData - } - } - bulkReportStatus: testPlanReport(ids: [1]) { + updateTestPlanVersionPhase: testPlanVersion(id: 26) { __typename - bulkUpdateStatus(status: CANDIDATE) { + updatePhase(phase: DRAFT) { locationOfData } } - reportRecommendedStatusTargetDate: testPlanReport( - id: 3 - ) { + testPlanVersion(id: 3) { __typename - updateRecommendedStatusTargetDate( - recommendedStatusTargetDate: "2023-12-25" + updateRecommendedPhaseTargetDate( + recommendedPhaseTargetDate: "2023-12-25" ) { locationOfData } @@ -552,6 +594,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/testQueue.test.js similarity index 91% rename from server/tests/integration/test-queue.test.js rename to server/tests/integration/testQueue.test.js index 8648ff342..ac577d459 100644 --- a/server/tests/integration/test-queue.test.js +++ b/server/tests/integration/testQueue.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,67 +191,6 @@ describe('test queue', () => { }); }); - it('can be finalized', async () => { - await dbCleaner(async () => { - const testPlanReportId = '3'; - // This report starts in a FINALIZED state. Let's set it to DRAFT. - await mutate(gql` - mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: DRAFT) { - testPlanReport { - status - } - } - } - } - `); - - const previous = await query(gql` - query { - testPlanReport(id: ${testPlanReportId}) { - status - } - } - `); - const previousStatus = previous.testPlanReport.status; - - const candidateResult = await mutate(gql` - mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: CANDIDATE) { - testPlanReport { - status - } - } - } - } - `); - const candidateResultStatus = - candidateResult.testPlanReport.updateStatus.testPlanReport - .status; - - const recommendedResult = await mutate(gql` - mutation { - testPlanReport(id: ${testPlanReportId}) { - updateStatus(status: RECOMMENDED) { - testPlanReport { - status - } - } - } - } - `); - const recommendedResultStatus = - recommendedResult.testPlanReport.updateStatus.testPlanReport - .status; - - expect(previousStatus).not.toBe('CANDIDATE'); - expect(candidateResultStatus).toBe('CANDIDATE'); - expect(recommendedResultStatus).toBe('RECOMMENDED'); - }); - }); - it('queries for information needed to add reports', async () => { const result = await query(gql` query { @@ -333,7 +274,6 @@ describe('test queue', () => { populatedData { testPlanReport { id - status at { id } @@ -343,6 +283,7 @@ describe('test queue', () => { } testPlanVersion { id + phase } } created { @@ -371,7 +312,6 @@ describe('test queue', () => { expect(first.testPlanReport).toEqual( expect.objectContaining({ id: expect.anything(), - status: 'DRAFT', at: expect.objectContaining({ id: atId }), @@ -382,7 +322,8 @@ describe('test queue', () => { ); expect(first.testPlanVersion).toEqual( expect.objectContaining({ - id: testPlanVersionId + id: testPlanVersionId, + phase: 'DRAFT' }) ); expect(first.created.length).toBe(1); @@ -403,7 +344,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); diff --git a/server/util/convertDateToString.js b/server/util/convertDateToString.js new file mode 100644 index 000000000..bc7a36e88 --- /dev/null +++ b/server/util/convertDateToString.js @@ -0,0 +1,8 @@ +const moment = require('moment'); + +const convertDateToString = (date, format = 'DD-MM-YYYY') => { + if (!date) return ''; + return moment(date).format(format); +}; + +module.exports = convertDateToString; diff --git a/yarn.lock b/yarn.lock index 826eae034..62541039e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2083,11 +2083,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@juggle/resize-observer@^3.3.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" - integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== - "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -2305,25 +2300,6 @@ dependencies: "@swc/helpers" "^0.4.14" -"@react-hook/latest@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80" - integrity sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg== - -"@react-hook/passive-layout-effect@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" - integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== - -"@react-hook/resize-observer@^1.2.6": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz#9a8cf4c5abb09becd60d1d65f6bf10eec211e291" - integrity sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA== - dependencies: - "@juggle/resize-observer" "^3.3.1" - "@react-hook/latest" "^1.0.2" - "@react-hook/passive-layout-effect" "^1.2.0" - "@remix-run/router@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.3.2.tgz#58cd2bd25df2acc16c628e1b6f6150ea6c7455bc"