diff --git a/CHANGELOG.md b/CHANGELOG.md index 822943ffd..c7d5a64d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [1.4.0](https://github.com/w3c/aria-at-app/compare/v1.3.1...v1.4.0) (2024-06-24) + + +### Features + +* Trends ([#1123](https://github.com/w3c/aria-at-app/issues/1123)) ([af8f492](https://github.com/w3c/aria-at-app/commit/af8f492b7d40ffadcedec88c47d06b166ec489fe)), closes [#791](https://github.com/w3c/aria-at-app/issues/791) [#792](https://github.com/w3c/aria-at-app/issues/792) [#1055](https://github.com/w3c/aria-at-app/issues/1055) [#1001](https://github.com/w3c/aria-at-app/issues/1001) [#1065](https://github.com/w3c/aria-at-app/issues/1065) [#1052](https://github.com/w3c/aria-at-app/issues/1052) [#1087](https://github.com/w3c/aria-at-app/issues/1087) [#1098](https://github.com/w3c/aria-at-app/issues/1098) [#1092](https://github.com/w3c/aria-at-app/issues/1092) [#1131](https://github.com/w3c/aria-at-app/issues/1131) [#1124](https://github.com/w3c/aria-at-app/issues/1124) + + +### Bug Fixes + +* use correct test number sequence for raising github issues ([#1128](https://github.com/w3c/aria-at-app/issues/1128)) ([70270e3](https://github.com/w3c/aria-at-app/commit/70270e3d83bf1eca6ea8f74cef4fcba104b1e783)) + ### [1.3.1](https://github.com/w3c/aria-at-app/compare/v1.3.0...v1.3.1) (2024-06-12) diff --git a/client/components/AddTestToQueueWithConfirmation/index.jsx b/client/components/AddTestToQueueWithConfirmation/index.jsx index 9b58541ff..9f8670edf 100644 --- a/client/components/AddTestToQueueWithConfirmation/index.jsx +++ b/client/components/AddTestToQueueWithConfirmation/index.jsx @@ -14,11 +14,16 @@ import { SCHEDULE_COLLECTION_JOB_MUTATION, EXISTING_TEST_PLAN_REPORTS } from './queries'; +import { TEST_QUEUE_PAGE_QUERY } from '../TestQueue2/queries'; +import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '../TestPlanReportStatusDialog/queries'; +import { ME_QUERY } from '../App/queries'; function AddTestToQueueWithConfirmation({ testPlanVersion, browser, at, + exactAtVersion, + minimumAtVersion, disabled = false, buttonText = 'Add to Test Queue', triggerUpdate = () => {} @@ -27,8 +32,27 @@ function AddTestToQueueWithConfirmation({ useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [canUseOldResults, setCanUseOldResults] = useState(false); - const [addTestPlanReport] = useMutation(ADD_TEST_QUEUE_MUTATION); - const [scheduleCollection] = useMutation(SCHEDULE_COLLECTION_JOB_MUTATION); + + const [addTestPlanReport] = useMutation(ADD_TEST_QUEUE_MUTATION, { + refetchQueries: [ + ME_QUERY, + EXISTING_TEST_PLAN_REPORTS, + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true + }); + + const [scheduleCollection] = useMutation(SCHEDULE_COLLECTION_JOB_MUTATION, { + refetchQueries: [ + ME_QUERY, + EXISTING_TEST_PLAN_REPORTS, + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true + }); + const { data: existingTestPlanReportsData } = useQuery( EXISTING_TEST_PLAN_REPORTS, { @@ -44,28 +68,26 @@ function AddTestToQueueWithConfirmation({ const existingTestPlanReports = existingTestPlanReportsData?.existingTestPlanVersion?.testPlanReports; - const conflictingReportExists = existingTestPlanReports?.some(report => { - return ( - report.at.id === at?.id && - report.browser.id === browser?.id && - report.isFinal - ); - }); - let latestOldVersion; let oldReportToCopyResultsFrom; - // Prioritize a conflicting report for the current version, otherwise - // check if any results data available from a previous result - if ( - !conflictingReportExists && - existingTestPlanReportsData?.oldTestPlanVersions?.length - ) { - latestOldVersion = - existingTestPlanReportsData?.oldTestPlanVersions?.reduce((a, b) => - new Date(a.updatedAt) > new Date(b.updatedAt) ? a : b - ); + // Check if any results data available from a previous result using the + // same testFormatVersion + const oldTestPlanVersions = + existingTestPlanReportsData?.oldTestPlanVersions?.filter( + ({ metadata }) => { + return ( + metadata.testFormatVersion === + existingTestPlanReportsData?.existingTestPlanVersion + ?.metadata.testFormatVersion + ); + } + ) || []; + if (oldTestPlanVersions?.length) { + latestOldVersion = oldTestPlanVersions?.reduce((a, b) => + new Date(a.updatedAt) > new Date(b.updatedAt) ? a : b + ); if ( new Date(latestOldVersion?.updatedAt) < new Date(testPlanVersion?.updatedAt) @@ -132,32 +154,40 @@ function AddTestToQueueWithConfirmation({ actions.push({ label: 'Add and run later', onClick: async () => { - await addTestToQueue( - canUseOldResults - ? { - copyResultsFromTestPlanReportId: - latestOldVersion.id - } - : {} - ); - await closeWithUpdate(); - } - }); - - if (!alreadyHasBotInTestPlanReport) { - actions.push({ - label: 'Add and run with bot', - onClick: async () => { - const testPlanReport = await addTestToQueue( + try { + await addTestToQueue( canUseOldResults ? { - copyResultsFromTestPlanReportId: + copyResultsFromTestPlanVersionId: latestOldVersion.id } : {} ); - await scheduleCollectionJob(testPlanReport); await closeWithUpdate(); + } catch (e) { + console.error(e); + } + } + }); + + if (!alreadyHasBotInTestPlanReport) { + actions.push({ + label: 'Add and run with bot', + onClick: async () => { + try { + const testPlanReport = await addTestToQueue( + canUseOldResults + ? { + copyResultsFromTestPlanVersionId: + latestOldVersion.id + } + : {} + ); + await scheduleCollectionJob(testPlanReport); + await closeWithUpdate(); + } catch (e) { + console.error(e); + } } }); } @@ -184,76 +214,47 @@ function AddTestToQueueWithConfirmation({ }; const renderPreserveReportDataDialog = () => { - let title; - let content; - let actions = []; - - if (oldReportToCopyResultsFrom) { - title = 'Older Results Data Found'; - content = - 'Older results with the same AT, browser and test plan ' + - 'were found for the report being created. Would you like ' + - 'to copy the older results into the report or create a ' + - 'completely new report?'; - actions = [ - { - label: 'Create empty report', - onClick: async () => { - setShowPreserveReportDataMessage(false); - if (hasAutomationSupport) { - setShowConfirmation(true); - } else { - await addTestToQueue(); - } - } - }, - { - label: 'Copy older results', - onClick: async () => { - setShowPreserveReportDataMessage(false); - setCanUseOldResults(true); - - if (hasAutomationSupport) { - setShowConfirmation(true); - } else { - await addTestToQueue({ - copyResultsFromTestPlanReportId: - latestOldVersion.id - }); - } - } - } - ]; - } else { - title = 'Conflicting Report Found'; - content = - 'The report could not be created because an existing ' + - 'report was found on the reports page with the same AT, ' + - 'browser and test plan version. Would you like to return ' + - 'the existing report back to the test queue?'; - actions = [ - { - label: 'Proceed', - onClick: async () => { - setShowPreserveReportDataMessage(false); - if (hasAutomationSupport) { - setShowConfirmation(true); - } else { - await addTestToQueue(); - } - } - } - ]; - } - return ( { + setShowPreserveReportDataMessage(false); + if (hasAutomationSupport) { + setShowConfirmation(true); + } else { + await addTestToQueue(); + } + } + }, + { + label: 'Copy older results', + onClick: async () => { + setShowPreserveReportDataMessage(false); + setCanUseOldResults(true); + + if (hasAutomationSupport) { + setShowConfirmation(true); + } else { + await addTestToQueue({ + copyResultsFromTestPlanVersionId: + latestOldVersion.id + }); + } + } + } + ]} useOnHide handleClose={async () => { setShowPreserveReportDataMessage(false); @@ -262,20 +263,23 @@ function AddTestToQueueWithConfirmation({ ); }; - const addTestToQueue = async ({ copyResultsFromTestPlanReportId } = {}) => { + const addTestToQueue = async ({ + copyResultsFromTestPlanVersionId + } = {}) => { let tpr; await triggerLoad(async () => { const res = await addTestPlanReport({ variables: { testPlanVersionId: testPlanVersion.id, atId: at.id, + minimumAtVersionId: minimumAtVersion?.id, + exactAtVersionId: exactAtVersion?.id, browserId: browser.id, - copyResultsFromTestPlanReportId + copyResultsFromTestPlanVersionId } }); const testPlanReport = - res?.data?.findOrCreateTestPlanReport?.populatedData - ?.testPlanReport ?? null; + res?.data?.createTestPlanReport?.testPlanReport ?? null; tpr = testPlanReport; }, 'Adding Test Plan to Test Queue'); setShowConfirmation(true); @@ -300,7 +304,7 @@ function AddTestToQueueWithConfirmation({ disabled={disabled} variant="secondary" onClick={async () => { - if (conflictingReportExists || oldReportToCopyResultsFrom) { + if (oldReportToCopyResultsFrom) { setShowPreserveReportDataMessage(true); } else { if (hasAutomationSupport) { @@ -333,6 +337,8 @@ AddTestToQueueWithConfirmation.propTypes = { key: PropTypes.string.isRequired, name: PropTypes.string.isRequired }), + exactAtVersion: PropTypes.object, + minimumAtVersion: PropTypes.object, buttonRef: PropTypes.object, onFocus: PropTypes.func, onBlur: PropTypes.func, diff --git a/client/components/AddTestToQueueWithConfirmation/queries.js b/client/components/AddTestToQueueWithConfirmation/queries.js index e7d7b40d3..8aafafdf6 100644 --- a/client/components/AddTestToQueueWithConfirmation/queries.js +++ b/client/components/AddTestToQueueWithConfirmation/queries.js @@ -30,6 +30,7 @@ export const EXISTING_TEST_PLAN_REPORTS = gql` id } } + metadata } oldTestPlanVersions: testPlanVersions( phases: [CANDIDATE, RECOMMENDED] @@ -46,6 +47,7 @@ export const EXISTING_TEST_PLAN_REPORTS = gql` id } } + metadata } } `; diff --git a/client/components/BotRunTestStatusList/index.js b/client/components/BotRunTestStatusList/index.js index 6d1873c50..9a5bf389e 100644 --- a/client/components/BotRunTestStatusList/index.js +++ b/client/components/BotRunTestStatusList/index.js @@ -5,6 +5,7 @@ import { useQuery } from '@apollo/client'; import styled from '@emotion/styled'; import ReportStatusDot from '../common/ReportStatusDot'; +// TODO: Remove when Test Queue v1 is removed const BotRunTestStatusUnorderedList = styled.ul` list-style-type: none; background-color: #f6f8fa; @@ -14,6 +15,21 @@ const BotRunTestStatusUnorderedList = styled.ul` white-space: nowrap; `; +const BotRunTestContainer = styled.div` + font-size: 0.875rem !important; + padding: 0.5rem 0; + margin: 0.5rem 0; + + background: #f5f5f5; + border-radius: 0.25rem; + + white-space: nowrap; +`; + +const BotRunTestStatusUnorderedListV2 = styled.ul` + list-style-type: none; +`; + /** * Generate a string describing the status of some number of "Tests" where the * word "Test" is pluralized appropriately and qualified with the provided @@ -33,7 +49,10 @@ const testCountString = (count, status) => const pollInterval = 2000; -const BotRunTestStatusList = ({ testPlanReportId }) => { +const BotRunTestStatusList = ({ + testPlanReportId, + fromTestQueueV2 = false // TODO: Remove when Test Queue v1 is removed +}) => { const { data: testPlanRunsQueryResult, startPolling, @@ -84,40 +103,78 @@ const BotRunTestStatusList = ({ testPlanReportId }) => { ) { return null; } + return ( - - {RUNNING > 0 && ( -
  • - - {testCountString(RUNNING, 'Running')} -
  • - )} - {ERROR > 0 && ( -
  • - - {testCountString(ERROR, 'Error')} -
  • - )} -
  • - - {testCountString(COMPLETED, 'Completed')} -
  • -
  • - - {testCountString(QUEUED, 'Queued')} -
  • - {CANCELLED > 0 && ( -
  • - - {testCountString(CANCELLED, 'Cancelled')} -
  • + <> + {fromTestQueueV2 ? ( + + Bot Status: + + {RUNNING > 0 && ( +
  • + + {testCountString(RUNNING, 'Running')} +
  • + )} + {ERROR > 0 && ( +
  • + + {testCountString(ERROR, 'Error')} +
  • + )} +
  • + + {testCountString(COMPLETED, 'Completed')} +
  • +
  • + + {testCountString(QUEUED, 'Queued')} +
  • + {CANCELLED > 0 && ( +
  • + + {testCountString(CANCELLED, 'Cancelled')} +
  • + )} +
    +
    + ) : ( + + {RUNNING > 0 && ( +
  • + + {testCountString(RUNNING, 'Running')} +
  • + )} + {ERROR > 0 && ( +
  • + + {testCountString(ERROR, 'Error')} +
  • + )} +
  • + + {testCountString(COMPLETED, 'Completed')} +
  • +
  • + + {testCountString(QUEUED, 'Queued')} +
  • + {CANCELLED > 0 && ( +
  • + + {testCountString(CANCELLED, 'Cancelled')} +
  • + )} +
    )} -
    + ); }; BotRunTestStatusList.propTypes = { - testPlanReportId: PropTypes.string.isRequired + testPlanReportId: PropTypes.string.isRequired, + fromTestQueueV2: PropTypes.bool }; export default BotRunTestStatusList; diff --git a/client/components/CandidateReview/CandidateTestPlanRun/index.jsx b/client/components/CandidateReview/CandidateTestPlanRun/index.jsx index 85a1ae709..10be4e4bd 100644 --- a/client/components/CandidateReview/CandidateTestPlanRun/index.jsx +++ b/client/components/CandidateReview/CandidateTestPlanRun/index.jsx @@ -317,6 +317,7 @@ const CandidateTestPlanRun = () => { versionString: testPlanVersion.versionString, testTitle: currentTest.title, testRowNumber: currentTest.rowNumber, + testSequenceNumber: currentTest.seq, testRenderedUrl: currentTest.renderedUrl, atName: testPlanReport.at.name }; @@ -464,7 +465,7 @@ const CandidateTestPlanRun = () => { testPlanTitle: testPlanVersion.title, versionString: testPlanVersion.versionString, - testRowNumber: currentTest.rowNumber + testSequenceNumber: currentTest.seq })} /> ); diff --git a/client/components/CandidateReview/TestPlans/index.jsx b/client/components/CandidateReview/TestPlans/index.jsx index 961c0a7b2..7a39ec514 100644 --- a/client/components/CandidateReview/TestPlans/index.jsx +++ b/client/components/CandidateReview/TestPlans/index.jsx @@ -550,11 +550,11 @@ const TestPlans = ({ testPlanVersions }) => { to={`/candidate-test-plan/${testPlanVersion.id}/${atId}`} > {getTestPlanVersionTitle( - testPlanVersion + testPlanVersion, + { + includeVersionString: true + } )}{' '} - { - testPlanVersion.versionString - }{' '} ({testsCount} Test {testsCount === 0 || testsCount > 1 @@ -755,9 +755,9 @@ const TestPlans = ({ testPlanVersions }) => { {getTestPlanVersionTitle( - testPlanVersion - )}{' '} - {testPlanVersion.versionString} + testPlanVersion, + { includeVersionString: true } + )} {jawsDataExists diff --git a/client/components/DataManagement/DataManagementRow/index.jsx b/client/components/DataManagement/DataManagementRow/index.jsx index c3b8d777c..14c0c9295 100644 --- a/client/components/DataManagement/DataManagementRow/index.jsx +++ b/client/components/DataManagement/DataManagementRow/index.jsx @@ -767,7 +767,6 @@ const DataManagementRow = ({ )} @@ -971,7 +970,6 @@ const DataManagementRow = ({ )} @@ -1060,7 +1058,6 @@ const DataManagementRow = ({ diff --git a/client/components/GraphQLProvider/GraphQLProvider.jsx b/client/components/GraphQLProvider/GraphQLProvider.jsx index e17645077..cb0fa25b1 100644 --- a/client/components/GraphQLProvider/GraphQLProvider.jsx +++ b/client/components/GraphQLProvider/GraphQLProvider.jsx @@ -31,6 +31,9 @@ const client = new ApolloClient({ typePolicies: { Query: { fields: { + me: { merge: true }, + testPlanVersion: { merge: true }, + testPlanVersions: { merge: false }, testPlanReport: { merge: true }, testPlanReports: { merge: false }, collectionJobByTestPlanRunId: { @@ -43,7 +46,8 @@ const client = new ApolloClient({ Mutation: { fields: { testPlanReport: { merge: false }, - testPlanRun: { merge: false } + testPlanRun: { merge: false }, + testPlanVersion: { merge: false } } } } diff --git a/client/components/ManageBotRunDialog/WithButton.jsx b/client/components/ManageBotRunDialog/WithButton.jsx index 3546b117e..920cd9498 100644 --- a/client/components/ManageBotRunDialog/WithButton.jsx +++ b/client/components/ManageBotRunDialog/WithButton.jsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Button } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faRobot } from '@fortawesome/free-solid-svg-icons'; import ManageBotRunDialog from '.'; import { useTestPlanRunIsFinished } from '../../hooks/useTestPlanRunIsFinished'; @@ -9,6 +11,7 @@ const ManageBotRunDialogWithButton = ({ testPlanReportId, runnableTestsLength, testers, + includeIcon = false, onChange }) => { const { runIsFinished } = useTestPlanRunIsFinished(testPlanRun.id); @@ -24,22 +27,25 @@ const ManageBotRunDialogWithButton = ({ onClick={async () => { setShowDialog(true); }} - className="mb-2" > + {/* TODO: Include by default after removing Test Queue v1 content */} + {includeIcon ? : null} Manage {testPlanRun?.tester?.username} Run - { - await onChange(); - setShowDialog(false); - }} - /> + {showDialog ? ( + { + await onChange(); + setShowDialog(false); + }} + /> + ) : null} ); }; @@ -49,6 +55,7 @@ ManageBotRunDialogWithButton.propTypes = { testPlanReportId: PropTypes.string.isRequired, runnableTestsLength: PropTypes.number.isRequired, testers: PropTypes.array.isRequired, + includeIcon: PropTypes.bool, onChange: PropTypes.func.isRequired }; diff --git a/client/components/ManageTestQueue/AddTestPlans.jsx b/client/components/ManageTestQueue/AddTestPlans.jsx new file mode 100644 index 000000000..d0ee8458b --- /dev/null +++ b/client/components/ManageTestQueue/AddTestPlans.jsx @@ -0,0 +1,343 @@ +import React, { useEffect, useState } from 'react'; +import { Form } from 'react-bootstrap'; +import RadioBox from '@components/common/RadioBox'; +import AddTestToQueueWithConfirmation from '@components/AddTestToQueueWithConfirmation'; +import { DisclosureContainer } from '@components/ManageTestQueue/index'; +import { gitUpdatedDateToString } from '@client/utils/gitUtils'; +import PropTypes from 'prop-types'; + +const AddTestPlans = ({ + ats = [], + testPlanVersions = [], + triggerUpdate = () => {} +}) => { + const [allTestPlans, setAllTestPlans] = useState([]); + const [allTestPlanVersions, setAllTestPlanVersions] = useState([]); + + const [selectedTestPlanVersionId, setSelectedTestPlanVersionId] = + useState(''); + const [matchingTestPlanVersions, setMatchingTestPlanVersions] = useState( + [] + ); + + const [selectedAtId, setSelectedAtId] = useState(''); + const [selectedBrowserId, setSelectedBrowserId] = useState(''); + const [ + selectedAtVersionExactOrMinimum, + setSelectedAtVersionExactOrMinimum + ] = useState('Exact Version'); + const [selectedReportAtVersionId, setSelectedReportAtVersionId] = + useState(null); + const [ + showMinimumAtVersionErrorMessage, + setShowMinimumAtVersionErrorMessage + ] = useState(false); + + useEffect(() => { + // Prevent allTestPlanVersions and filteredTestPlanVersions from being unnecessarily overwritten + if (allTestPlanVersions.length) return; + + const _allTestPlanVersions = testPlanVersions + .map(version => ({ ...version })) + .flat(); + + // Get valid test plans by removing duplicate entries from different + // test plan versions of the same test plan being imported multiple times + const _allTestPlans = _allTestPlanVersions + .filter( + (v, i, a) => + a.findIndex( + t => + t.title === v.title && + t.testPlan.directory === v.testPlan.directory + ) === i + ) + .map(({ id, title, testPlan }) => ({ + id, + title, + directory: testPlan.directory + })) + // sort by the testPlanVersion titles + .sort((a, b) => (a.title < b.title ? -1 : 1)); + + // mark the first testPlanVersion as selected + if (_allTestPlans.length) { + const plan = _allTestPlans[0]; + updateMatchingTestPlanVersions(plan.id, _allTestPlanVersions); + } + + setAllTestPlans(_allTestPlans); + setAllTestPlanVersions(_allTestPlanVersions); + }, [testPlanVersions]); + + const updateMatchingTestPlanVersions = (value, allTestPlanVersions) => { + // update test plan versions based on selected test plan + const retrievedTestPlanVersion = allTestPlanVersions.find( + item => item.id === value + ); + + // find the versions that apply and pre-set these + const matchingTestPlanVersions = allTestPlanVersions + .filter( + item => + item.title === retrievedTestPlanVersion.title && + item.testPlan.directory === + retrievedTestPlanVersion.testPlan.directory && + item.phase !== 'DEPRECATED' && + item.phase !== 'RD' + ) + .sort((a, b) => + new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1 + ); + setMatchingTestPlanVersions(matchingTestPlanVersions); + + if (matchingTestPlanVersions.length) + setSelectedTestPlanVersionId(matchingTestPlanVersions[0].id); + else setSelectedTestPlanVersionId(null); + }; + + const onTestPlanVersionChange = e => { + const { value } = e.target; + setShowMinimumAtVersionErrorMessage(false); + setSelectedAtVersionExactOrMinimum('Exact Version'); + setSelectedTestPlanVersionId(value); + }; + + const onAtChange = e => { + const { value } = e.target; + setShowMinimumAtVersionErrorMessage(false); + setSelectedAtId(value); + setSelectedReportAtVersionId(null); + }; + + const onReportAtVersionIdChange = e => { + const { value } = e.target; + setSelectedReportAtVersionId(value); + }; + + const onBrowserChange = e => { + const { value } = e.target; + setSelectedBrowserId(value); + }; + + const selectedTestPlanVersion = allTestPlanVersions.find( + ({ id }) => id === selectedTestPlanVersionId + ); + + const exactOrMinimumAtVersion = ats + .find(item => item.id === selectedAtId) + ?.atVersions.find(item => item.id === selectedReportAtVersionId); + + return ( + + + Select a test plan, assistive technology and browser to add a + new test plan report to the test queue. + +
    + + + Test Plan + + { + const { value } = e.target; + setShowMinimumAtVersionErrorMessage(false); + setSelectedAtVersionExactOrMinimum('Exact Version'); + updateMatchingTestPlanVersions( + value, + allTestPlanVersions + ); + }} + > + {allTestPlans.map(item => ( + + ))} + + + + + Test Plan Version + + + {matchingTestPlanVersions.length ? ( + matchingTestPlanVersions.map(item => ( + + )) + ) : ( + + )} + + +
    {/* blank grid cell */}
    + + + Assistive Technology + + + + {ats.map(item => ( + + ))} + + + + + Assistive Technology Version + +
    + { + if ( + selectedTestPlanVersion?.phase === + 'RECOMMENDED' && + exactOrMinimum === 'Minimum Version' + ) { + setShowMinimumAtVersionErrorMessage(true); + return; + } + + setSelectedAtVersionExactOrMinimum( + exactOrMinimum + ); + }} + /> + + + {ats + .find(at => at.id === selectedAtId) + ?.atVersions.map(item => ( + + ))} + + {showMinimumAtVersionErrorMessage && + selectedTestPlanVersion?.phase === 'RECOMMENDED' ? ( +
    + The selected test plan version is in the + recommended phase and only exact versions can be + chosen. +
    + ) : null} +
    +
    + + + Browser + + + + {ats + .find(at => at.id === selectedAtId) + ?.browsers.map(item => ( + + ))} + + +
    + item.id === selectedTestPlanVersionId + )} + at={ats.find(item => item.id === selectedAtId)} + exactAtVersion={ + selectedAtVersionExactOrMinimum === 'Exact Version' + ? exactOrMinimumAtVersion + : null + } + minimumAtVersion={ + selectedAtVersionExactOrMinimum === 'Minimum Version' + ? exactOrMinimumAtVersion + : null + } + browser={ats + .find(at => at.id === selectedAtId) + ?.browsers.find( + browser => browser.id === selectedBrowserId + )} + triggerUpdate={triggerUpdate} + disabled={ + !selectedTestPlanVersionId || + !selectedAtId || + !selectedReportAtVersionId || + !selectedBrowserId + } + /> +
    + ); +}; + +AddTestPlans.propTypes = { + ats: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + browsers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }) + ).isRequired + }) + ).isRequired, + testPlanVersions: PropTypes.array, + triggerUpdate: PropTypes.func +}; + +export default AddTestPlans; diff --git a/client/components/ManageTestQueue/ManageAtVersions.jsx b/client/components/ManageTestQueue/ManageAtVersions.jsx new file mode 100644 index 000000000..c97211ae5 --- /dev/null +++ b/client/components/ManageTestQueue/ManageAtVersions.jsx @@ -0,0 +1,451 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Form } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { DisclosureContainer } from '@components/ManageTestQueue/index'; +import BasicModal from '@components/common/BasicModal'; +import UpdateVersionModal from '@components/common/UpdateVersionModal'; +import { convertStringToDate } from '@client/utils/formatter'; +import { useMutation } from '@apollo/client'; +import { + ADD_AT_VERSION_MUTATION, + DELETE_AT_VERSION_MUTATION, + EDIT_AT_VERSION_MUTATION +} from '@components/TestQueue/queries'; +import { useTriggerLoad } from '@components/common/LoadingStatus'; +import { THEMES, useThemedModal } from '@client/hooks/useThemedModal'; +import PropTypes from 'prop-types'; + +const ManageAtVersions = ({ ats = [], triggerUpdate = () => {} }) => { + const { triggerLoad } = useTriggerLoad(); + const { + themedModal, + showThemedModal, + setShowThemedModal, + setThemedModalTitle, + setThemedModalContent, + setThemedModalType, + setThemedModalActions, + setThemedModalShowCloseAction, + focus, + setFocusRef, + hideThemedModal + } = useThemedModal({ + type: THEMES.WARNING, + title: 'Error Updating Assistive Technology Version' + }); + + const loadedAts = useRef(false); + + const [selectedAtId, setSelectedAtId] = useState('1'); + const [selectedAtVersions, setSelectedAtVersions] = useState([]); + const [selectedAtVersionId, setSelectedAtVersionId] = useState(''); + + const [addAtVersion] = useMutation(ADD_AT_VERSION_MUTATION); + const [editAtVersion] = useMutation(EDIT_AT_VERSION_MUTATION); + const [deleteAtVersion] = useMutation(DELETE_AT_VERSION_MUTATION); + + // Update modal state values + const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false); + const [updateVersionModalTitle, setUpdateVersionModalTitle] = useState(''); + const [updateVersionModalType, setUpdateVersionModalType] = useState('add'); + const [updateVersionModalVersionText, setUpdateVersionModalVersionText] = + useState(''); + const [ + updateVersionModalModalDateText, + setUpdateVersionModalModalDateText + ] = useState(''); + + // Feedback modal state values + const [showFeedbackModal, setShowFeedbackModal] = useState(false); + const [feedbackModalTitle, setFeedbackModalTitle] = useState(''); + const [feedbackModalContent, setFeedbackModalContent] = useState(<>); + + useEffect(() => { + if (ats.length) { + if (!loadedAts.current) setSelectedAtId(ats[0].id); + + // Required during refetch logic around managing AT Versions + if (!loadedAts.current) setSelectedAtVersions(ats[0].atVersions); + else { + setSelectedAtVersions( + ats.find(item => item.id === selectedAtId).atVersions + ); + } + + if (!loadedAts.current) + setSelectedAtVersionId(ats[0]?.atVersions[0]?.id); + loadedAts.current = true; + } + }, [ats]); + + const getAtVersionFromId = id => + selectedAtVersions.find(item => id === item.id); + + const showThemedMessage = ({ + title, + content, + theme, + actions = null, + showCloseAction = false + }) => { + setThemedModalTitle(title); + setThemedModalContent(content); + setThemedModalType(theme); + setThemedModalActions(actions); + setThemedModalShowCloseAction(showCloseAction); + setShowThemedModal(true); + }; + + const showFeedbackMessage = (title, content) => { + setFeedbackModalTitle(title); + setFeedbackModalContent(content); + setShowFeedbackModal(true); + }; + + const onAtChange = e => { + const { value } = e.target; + if (selectedAtId !== value) { + setSelectedAtId(value); + const at = ats.find(item => item.id === value); + setSelectedAtVersions(at.atVersions); + setSelectedAtVersionId(at.atVersions[0].id); + } + }; + + const onAtVersionChange = e => { + const { value } = e.target; + setSelectedAtVersionId(value); + }; + + const onOpenAtVersionModalClick = type => { + if (type === 'add') { + const selectedAt = ats.find(item => item.id === selectedAtId); + setUpdateVersionModalTitle( + `Add a New Version for ${selectedAt.name}` + ); + setUpdateVersionModalType('add'); + setUpdateVersionModalVersionText(''); + setUpdateVersionModalModalDateText(''); + setShowUpdateVersionModal(true); + } + + if (type === 'edit') { + const selectedAt = ats.find(item => item.id === selectedAtId); + setUpdateVersionModalTitle( + `Edit ${selectedAt.name} Version ${ + getAtVersionFromId(selectedAtVersionId)?.name + }` + ); + setUpdateVersionModalType('edit'); + setUpdateVersionModalVersionText( + getAtVersionFromId(selectedAtVersionId)?.name + ); + setUpdateVersionModalModalDateText( + getAtVersionFromId(selectedAtVersionId)?.releasedAt + ); + setShowUpdateVersionModal(true); + } + + if (type === 'delete') { + const theme = 'danger'; + + const selectedAt = ats.find(item => item.id === selectedAtId); + showThemedMessage({ + title: `Remove ${selectedAt.name} Version ${ + getAtVersionFromId(selectedAtVersionId)?.name + }`, + content: ( + <> + You are about to remove{' '} + + {selectedAt.name} Version{' '} + {getAtVersionFromId(selectedAtVersionId)?.name} + {' '} + from the ARIA-AT App. + + ), + actions: [ + { + text: 'Remove', + action: () => onUpdateAtVersionAction('delete', {}) + } + ], + showCloseAction: true, + theme + }); + } + }; + + const onUpdateAtVersionAction = async ( + actionType, + { updatedVersionText, updatedDateAvailabilityText } + ) => { + const selectedAt = ats.find(item => item.id === selectedAtId); + + if (actionType === 'add') { + const existingAtVersion = selectedAtVersions.find( + item => item.name.trim() === updatedVersionText.trim() + ); + if (existingAtVersion) { + setSelectedAtVersionId(existingAtVersion.id); + + onUpdateModalClose(); + showFeedbackMessage( + 'Existing Assistive Technology Version', + <> + + {selectedAt.name} {updatedVersionText} + {' '} + already exists in the system. Please try adding a + different one. + + ); + return; + } + + onUpdateModalClose(); + await triggerLoad(async () => { + const addAtVersionData = await addAtVersion({ + variables: { + atId: selectedAtId, + name: updatedVersionText, + releasedAt: convertStringToDate( + updatedDateAvailabilityText + ) + } + }); + setSelectedAtVersionId( + addAtVersionData.data?.at?.findOrCreateAtVersion?.id + ); + await triggerUpdate(); + }, 'Adding Assistive Technology Version'); + + showFeedbackMessage( + 'Successfully Added Assistive Technology Version', + <> + Successfully added{' '} + + {selectedAt.name} {updatedVersionText} + + . + + ); + } + + if (actionType === 'edit') { + onUpdateModalClose(); + await triggerLoad(async () => { + await editAtVersion({ + variables: { + atVersionId: selectedAtVersionId, + name: updatedVersionText, + releasedAt: convertStringToDate( + updatedDateAvailabilityText + ) + } + }); + await triggerUpdate(); + }, 'Updating Assistive Technology Version'); + + showFeedbackMessage( + 'Successfully Updated Assistive Technology Version', + <> + Successfully updated{' '} + + {selectedAt.name} {updatedVersionText} + + . + + ); + } + + if (actionType === 'delete') { + const deleteAtVersionData = await deleteAtVersion({ + variables: { + atVersionId: selectedAtVersionId + } + }); + if ( + !deleteAtVersionData.data?.atVersion?.deleteAtVersion?.isDeleted + ) { + const patternName = + deleteAtVersionData.data?.atVersion?.deleteAtVersion + ?.failedDueToTestResults[0]?.testPlanVersion?.title; + const theme = 'warning'; + + // Removing an AT Version already in use + showThemedMessage({ + title: 'Assistive Technology Version already being used', + content: ( + <> + + {selectedAt.name} Version{' '} + {getAtVersionFromId(selectedAtVersionId)?.name} + {' '} + can't be removed because it is already being + used to test the {patternName} Test Plan. + + ), + theme + }); + } else { + onThemedModalClose(); + + await triggerLoad(async () => { + await triggerUpdate(); + }, 'Removing Assistive Technology Version'); + + // Show confirmation that AT has been deleted + showFeedbackMessage( + 'Successfully Removed Assistive Technology Version', + <> + Successfully removed version for{' '} + {selectedAt.name}. + + ); + + // Reset atVersion to valid existing item + setSelectedAtVersionId(selectedAt.atVersions[0]?.id); + } + } + }; + + const onUpdateModalClose = () => { + setUpdateVersionModalVersionText(''); + setUpdateVersionModalModalDateText(''); + setShowUpdateVersionModal(false); + focus(); + }; + + const onThemedModalClose = () => { + hideThemedModal(); + focus(); + }; + + return ( + <> + + + Select an assistive technology and manage its versions in + the ARIA-AT App + +
    + + + Assistive Technology + + + {ats + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(item => ( + + ))} + + +
    + + + Available Versions + + + {selectedAtVersions.map(item => ( + + ))} + + +
    + + + +
    +
    +
    +
    + + {showThemedModal && themedModal} + {showUpdateVersionModal && ( + + )} + {showFeedbackModal && ( + { + setShowFeedbackModal(false); + }} + /> + )} + + ); +}; + +ManageAtVersions.propTypes = { + ats: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + browsers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }) + ).isRequired + }) + ).isRequired, + triggerUpdate: PropTypes.func +}; + +export default ManageAtVersions; diff --git a/client/components/ManageTestQueue/index.jsx b/client/components/ManageTestQueue/index.jsx index de76dbf08..58b85c5ab 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -1,25 +1,12 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { useMutation } from '@apollo/client'; -import { Form } from 'react-bootstrap'; +import React, { useState } from 'react'; import styled from '@emotion/styled'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import PropTypes from 'prop-types'; -import BasicModal from '../common/BasicModal'; -import UpdateVersionModal from '../common/UpdateVersionModal'; -import BasicThemedModal from '../common/BasicThemedModal'; -import { - ADD_AT_VERSION_MUTATION, - EDIT_AT_VERSION_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'; +import ManageAtVersions from '@components/ManageTestQueue/ManageAtVersions'; +import AddTestPlans from '@components/ManageTestQueue/AddTestPlans'; -const DisclosureContainer = styled.div` +export const DisclosureContainer = styled.div` // Following directives are related to the ManageTestQueue component > span { display: block; @@ -28,11 +15,8 @@ const DisclosureContainer = styled.div` // Add Test Plan to Test Queue button > button { - display: flex; padding: 0.5rem 1rem; margin-top: 1rem; - margin-left: auto; - margin-right: 0; } .disclosure-row-manage-ats { @@ -78,9 +62,41 @@ const DisclosureContainer = styled.div` .disclosure-row-test-plans { display: grid; - grid-auto-flow: column; - grid-template-columns: 1fr 1fr 1fr 1fr; - grid-gap: 1rem; + row-gap: 0.5rem; + grid-template-columns: 2fr 2fr 1fr; + column-gap: 2rem; + + & > :nth-of-type(3) { + display: block; + } + & > :nth-of-type(5) { + grid-column: span 2; + } + + @media (max-width: 768px) { + grid-template-columns: 1fr; + + & > :nth-of-type(3) { + display: none; + } + & > :nth-of-type(5) { + grid-column: initial; + } + } + } + + .form-group-at-version { + display: flex; + flex-wrap: wrap; + column-gap: 1rem; + row-gap: 0.75rem; + + select { + width: inherit; + @media (max-width: 767px) { + flex-grow: 1; + } + } } .disclosure-form-label { @@ -94,378 +110,14 @@ const ManageTestQueue = ({ testPlanVersions = [], triggerUpdate = () => {} }) => { - const { triggerLoad, loadingMessage } = useTriggerLoad(); - - const loadedAts = useRef(false); - const focusButtonRef = useRef(); - const addAtVersionButtonRef = useRef(); - const editAtVersionButtonRef = useRef(); - const deleteAtVersionButtonRef = useRef(); + const { loadingMessage } = useTriggerLoad(); const [showManageATs, setShowManageATs] = useState(false); const [showAddTestPlans, setShowAddTestPlans] = useState(false); - const [selectedManageAtId, setSelectedManageAtId] = useState('1'); - const [selectedManageAtVersions, setSelectedManageAtVersions] = useState( - [] - ); - const [selectedManageAtVersionId, setSelectedManageAtVersionId] = - useState(''); - - const [showAtVersionModal, setShowAtVersionModal] = useState(false); - const [atVersionModalTitle, setAtVersionModalTitle] = useState(''); - const [atVersionModalType, setAtVersionModalType] = useState('add'); - const [atVersionModalVersionText, setAtVersionModalVersionText] = - useState(''); - const [atVersionModalDateText, setAtVersionModalDateText] = useState(''); - - const [showThemedModal, setShowThemedModal] = useState(false); - const [themedModalType, setThemedModalType] = useState('warning'); - const [themedModalTitle, setThemedModalTitle] = useState(''); - const [themedModalContent, setThemedModalContent] = useState(<>); - - const [showFeedbackModal, setShowFeedbackModal] = useState(false); - const [feedbackModalTitle, setFeedbackModalTitle] = useState(''); - const [feedbackModalContent, setFeedbackModalContent] = useState(<>); - - const [allTestPlanVersions, setAllTestPlanVersions] = useState([]); - const [filteredTestPlanVersions, setFilteredTestPlanVersions] = useState( - [] - ); - const [selectedTestPlanVersionId, setSelectedTestPlanVersionId] = - useState(''); - const [matchingTestPlanVersions, setMatchingTestPlanVersions] = useState( - [] - ); - - const [selectedAtId, setSelectedAtId] = useState(''); - const [selectedBrowserId, setSelectedBrowserId] = useState(''); - - const [addAtVersion] = useMutation(ADD_AT_VERSION_MUTATION); - const [editAtVersion] = useMutation(EDIT_AT_VERSION_MUTATION); - const [deleteAtVersion] = useMutation(DELETE_AT_VERSION_MUTATION); const onManageAtsClick = () => setShowManageATs(!showManageATs); const onAddTestPlansClick = () => setShowAddTestPlans(!showAddTestPlans); - useEffect(() => { - const allTestPlanVersions = testPlanVersions - .map(version => ({ ...version })) - .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 - ) - // sort by the testPlanVersion titles - .sort((a, b) => (a.title < b.title ? -1 : 1)); - - // mark the first testPlanVersion as selected - if (filteredTestPlanVersions.length) { - const plan = filteredTestPlanVersions[0]; - updateMatchingTestPlanVersions(plan.id, allTestPlanVersions); - } - - setAllTestPlanVersions(allTestPlanVersions); - setFilteredTestPlanVersions(filteredTestPlanVersions); - }, [testPlanVersions]); - - useEffect(() => { - if (ats.length) { - if (!loadedAts.current) setSelectedManageAtId(ats[0].id); - - // Required during refetch logic around managing AT Versions - if (!loadedAts.current) - setSelectedManageAtVersions(ats[0].atVersions); - else - setSelectedManageAtVersions( - ats.find(item => item.id === selectedManageAtId).atVersions - ); - - if (!loadedAts.current) - setSelectedManageAtVersionId(ats[0]?.atVersions[0]?.id); - loadedAts.current = true; - } - }, [ats]); - - const updateMatchingTestPlanVersions = (value, allTestPlanVersions) => { - // update test plan versions based on selected test plan - const retrievedTestPlan = allTestPlanVersions.find( - item => item.id === value - ); - - // find the versions that apply and pre-set these - 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); - - if (matchingTestPlanVersions.length) - setSelectedTestPlanVersionId(matchingTestPlanVersions[0].id); - else setSelectedTestPlanVersionId(null); - }; - - const onManageAtChange = e => { - const { value } = e.target; - if (selectedManageAtId !== value) { - setSelectedManageAtId(value); - const at = ats.find(item => item.id === value); - setSelectedManageAtVersions(at.atVersions); - setSelectedManageAtVersionId(at.atVersions[0].id); - } - }; - - const onManageAtVersionChange = e => { - const { value } = e.target; - setSelectedManageAtVersionId(value); - }; - - const onOpenAtVersionModalClick = (type = 'add') => { - if (type === 'add') { - focusButtonRef.current = addAtVersionButtonRef.current; - - const selectedAt = ats.find(item => item.id === selectedManageAtId); - setAtVersionModalTitle(`Add a New Version for ${selectedAt.name}`); - setAtVersionModalType('add'); - setAtVersionModalVersionText(''); - setAtVersionModalDateText(''); - setShowAtVersionModal(true); - } - - if (type === 'edit') { - focusButtonRef.current = editAtVersionButtonRef.current; - - const selectedAt = ats.find(item => item.id === selectedManageAtId); - setAtVersionModalTitle( - `Edit ${selectedAt.name} Version ${ - getAtVersionFromId(selectedManageAtVersionId)?.name - }` - ); - setAtVersionModalType('edit'); - setAtVersionModalVersionText( - getAtVersionFromId(selectedManageAtVersionId)?.name - ); - setAtVersionModalDateText( - getAtVersionFromId(selectedManageAtVersionId)?.releasedAt - ); - setShowAtVersionModal(true); - } - }; - - const onRemoveClick = () => { - focusButtonRef.current = deleteAtVersionButtonRef.current; - - const theme = 'danger'; - const selectedAt = ats.find(item => item.id === selectedManageAtId); - - showThemedMessage( - `Remove ${selectedAt.name} Version ${ - getAtVersionFromId(selectedManageAtVersionId)?.name - }`, - <> - You are about to remove{' '} - - {selectedAt.name} Version{' '} - {getAtVersionFromId(selectedManageAtVersionId)?.name} - {' '} - from the ARIA-AT App. - , - theme - ); - }; - - const onUpdateModalClose = () => { - setAtVersionModalVersionText(''); - setAtVersionModalDateText(''); - setShowAtVersionModal(false); - focusButtonRef.current.focus(); - }; - - const onThemedModalClose = () => { - setShowThemedModal(false); - focusButtonRef.current.focus(); - }; - - const getAtVersionFromId = id => { - return selectedManageAtVersions.find(item => id === item.id); - }; - - const onAtChange = e => { - const { value } = e.target; - setSelectedAtId(value); - }; - - const onBrowserChange = e => { - const { value } = e.target; - setSelectedBrowserId(value); - }; - - const onTestPlanVersionChange = e => { - const { value } = e.target; - setSelectedTestPlanVersionId(value); - }; - - const onUpdateAtVersionAction = async ( - actionType, - { updatedVersionText, updatedDateAvailabilityText } - ) => { - const selectedAt = ats.find(item => item.id === selectedManageAtId); - - if (actionType === 'add') { - const existingAtVersion = selectedManageAtVersions.find( - item => item.name.trim() === updatedVersionText.trim() - ); - if (existingAtVersion) { - setSelectedManageAtVersionId(existingAtVersion.id); - - onUpdateModalClose(); - showFeedbackMessage( - 'Existing Assistive Technology Version', - <> - - {selectedAt.name} {updatedVersionText} - {' '} - already exists in the system. Please try adding a - different one. - - ); - return; - } - - onUpdateModalClose(); - await triggerLoad(async () => { - const addAtVersionData = await addAtVersion({ - variables: { - atId: selectedManageAtId, - name: updatedVersionText, - releasedAt: convertStringToDate( - updatedDateAvailabilityText - ) - } - }); - setSelectedManageAtVersionId( - addAtVersionData.data?.at?.findOrCreateAtVersion?.id - ); - await triggerUpdate(); - }, 'Adding Assistive Technology Version'); - - showFeedbackMessage( - 'Successfully Added Assistive Technology Version', - <> - Successfully added{' '} - - {selectedAt.name} {updatedVersionText} - - . - - ); - } - - if (actionType === 'edit') { - onUpdateModalClose(); - await triggerLoad(async () => { - await editAtVersion({ - variables: { - atVersionId: selectedManageAtVersionId, - name: updatedVersionText, - releasedAt: convertStringToDate( - updatedDateAvailabilityText - ) - } - }); - await triggerUpdate(); - }, 'Updating Assistive Technology Version'); - - showFeedbackMessage( - 'Successfully Updated Assistive Technology Version', - <> - Successfully updated{' '} - - {selectedAt.name} {updatedVersionText} - - . - - ); - } - - if (actionType === 'delete') { - const deleteAtVersionData = await deleteAtVersion({ - variables: { - atVersionId: selectedManageAtVersionId - } - }); - if ( - !deleteAtVersionData.data?.atVersion?.deleteAtVersion?.isDeleted - ) { - const patternName = - deleteAtVersionData.data?.atVersion?.deleteAtVersion - ?.failedDueToTestResults[0]?.testPlanVersion?.title; - const theme = 'warning'; - - // Removing an AT Version already in use - showThemedMessage( - 'Assistive Technology Version already being used', - <> - - {selectedAt.name} Version{' '} - { - getAtVersionFromId(selectedManageAtVersionId) - ?.name - } - {' '} - can't be removed because it is already being used - to test the {patternName} Test Plan. - , - theme - ); - } else { - onThemedModalClose(); - await triggerLoad(async () => { - await triggerUpdate(); - }, 'Removing Assistive Technology Version'); - - // Show confirmation that AT has been deleted - showFeedbackMessage( - 'Successfully Removed Assistive Technology Version', - <> - Successfully removed version for{' '} - {selectedAt.name}. - - ); - - // Reset atVersion to valid existing item - setSelectedManageAtVersionId(selectedAt.atVersions[0]?.id); - } - } - }; - - const showFeedbackMessage = (title, content) => { - setFeedbackModalTitle(title); - setFeedbackModalContent(content); - setShowFeedbackModal(true); - }; - - const showThemedMessage = (title, content, theme) => { - setThemedModalTitle(title); - setThemedModalContent(content); - setThemedModalType(theme); - setShowThemedModal(true); - }; - return ( - - Select an Assistive Technology and manage its - versions in the ARIA-AT App - -
    - - - Assistive Technology - - - {ats.map(item => ( - - ))} - - -
    - - - Available Versions - - - {selectedManageAtVersions.map(item => ( - - ))} - - -
    - - - -
    -
    -
    - , - - - Select a Test Plan and version and an Assistive - Technology and Browser to add it to the Test Queue - -
    - - - Test Plan - - { - const { value } = e.target; - updateMatchingTestPlanVersions( - value, - allTestPlanVersions - ); - }} - > - {filteredTestPlanVersions.map(item => ( - - ))} - - - - - Test Plan Version - - - {matchingTestPlanVersions.length ? ( - matchingTestPlanVersions.map(item => ( - - )) - ) : ( - - )} - - - - - Assistive Technology - - - - {ats.map(item => ( - - ))} - - - - - Browser - - - - {ats - .find(at => at.id === selectedAtId) - ?.browsers.map(item => ( - - ))} - - -
    - item.id === selectedTestPlanVersionId - )} - at={ats.find(item => item.id === selectedAtId)} - browser={ats - .find(at => at.id === selectedAtId) - ?.browsers.find( - browser => browser.id === selectedBrowserId - )} - triggerUpdate={triggerUpdate} - disabled={ - !selectedTestPlanVersionId || - !selectedAtId || - !selectedBrowserId - } - /> -
    + , + ]} onClick={[onManageAtsClick, onAddTestPlansClick]} expanded={[showManageATs, showAddTestPlans]} stacked /> - - {showAtVersionModal && ( - - )} - - {showThemedModal && ( - - onUpdateAtVersionAction('delete', {}) - : onThemedModalClose - } - ]} - handleClose={onThemedModalClose} - showCloseAction={themedModalType === 'danger'} - /> - )} - - {showFeedbackModal && ( - { - setShowFeedbackModal(false); - focusButtonRef.current.focus(); - }} - /> - )}
    ); }; diff --git a/client/components/Reports/SummarizeTestPlanReport.jsx b/client/components/Reports/SummarizeTestPlanReport.jsx index e67bcc326..4b6509ea8 100644 --- a/client/components/Reports/SummarizeTestPlanReport.jsx +++ b/client/components/Reports/SummarizeTestPlanReport.jsx @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { getTestPlanTargetTitle, getTestPlanVersionTitle } from './getTitles'; -import { Breadcrumb, Button, Container } from 'react-bootstrap'; +import { Breadcrumb, Button, Container, Table } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -12,11 +12,12 @@ import { } from '@fortawesome/free-solid-svg-icons'; import styled from '@emotion/styled'; import { getMetrics } from 'shared'; +import { none } from './None'; import { convertDateToString } from '../../utils/formatter'; import DisclaimerInfo from '../DisclaimerInfo'; import TestPlanResultsTable from '../common/TestPlanResultsTable'; import DisclosureComponent from '../common/DisclosureComponent'; -import { Navigate, useLocation, useParams } from 'react-router-dom'; +import { Link, Navigate, useLocation, useParams } from 'react-router-dom'; import createIssueLink from '../../utils/createIssueLink'; const ResultsContainer = styled.div` @@ -90,13 +91,147 @@ const SummarizeTestPlanReport = ({ testPlanVersion, testPlanReports }) => { ); if (!testPlanReport) return ; - const { at, browser } = testPlanReport; + const { at, browser, recommendedAtVersion } = testPlanReport; + const overallMetrics = getMetrics({ testPlanReport }); // Construct testPlanTarget const testPlanTarget = { id: `${at.id}${browser.id}`, at, - browser + browser, + atVersion: recommendedAtVersion + }; + + const renderVersionsSummaryTable = () => { + if (testPlanVersion.phase !== 'RECOMMENDED') return null; + + const title = `${testPlanTarget.at.name} Versions Summary`; + const testPlanReportsForTarget = testPlanVersion.testPlanReports.filter( + testPlanReport => + testPlanReport.at.id === at.id && + testPlanReport.browser.id === browser.id + ); + testPlanReportsForTarget.sort( + (a, b) => + new Date(b.recommendedAtVersion.releasedAt) - + new Date(a.recommendedAtVersion.releasedAt) + ); + + return ( + <> +

    {title}

    +

    + The following table displays a summary of data for all + versions of {testPlanTarget.at.name} that have been tested. +

    + + + + + + + + + + + {testPlanReportsForTarget.map(testPlanReport => { + const { recommendedAtVersion, metrics } = + testPlanReport; + const { + mustFormatted, + shouldFormatted, + mayFormatted + } = metrics; + + return ( + + + + + + + ); + })} + +
    VersionsMust-Have BehaviorsShould-Have BehaviorsMay-Have Behaviors
    + {testPlanReportId === + testPlanReport.id ? ( + <>{recommendedAtVersion.name} + ) : ( + + {recommendedAtVersion.name} + + )} + {mustFormatted || none}{shouldFormatted || none}{mayFormatted || none}
    + + ); + }; + + const renderResultsForTargetTable = () => { + const title = `Results for ${getTestPlanTargetTitle(testPlanTarget)}`; + return ( + <> +

    {title}

    + + + + + + + + + + + {testPlanReport.finalizedTestResults.map(testResult => { + const { + mustFormatted, + shouldFormatted, + mayFormatted + } = getMetrics({ + testResult + }); + return ( + + + + + + + ); + })} + + + + + + + +
    Test NameMust-Have BehaviorsShould-Have BehaviorsMay-Have Behaviors
    + + {testResult.test.title} + + {mustFormatted || none}{shouldFormatted || none}{mayFormatted || none}
    All Tests{overallMetrics.mustFormatted || none}{overallMetrics.shouldFormatted || none}{overallMetrics.mayFormatted || none}
    + + ); }; return ( @@ -176,6 +311,10 @@ const SummarizeTestPlanReport = ({ testPlanVersion, testPlanReports }) => { ) : null} + + {renderVersionsSummaryTable()} + {renderResultsForTargetTable()} + {testPlanReport.finalizedTestResults.map((testResult, index) => { const test = testResult.test; @@ -186,6 +325,7 @@ const SummarizeTestPlanReport = ({ testPlanVersion, testPlanReports }) => { versionString: testPlanVersion.versionString, testTitle: test.title, testRowNumber: test.rowNumber, + testSequenceNumber: index + 1, testRenderedUrl: test.renderedUrl, atName: testPlanReport.at.name, atVersionName: testResult.atVersion.name, diff --git a/client/components/Reports/SummarizeTestPlanVersion.jsx b/client/components/Reports/SummarizeTestPlanVersion.jsx index 55ce16dae..8997d6e46 100644 --- a/client/components/Reports/SummarizeTestPlanVersion.jsx +++ b/client/components/Reports/SummarizeTestPlanVersion.jsx @@ -19,6 +19,23 @@ const FullHeightContainer = styled(Container)` const SummarizeTestPlanVersion = ({ testPlanVersion, testPlanReports }) => { const { exampleUrl, designPatternUrl } = testPlanVersion.metadata; + + // Sort the test plan reports alphabetically by AT name first, then browser + const sortedTestPlanReports = testPlanReports.slice().sort((a, b) => { + const atNameA = a.at.name.toLowerCase(); + const atNameB = b.at.name.toLowerCase(); + const browserNameA = a.browser.name.toLowerCase(); + const browserNameB = b.browser.name.toLowerCase(); + + if (atNameA < atNameB) return -1; + if (atNameA > atNameB) return 1; + + if (browserNameA < browserNameB) return -1; + if (browserNameA > browserNameB) return 1; + + return 0; + }); + return ( @@ -80,17 +97,18 @@ const SummarizeTestPlanVersion = ({ testPlanVersion, testPlanReports }) => { ) : null} - {testPlanReports.map(testPlanReport => { + {sortedTestPlanReports.map(testPlanReport => { if (testPlanReport.status === 'DRAFT') return null; const overallMetrics = getMetrics({ testPlanReport }); - const { at, browser } = testPlanReport; + const { at, browser, recommendedAtVersion } = testPlanReport; // Construct testPlanTarget const testPlanTarget = { id: `${at.id}${browser.id}`, at, - browser + browser, + atVersion: recommendedAtVersion }; return ( @@ -126,9 +144,9 @@ const SummarizeTestPlanVersion = ({ testPlanVersion, testPlanReports }) => { Test Name - MUST HAVE Behaviors - SHOULD HAVE Behaviors - MAY HAVE Behaviors + Must-Have Behaviors + Should-Have Behaviors + May-Have Behaviors @@ -187,7 +205,7 @@ const SummarizeTestPlanVersion = ({ testPlanVersion, testPlanReports }) => { SummarizeTestPlanVersion.propTypes = { testPlanVersion: PropTypes.shape({ gitSha: PropTypes.string, - testPlan: PropTypes.string, + testPlan: PropTypes.object, directory: PropTypes.string, versionString: PropTypes.string, id: PropTypes.string.isRequired, diff --git a/client/components/Reports/getTitles.js b/client/components/Reports/getTitles.js index 9399d564f..8935badb5 100644 --- a/client/components/Reports/getTitles.js +++ b/client/components/Reports/getTitles.js @@ -1,13 +1,16 @@ -const getTestPlanVersionTitle = testPlanVersion => { - return testPlanVersion.title || `"${testPlanVersion.testPlan.directory}"`; +const getTestPlanVersionTitle = ( + testPlanVersion, + { includeVersionString = false } = {} +) => { + let title = testPlanVersion.title || testPlanVersion.testPlan?.directory; + if (includeVersionString && testPlanVersion.versionString) + title = `${title} ${testPlanVersion.versionString}`; + return title; }; -// const getTestPlanTargetTitle = ({ browser, browserVersion, at, atVersion }) => { -// return `${at.name} ${atVersion} and ${browser.name} ${browserVersion}`; -// }; - -const getTestPlanTargetTitle = ({ browser, at }) => { - return `${at.name} and ${browser.name}`; +const getTestPlanTargetTitle = ({ at, browser, atVersion }) => { + if (!atVersion) return `${at.name} and ${browser.name}`; + return `${at.name} ${atVersion.name} and ${browser.name}`; }; export { getTestPlanTargetTitle, getTestPlanVersionTitle }; diff --git a/client/components/Reports/queries.js b/client/components/Reports/queries.js index 90afc509a..65f0799a6 100644 --- a/client/components/Reports/queries.js +++ b/client/components/Reports/queries.js @@ -52,6 +52,11 @@ export const REPORT_PAGE_QUERY = gql` id name } + recommendedAtVersion { + id + name + releasedAt + } runnableTests { id title diff --git a/client/components/TestPlanReportStatusDialog/WithButton.jsx b/client/components/TestPlanReportStatusDialog/WithButton.jsx index 8e17d674a..a82215b88 100644 --- a/client/components/TestPlanReportStatusDialog/WithButton.jsx +++ b/client/components/TestPlanReportStatusDialog/WithButton.jsx @@ -2,8 +2,7 @@ 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 { calculatePercentComplete } from '../../utils/calculatePercentComplete'; import styled from '@emotion/styled'; import ReportStatusDot from '../common/ReportStatusDot'; import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from './queries'; @@ -26,7 +25,10 @@ const TestPlanReportStatusDialogButton = styled(Button)` margin-top: auto; `; -const TestPlanReportStatusDialogWithButton = ({ ats, testPlanVersionId }) => { +const TestPlanReportStatusDialogWithButton = ({ + testPlanVersionId, + triggerUpdate: refetchOther +}) => { const { data: { testPlanVersion } = {}, refetch, @@ -39,60 +41,48 @@ const TestPlanReportStatusDialogWithButton = ({ ats, testPlanVersionId }) => { 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 { testPlanReportStatuses } = testPlanVersion ?? {}; const buttonLabel = useMemo(() => { - const initialCounts = { completed: 0, inProgress: 0, missing: 0 }; + if (!testPlanReportStatuses) return; - 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 counts = { completed: 0, inProgress: 0, missing: 0 }; + + testPlanReportStatuses.forEach(status => { + if (!status.isRequired) return; + + const { testPlanReport } = status; + + if (testPlanReport) { const percentComplete = - calculateTestPlanReportCompletionPercentage(matchingReport); - if (percentComplete === 100 && matchingReport.markedFinalAt) { - acc.completed++; + calculatePercentComplete(testPlanReport); + + if (percentComplete === 100 && testPlanReport.markedFinalAt) { + counts.completed += 1; } else { - acc.inProgress++; + counts.inProgress += 1; } } else { - acc.missing++; + counts.missing += 1; } - return acc; - }, initialCounts); + }); - // All AT/browser pairs that require a report have a complete report - if (counts.completed === requiredReports.length) { + if (counts.missing === 0 && counts.inProgress === 0) { 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) { + } else if (counts.missing === 0 && counts.inProgress !== 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 && + } else if ( + counts.missing !== 0 && (counts.completed > 0 || counts.inProgress > 0) ) { return ( @@ -101,18 +91,19 @@ const TestPlanReportStatusDialogWithButton = ({ ats, testPlanVersionId }) => { 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) { + } else if ( + counts.missing !== 0 && + counts.completed === 0 && + counts.inProgress === 0 + ) { return ( Required Reports Not Started ); - } - // Fallback case - else { + } else { + // Fallback case return ( @@ -120,7 +111,7 @@ const TestPlanReportStatusDialogWithButton = ({ ats, testPlanVersionId }) => { ); } - }, [requiredReports, testPlanReports]); + }, [testPlanReportStatuses]); if ( loading || @@ -148,34 +139,18 @@ const TestPlanReportStatusDialogWithButton = ({ ats, testPlanVersionId }) => { setShowDialog(false); buttonRef.current.focus(); }} - triggerUpdate={refetch} - ats={ats} + triggerUpdate={async () => { + await refetch(); + if (refetchOther) await refetchOther(); + }} /> ); }; TestPlanReportStatusDialogWithButton.propTypes = { - ats: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - browsers: PropTypes.arrayOf( - PropTypes.shape({ id: PropTypes.string.isRequired }).isRequired - ).isRequired, - candidateBrowsers: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired - }).isRequired - ).isRequired, - recommendedBrowsers: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired - }).isRequired - ).isRequired - }).isRequired - ).isRequired, - testPlanVersionId: PropTypes.string.isRequired + testPlanVersionId: PropTypes.string.isRequired, + triggerUpdate: PropTypes.func }; export default TestPlanReportStatusDialogWithButton; diff --git a/client/components/TestPlanReportStatusDialog/index.jsx b/client/components/TestPlanReportStatusDialog/index.jsx index 493f87db6..344e00281 100644 --- a/client/components/TestPlanReportStatusDialog/index.jsx +++ b/client/components/TestPlanReportStatusDialog/index.jsx @@ -1,25 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from '@emotion/styled'; import AddTestToQueueWithConfirmation from '../AddTestToQueueWithConfirmation'; import { useQuery } from '@apollo/client'; import { ME_QUERY } from '../App/queries'; import { evaluateAuth } from '../../utils/evaluateAuth'; -import { calculateTestPlanReportCompletionPercentage } from './calculateTestPlanReportCompletionPercentage'; -import { convertDateToString } from '../../utils/formatter'; import { ThemeTable } from '../common/ThemeTable'; import BasicModal from '../common/BasicModal'; import './TestPlanReportStatusDialog.css'; - -const IncompleteStatusReport = styled.span` - min-width: 5rem; - display: inline-block; -`; +import ReportStatusSummary from '../common/ReportStatusSummary'; +import { AtVersion } from '../common/AtBrowserVersion'; const TestPlanReportStatusDialog = ({ testPlanVersion, show, - ats, handleHide = () => {}, triggerUpdate = () => {} }) => { @@ -27,133 +20,57 @@ const TestPlanReportStatusDialog = ({ fetchPolicy: 'cache-and-network' }); - const { testPlanReports } = testPlanVersion; - - const auth = evaluateAuth(me ?? {}); + const auth = evaluateAuth(me); const { isSignedIn, isAdmin } = auth; - 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 = ({ report, at, browser }) => { - if (report) { - const { markedFinalAt } = report; - if (markedFinalAt) { - return renderCompleteReportStatus(report); - } else { - return renderPartialCompleteReportStatus(report); - } - } - return ( - <> - Missing - {isSignedIn && isAdmin ? ( - - ) : null} - - ); - }; + const { testPlanReportStatuses } = testPlanVersion; let requiredReports = 0; - const rowData = []; - ats.forEach(at => { - // DRAFT as well because those reports are required to be promoted to CANDIDATE - if ( - testPlanVersion.phase === 'DRAFT' || - testPlanVersion.phase === 'CANDIDATE' - ) { - requiredReports += at.candidateBrowsers.length; - } - if (testPlanVersion.phase === 'RECOMMENDED') { - requiredReports += at.recommendedBrowsers.length; - } + const tableRows = testPlanReportStatuses.map(status => { + const { + isRequired, + at, + browser, + minimumAtVersion, + exactAtVersion, + testPlanReport + } = status; - at.browsers.forEach(browser => { - const report = testPlanReports.find(eachReport => { - return ( - eachReport.at.id === at.id && - eachReport.browser.id === browser.id - ); - }); + if (isRequired) requiredReports += 1; - let isRequired = false; - if ( - testPlanVersion.phase === 'DRAFT' || - testPlanVersion.phase === 'CANDIDATE' - ) { - isRequired = at.candidateBrowsers.some(candidateBrowser => { - return candidateBrowser.id === browser.id; - }); - } else if (testPlanVersion.phase === 'RECOMMENDED') { - isRequired = at.recommendedBrowsers.some(recommendedBrowser => { - return recommendedBrowser.id === browser.id; - }); - } - rowData.push({ report, at, browser, isRequired }); - }); - }); + const key = + `${at.name}-${browser.name}-` + + `${minimumAtVersion?.id ?? exactAtVersion?.id}-` + + `${testPlanReport?.id ?? 'missing'}`; - // Sort by required then AT then browser - rowData.sort((a, b) => { - if (a.isRequired !== b.isRequired) return a.isRequired ? -1 : 1; - if (a.at.name !== b.at.name) return a.at.name.localeCompare(b.at.name); - return a.browser.name.localeCompare(b.browser.name); - }); - const tableRows = rowData.map(({ report, at, browser, isRequired }) => { return ( - + {isRequired ? 'Yes' : 'No'} - {at.name} + + + {browser.name} - {renderReportStatus({ report, at, browser })} + + + {isSignedIn && isAdmin && !testPlanReport ? ( + + ) : null} + ); }); @@ -237,12 +154,8 @@ TestPlanReportStatusDialog.propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, phase: PropTypes.string.isRequired, - testPlanReports: PropTypes.arrayOf( + testPlanReportStatuses: 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 @@ -250,32 +163,27 @@ TestPlanReportStatusDialog.propTypes = { browser: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired - }).isRequired + }).isRequired, + minimumAtVersion: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }), + exactAtVersion: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }), + testPlanReport: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string, + runnableTests: PropTypes.arrayOf(PropTypes.object), + finalizedTestResults: PropTypes.arrayOf(PropTypes.object) + }) }).isRequired ).isRequired }).isRequired, handleHide: PropTypes.func.isRequired, triggerUpdate: PropTypes.func, - show: PropTypes.bool.isRequired, - ats: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - browsers: PropTypes.arrayOf( - PropTypes.shape({ id: PropTypes.string.isRequired }).isRequired - ).isRequired, - candidateBrowsers: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired - }).isRequired - ).isRequired, - recommendedBrowsers: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired - }).isRequired - ).isRequired - }).isRequired - ).isRequired + show: PropTypes.bool.isRequired }; export default TestPlanReportStatusDialog; diff --git a/client/components/TestPlanReportStatusDialog/isRequired.js b/client/components/TestPlanReportStatusDialog/isRequired.js deleted file mode 100644 index bf2281e46..000000000 --- a/client/components/TestPlanReportStatusDialog/isRequired.js +++ /dev/null @@ -1,60 +0,0 @@ -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 index 4a7b4e044..80a08ffed 100644 --- a/client/components/TestPlanReportStatusDialog/queries.js +++ b/client/components/TestPlanReportStatusDialog/queries.js @@ -16,44 +16,57 @@ export const TEST_PLAN_REPORT_STATUS_DIALOG_QUERY = gql` testPlan { directory } - testPlanReports { - id - metrics - isFinal - markedFinalAt + testPlanReportStatuses { + isRequired at { id + key name } browser { id + key name } - issues { - link - isOpen - feedbackType + minimumAtVersion { + id + name } - draftTestPlanRuns { - tester { - username - } - testPlanReport { - id + exactAtVersion { + id + name + } + testPlanReport { + id + metrics + isFinal + markedFinalAt + issues { + link + isOpen + feedbackType } - testResults { - test { - id + draftTestPlanRuns { + tester { + username } - atVersion { + testPlanReport { id - name } - browserVersion { - id - name + testResults { + test { + id + } + atVersion { + id + name + } + browserVersion { + id + name + } + completedAt } - completedAt } } } diff --git a/client/components/TestPlanVersionsPage/index.jsx b/client/components/TestPlanVersionsPage/index.jsx index d14b2f62c..25c4a1141 100644 --- a/client/components/TestPlanVersionsPage/index.jsx +++ b/client/components/TestPlanVersionsPage/index.jsx @@ -21,85 +21,16 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import DisclosureComponentUnstyled from '../common/DisclosureComponent'; +import useForceUpdate from '../../hooks/useForceUpdate'; const DisclosureContainer = styled.div` - // Following directives are related to the ManageTestQueue component - > span { - display: block; - margin-bottom: 1rem; - } - - // Add Test Plan to Test Queue button - > button { - display: flex; - padding: 0.5rem 1rem; - margin-top: 1rem; - margin-left: auto; - margin-right: 0; - } - - .disclosure-row-manage-ats { - display: grid; - grid-auto-flow: column; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - grid-gap: 1rem; - - .ats-container { - grid-column: 1 / span 2; - } - - .at-versions-container { - display: flex; - flex-direction: column; - grid-column: 3 / span 3; - } - - .disclosure-buttons-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - - > button { - margin: 0; - padding: 0; - color: #275caa; - border: none; - background-color: transparent; - - &:nth-of-type(2) { - margin-left: auto; - } - - // remove button - &:nth-of-type(3) { - margin-left: 1rem; - color: #ce1b4c; - } - } - } - } - - .disclosure-row-test-plans { - display: grid; - grid-auto-flow: column; - grid-template-columns: 1fr 1fr 1fr 1fr; - grid-gap: 1rem; - } - - .disclosure-form-label { - font-weight: bold; - font-size: 1rem; - } - .timeline-for-version-table { padding: 0.5rem 1rem; } `; const DisclosureComponent = styled(DisclosureComponentUnstyled)` - & h2 { - padding: 0; - margin: 0; + h2 { font-size: 1.25em; button { @@ -145,13 +76,6 @@ const ThemeTableHeader = styled(UnstyledThemeTableHeader)` margin: 0 !important; `; -// https://stackoverflow.com/a/53215514 -const useForceUpdate = () => { - const [, updateState] = React.useState(); - const forceUpdate = React.useCallback(() => updateState({}), []); - return forceUpdate; -}; - const TestPlanVersionsPage = () => { const { testPlanDirectory } = useParams(); @@ -618,7 +542,6 @@ const TestPlanVersionsPage = () => { )} on ${getEventDate(testPlanVersion)}`} > { Test Plan + + AT Version Requirements + Testers Report Status Actions diff --git a/client/components/TestQueue/queries.js b/client/components/TestQueue/queries.js index f0abcd495..6dcf3a0ce 100644 --- a/client/components/TestQueue/queries.js +++ b/client/components/TestQueue/queries.js @@ -64,6 +64,14 @@ export const TEST_QUEUE_PAGE_QUERY = gql` key name } + minimumAtVersion { + id + name + } + exactAtVersion { + id + name + } browser { id key @@ -117,6 +125,14 @@ export const TEST_PLAN_REPORT_QUERY = gql` key name } + minimumAtVersion { + id + name + } + exactAtVersion { + id + name + } browser { id key @@ -236,33 +252,32 @@ export const ADD_TEST_QUEUE_MUTATION = gql` mutation AddTestPlanReport( $testPlanVersionId: ID! $atId: ID! + $exactAtVersionId: ID + $minimumAtVersionId: ID $browserId: ID! - $copyResultsFromTestPlanReportId: ID + $copyResultsFromTestPlanVersionId: ID ) { - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: $testPlanVersionId atId: $atId + exactAtVersionId: $exactAtVersionId + minimumAtVersionId: $minimumAtVersionId browserId: $browserId - copyResultsFromTestPlanReportId: $copyResultsFromTestPlanReportId + copyResultsFromTestPlanVersionId: $copyResultsFromTestPlanVersionId } ) { - populatedData { - testPlanReport { + testPlanReport { + id + at { id - at { - id - } - browser { - id - } } - testPlanVersion { + browser { id } } - created { - locationOfData + testPlanVersion { + id } } } @@ -291,10 +306,13 @@ export const ASSIGN_TESTER_MUTATION = gql` } `; -export const UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION = gql` - mutation UpdateTestPlanReportMarkedFinalAt($testReportId: ID!) { +export const MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION = gql` + mutation MarkTestPlanReportAsFinal( + $testReportId: ID! + $primaryTestPlanRunId: ID! + ) { testPlanReport(id: $testReportId) { - markAsFinal { + markAsFinal(primaryTestPlanRunId: $primaryTestPlanRunId) { testPlanReport { markedFinalAt } diff --git a/client/components/TestQueue2/Actions.jsx b/client/components/TestQueue2/Actions.jsx new file mode 100644 index 000000000..c4f952b3d --- /dev/null +++ b/client/components/TestQueue2/Actions.jsx @@ -0,0 +1,349 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import useConfirmationModal from '../../hooks/useConfirmationModal'; +import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; +import { useApolloClient } from '@apollo/client'; +import styled from '@emotion/styled'; +import { Button, Dropdown, Form } from 'react-bootstrap'; +import BasicModal from '../common/BasicModal'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faSquareCheck, + faFileImport, + faTrashAlt +} from '@fortawesome/free-solid-svg-icons'; +import { + MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION, + REMOVE_TEST_PLAN_REPORT_MUTATION, + TEST_QUEUE_PAGE_QUERY + // TEST_PLAN_REPORT_QUERY +} from './queries'; +import useForceUpdate from '../../hooks/useForceUpdate'; +import BasicThemedModal from '../common/BasicThemedModal'; +import { evaluateAuth } from '../../utils/evaluateAuth'; +import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '../TestPlanReportStatusDialog/queries'; +import ManageBotRunDialogWithButton from '@components/ManageBotRunDialog/WithButton'; + +const ActionContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; +`; + +const Actions = ({ + me, + testPlan, + testPlanReport, + testers = [], + triggerUpdate = () => {} +}) => { + const primaryRunIdRef = useRef({}); + + const { showConfirmationModal, hideConfirmationModal } = + useConfirmationModal(); + + const { triggerLoad, loadingMessage } = useTriggerLoad(); + + const forceUpdate = useForceUpdate(); + + const client = useApolloClient(); + + const { isAdmin, isTester } = evaluateAuth(me); + + const selfAssignedRun = + me && + testPlanReport.draftTestPlanRuns.find( + testPlanRun => testPlanRun.tester.id === me.id + ); + + const nonSelfAssignedRuns = testPlanReport.draftTestPlanRuns + .filter(testPlanRun => testPlanRun.tester.id !== me?.id) + .sort((a, b) => a.tester.username.localeCompare(b.tester.username)); + + const completedAllTests = testPlanReport.draftTestPlanRuns.every( + testPlanRun => + testPlanRun.testResultsLength === testPlanReport.runnableTestsLength + ); + + const assignedBotRun = testPlanReport.draftTestPlanRuns.find( + testPlanRun => testPlanRun.tester.isBot + ); + + const canMarkAsFinal = + !assignedBotRun && + !testPlanReport.conflictsLength && + testPlanReport.draftTestPlanRuns.length > 0 && + testPlanReport.draftTestPlanRuns[0].testResultsLength > 0 && + completedAllTests; + + const markAsFinal = () => { + const runs = testPlanReport.draftTestPlanRuns; + + primaryRunIdRef.current = runs[0].id; + + const onChangePrimary = event => { + const id = event.target.value; + primaryRunIdRef.current = id; + forceUpdate(); + }; + + const onConfirm = async () => { + await triggerLoad(async () => { + await client.mutate({ + mutation: MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION, + refetchQueries: [ + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true, + variables: { + testPlanReportId: testPlanReport.id, + primaryTestPlanRunId: primaryRunIdRef.current + } + }); + }, 'Marking as Final ...'); + + hideConfirmationModal(); + }; + + let title; + let content; + + if (runs.length === 1) { + title = + "Are you sure you want to mark as final with a single tester's results?"; + content = ( + <> +

    + Only {runs[0].tester.username}'s results are + included in this report, so their run will be marked as + the primary run. Only their output will be displayed on + report pages. +

    +

    + Their run being marked as primary may also set the + minimum required Assistive Technology Version that can + be used for subsequent reports with this Test Plan + Version and Assistive Technology combination. +

    + + ); + } else { + // Multiple testers runs to choose from + title = 'Select Primary Test Plan Run'; + content = ( + <> +

    + When a tester's run is marked as primary, it means + that their output for collected results will be + prioritized and shown on report pages. +

    +

    + A tester's run being marked as primary may also set + the minimum required Assistive Technology Version that + can be used for subsequent reports with that Test Plan + Version and Assistive Technology combination. +

    + + {runs.map(run => ( + + ))} + + + ); + } + + showConfirmationModal( + + ); + }; + + const deleteReport = () => { + const onConfirm = async () => { + await triggerLoad(async () => { + await client.mutate({ + mutation: REMOVE_TEST_PLAN_REPORT_MUTATION, + refetchQueries: [ + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true, + variables: { testPlanReportId: testPlanReport.id } + }); + }, 'Deleting...'); + + hideConfirmationModal(); + + setTimeout(() => { + const focusTarget = + document.querySelector(`h2#${testPlan.directory}`) ?? + document.querySelector('#main'); + + focusTarget.focus(); + }, 1); + }; + + showConfirmationModal( + hideConfirmationModal()} + /> + ); + }; + + return ( + + + {!isTester && ( + + )} + {isTester && ( + + )} + {isAdmin && ( + + + + Open run as...  + + + {nonSelfAssignedRuns.map(testPlanRun => ( + + {testPlanRun.tester.username} + + ))} + + + )} + {isAdmin && assignedBotRun && ( + + )} + {isAdmin && ( + + )} + {isAdmin && ( + + )} + + + ); +}; + +Actions.propTypes = { + me: PropTypes.shape({ + id: PropTypes.string.isRequired, + username: PropTypes.string.isRequired + }), + testPlan: PropTypes.shape({ + directory: PropTypes.string.isRequired + }).isRequired, + testPlanReport: PropTypes.shape({ + id: PropTypes.string.isRequired, + runnableTestsLength: PropTypes.number.isRequired, + conflictsLength: PropTypes.number.isRequired, + draftTestPlanRuns: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + testResultsLength: PropTypes.number.isRequired, + tester: PropTypes.shape({ + id: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + isBot: PropTypes.bool.isRequired + }) + }) + ).isRequired + }).isRequired, + testers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + isBot: PropTypes.bool.isRequired, + ats: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired + }) + ) + }) + ).isRequired, + triggerUpdate: PropTypes.func.isRequired +}; + +export default Actions; diff --git a/client/components/TestQueue2/AssignTesters.jsx b/client/components/TestQueue2/AssignTesters.jsx new file mode 100644 index 000000000..60afd6098 --- /dev/null +++ b/client/components/TestQueue2/AssignTesters.jsx @@ -0,0 +1,382 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { useApolloClient } from '@apollo/client'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck, faRobot, faUser } from '@fortawesome/free-solid-svg-icons'; +import { Button, Dropdown } from 'react-bootstrap'; +import CompletionStatusListItem from './CompletionStatusListItem'; +import useConfirmationModal from '../../hooks/useConfirmationModal'; +import BasicThemedModal from '../common/BasicThemedModal'; +import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; +import { useAriaLiveRegion } from '../providers/AriaLiveRegionProvider'; +import { evaluateAuth } from '../../utils/evaluateAuth'; +import { isSupportedByResponseCollector } from '../../utils/automation'; +import { + ASSIGN_TESTER_MUTATION, + DELETE_TEST_PLAN_RUN, + TEST_QUEUE_PAGE_QUERY +} from './queries'; +import { SCHEDULE_COLLECTION_JOB_MUTATION } from '../AddTestToQueueWithConfirmation/queries'; +import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '../TestPlanReportStatusDialog/queries'; + +const AssignTestersContainer = styled.div` + display: flex; + + [role='menu'] { + width: 250px; + max-height: 200px; + overflow-y: scroll; + } + + [role='menuitem']:active { + background-color: #0b60ab; + } +`; + +const AssignTestersDropdownButton = styled(Dropdown.Toggle)` + width: min-content !important; + margin-right: 0.5rem; +`; + +const AssignedTestersUl = styled.ul` + font-weight: normal; + padding-top: 0.5rem; + text-align: center; + + li:not(:last-of-type) { + padding-bottom: 0.25rem; + } + a, + span { + font-weight: normal; + padding-right: 0.5rem; + } + em { + color: rgb(var(--bs-secondary-rgb)); + font-style: normal; + display: inline-block; + } +`; + +const AssignTesters = ({ me, testers, testPlanReport }) => { + const { triggerLoad, loadingMessage } = useTriggerLoad(); + + const setAlertMessage = useAriaLiveRegion(); + + const { showConfirmationModal, hideConfirmationModal } = + useConfirmationModal(); + + const client = useApolloClient(); + + const dropdownButtonRef = useRef(); + const assignSelfButtonRef = useRef(); + + const { isAdmin, isTester } = evaluateAuth(me); + + const isSelfAssigned = + me && + testPlanReport.draftTestPlanRuns.some( + testPlanRun => testPlanRun.tester.id === me.id + ); + + const onToggle = isShown => { + setTimeout(() => { + if (!isShown) return; + document + .querySelector( + `#assign-testers-${testPlanReport.id} [role="menuitemcheckbox"]` + ) + .focus(); + }, 1); + }; + + const onKeyDown = event => { + const { key } = event; + if (key.match(/[0-9a-zA-Z]/)) { + const container = event.target.closest('[role=menu]'); + const matchingMenuItem = Array.from(container.children).find( + menuItem => { + return menuItem.innerText + .trim() + .toLowerCase() + .startsWith(key.toLowerCase()); + } + ); + + if (matchingMenuItem) { + matchingMenuItem.focus(); + } + } + }; + + const toggleTesterCommon = async ({ + testerId, + assignedCallback, + unassignedCallback + }) => { + const tester = testers.find(tester => tester.id === testerId); + + const isAssigned = testPlanReport.draftTestPlanRuns.some( + testPlanRun => testPlanRun.tester.id === testerId + ); + + if (isAssigned) { + const isSelfAssigned = tester.id === me.id; + const testerFormatted = isSelfAssigned + ? 'your' + : `${tester.username}'s`; + + const onConfirm = async () => { + await triggerLoad(async () => { + await client.mutate({ + mutation: DELETE_TEST_PLAN_RUN, + refetchQueries: [ + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true, + variables: { + testReportId: testPlanReport.id, + testerId: tester.id + } + }); + }, 'Deleting...'); + + hideConfirmationModal(); + + await assignedCallback?.({ tester }); + }; + + showConfirmationModal( + hideConfirmationModal()} + /> + ); + + return; + } + + await triggerLoad(async () => { + if (tester.isBot) { + await client.mutate({ + mutation: SCHEDULE_COLLECTION_JOB_MUTATION, + refetchQueries: [ + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true, + variables: { + testPlanReportId: testPlanReport.id + } + }); + } else { + await client.mutate({ + mutation: ASSIGN_TESTER_MUTATION, + refetchQueries: [ + TEST_QUEUE_PAGE_QUERY, + TEST_PLAN_REPORT_STATUS_DIALOG_QUERY + ], + awaitRefetchQueries: true, + variables: { + testReportId: testPlanReport.id, + testerId: tester.id + } + }); + } + }, 'Assigning...'); + + await unassignedCallback?.({ tester }); + }; + + const onSelect = testerId => { + const assignedCallback = ({ tester }) => { + setAlertMessage(`${tester.username}'s run has been deleted`); + + setTimeout(() => { + dropdownButtonRef.current.focus(); + }, 1); + }; + + const unassignedCallback = ({ tester }) => { + setAlertMessage(`Assigned ${tester.username}`); + + setTimeout(() => { + dropdownButtonRef.current.focus(); + }, 1); + }; + + toggleTesterCommon({ testerId, assignedCallback, unassignedCallback }); + }; + + const onToggleSelf = () => { + const assignedCallback = () => { + setTimeout(() => { + assignSelfButtonRef.current.focus(); + }, 1); + }; + + toggleTesterCommon({ + testerId: me.id, + assignedCallback + }); + }; + + const renderDropdownItem = ({ tester }) => { + const { id, username, isBot, ats } = tester; + + if (isBot) { + const foundAtForBot = ats.find( + ({ id }) => id === testPlanReport.at?.id + ); + const supportedByResponseCollector = isSupportedByResponseCollector( + { + id: testPlanReport.id, + at: testPlanReport.at, + browser: testPlanReport.browser + } + ); + if (!foundAtForBot || !supportedByResponseCollector) return null; + } + + const isAssigned = testPlanReport.draftTestPlanRuns.some( + testPlanRun => testPlanRun.tester.username === username + ); + + let icon; + if (isAssigned) icon = faCheck; + else if (isBot) icon = faRobot; + + return ( + + + {icon && } + {`${username}`} + + + ); + }; + + return ( + + + {isAdmin && ( + + + Assign Testers + + + + {testers.map(tester => + renderDropdownItem({ tester }) + )} + + + )} + {isTester && ( + + )} + + + {testPlanReport.draftTestPlanRuns + .slice() + .sort((a, b) => + a.tester.username.localeCompare(b.tester.username) + ) + .map((testPlanRun, index) => { + const tester = testPlanRun.tester; + const rowId = `plan-${testPlanReport.id}-assignee-${tester.id}-run-${testPlanRun.id}`; + return ( + + + {index === + testPlanReport.draftTestPlanRuns.length - + 1 ? null : ( +
    + )} +
    + ); + })} +
    +
    + ); +}; + +AssignTesters.propTypes = { + me: PropTypes.shape({ + id: PropTypes.string.isRequired + }), + testers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + isBot: PropTypes.bool.isRequired, + ats: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + key: PropTypes.string.isRequired + }) + ) + }) + ).isRequired, + testPlanReport: PropTypes.shape({ + id: PropTypes.string.isRequired, + runnableTestsLength: PropTypes.number.isRequired, + draftTestPlanRuns: PropTypes.arrayOf( + PropTypes.shape({ + tester: PropTypes.shape({ id: PropTypes.string.isRequired }) + .isRequired + }) + ).isRequired, + at: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + key: PropTypes.string.isRequired + }).isRequired, + browser: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + key: PropTypes.string.isRequired + }).isRequired + }).isRequired +}; + +export default AssignTesters; diff --git a/client/components/TestQueue2/CompletionStatusListItem/index.jsx b/client/components/TestQueue2/CompletionStatusListItem/index.jsx new file mode 100644 index 000000000..9e55a58a1 --- /dev/null +++ b/client/components/TestQueue2/CompletionStatusListItem/index.jsx @@ -0,0 +1,88 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faRobot } from '@fortawesome/free-solid-svg-icons'; +import BotTestCompletionStatus from '@components/TestQueueCompletionStatusListItem/BotTestCompletionStatus'; +import PreviouslyAutomatedTestCompletionStatus from '@components/TestQueueCompletionStatusListItem/PreviouslyAutomatedTestCompletionStatus'; + +const CompletionStatusListItem = ({ + rowId, + testPlanReport, + testPlanRun, + tester +}) => { + const { username, isBot } = tester; + const testPlanRunPreviouslyAutomated = useMemo( + () => testPlanRun.initiatedByAutomation, + [testPlanRun] + ); + + let info; + let completionStatus; + + if (isBot) { + info = ( + + + {username} + + ); + completionStatus = ( + + ); + } else { + info = ( + + {username} + + ); + + completionStatus = testPlanRunPreviouslyAutomated ? ( + + ) : ( + + {`${testPlanRun.testResultsLength} of ` + + `${testPlanReport.runnableTestsLength} tests complete`} + + ); + } + + return ( +
  • + {info} + {completionStatus} +
  • + ); +}; + +// TODO: Update shape for testPlanReport and tester +CompletionStatusListItem.propTypes = { + rowId: PropTypes.string.isRequired, + testPlanReport: PropTypes.object.isRequired, + testPlanRun: PropTypes.shape({ + id: PropTypes.string.isRequired, + testResultsLength: PropTypes.number.isRequired, + initiatedByAutomation: PropTypes.bool.isRequired, + tester: PropTypes.shape({ + username: PropTypes.string.isRequired, + isBot: PropTypes.bool.isRequired + }).isRequired + }).isRequired, + tester: PropTypes.object.isRequired +}; + +export default CompletionStatusListItem; diff --git a/client/components/TestQueue2/TestQueue2.test.js b/client/components/TestQueue2/TestQueue2.test.js new file mode 100644 index 000000000..3dee29794 --- /dev/null +++ b/client/components/TestQueue2/TestQueue2.test.js @@ -0,0 +1,27 @@ +import getPage from '../../tests/util/getPage'; + +describe('Test Queue', () => { + const clearEntireTestQueue = () => { + console.error('TODO: IMPLEMENT clearEntireTestQueue'); + }; + + it('renders Test Queue page h1', async () => { + await getPage({ role: 'admin', url: '/test-queue' }, async page => { + const h1Element = await page.$eval( + 'h1', + element => element.textContent + ); + expect(h1Element).toBe('Test Queue'); + }); + }); + + it.skip('renders error message when no test plan reports exist', async () => { + await getPage({ role: 'admin', url: '/test-queue' }, async page => { + await clearEntireTestQueue(); + await page.waitForSelector( + '::-p-text(There are currently no test plan reports to show.)' + ); + }); + expect(true).toBe(false); + }); +}); diff --git a/client/components/TestQueue2/index.jsx b/client/components/TestQueue2/index.jsx new file mode 100644 index 000000000..72e3b162f --- /dev/null +++ b/client/components/TestQueue2/index.jsx @@ -0,0 +1,416 @@ +import React, { Fragment, useRef } from 'react'; +import { useApolloClient, useQuery } from '@apollo/client'; +import PageStatus from '../common/PageStatus'; +import { TEST_QUEUE_PAGE_QUERY } from './queries'; +import { Container, Table as BootstrapTable } from 'react-bootstrap'; +import { Helmet } from 'react-helmet'; +import { evaluateAuth } from '../../utils/evaluateAuth'; +import ManageTestQueue from '../ManageTestQueue'; +import DisclosureComponentUnstyled from '../common/DisclosureComponent'; +import useForceUpdate from '../../hooks/useForceUpdate'; +import styled from '@emotion/styled'; +import VersionString from '../common/VersionString'; +import PhasePill from '../common/PhasePill'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; +import TestPlanReportStatusDialogWithButton from '../TestPlanReportStatusDialog/WithButton'; +import ReportStatusSummary from '../common/ReportStatusSummary'; +import { AtVersion, BrowserVersion } from '../common/AtBrowserVersion'; +import { calculatePercentComplete } from '../../utils/calculatePercentComplete'; +import ProgressBar from '../common/ClippedProgressBar'; +import AssignTesters from './AssignTesters'; +import Actions from './Actions'; +import BotRunTestStatusList from '../BotRunTestStatusList'; + +const DisclosureComponent = styled(DisclosureComponentUnstyled)` + h3 { + font-size: 1rem; + + button { + font-size: unset; + font-weight: unset; + } + } + + [role='region'] { + padding: 0; + } +`; + +const MetadataContainer = styled.div` + display: flex; + gap: 1.25em; + margin: 0.5rem 1.25rem; + align-items: center; + min-height: 40px; /* temp because the status dialog button keeps disappearing */ + + & button { + margin-bottom: 0; + margin-top: 0; + font-size: 16px; + } + & button:hover { + color: white; + } + & button, + & button:focus { + color: #2e2f33; + } +`; + +const TableOverflowContainer = styled.div` + width: 100%; + + @media (max-width: 1080px) { + overflow-x: scroll; + } +`; + +const Table = styled(BootstrapTable)` + margin-bottom: 0; + + th { + padding: 0.75rem; + } + + th:first-of-type, + td:first-of-type { + border-left: none; + } + th:last-of-type, + td:last-of-type { + border-right: none; + } + tr:last-of-type, + tr:last-of-type td { + border-bottom: none; + } + + th:nth-of-type(1), + td:nth-of-type(1) { + min-width: 220px; + } + th:nth-of-type(2), + td:nth-of-type(2) { + min-width: 150px; + } + th:nth-of-type(3), + td:nth-of-type(3) { + min-width: 230px; + } + th:nth-of-type(4), + td:nth-of-type(4) { + width: 20%; + min-width: 125px; + } + th:nth-of-type(5), + td:nth-of-type(5) { + width: 20%; + min-width: 175px; + } +`; + +const StatusContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + text-align: center; + color: rgb(var(--bs-secondary-rgb)); +`; + +const TestQueue = () => { + const client = useApolloClient(); + const { data, error, refetch } = useQuery(TEST_QUEUE_PAGE_QUERY, { + fetchPolicy: 'cache-and-network' + }); + + const openDisclosuresRef = useRef({}); + const forceUpdate = useForceUpdate(); + + if (error) { + return ( + + ); + } + + if (!data) { + return ( + + ); + } + + const isSignedIn = !!data.me; + + const { isAdmin } = evaluateAuth(data.me); + + const testPlanVersions = []; + data.testPlans.forEach(testPlan => { + // testPlan.directory is needed by ManageTestQueue + const populatedTestPlanVersions = testPlan.testPlanVersions.map( + testPlanVersion => ({ + ...testPlanVersion, + testPlan: { directory: testPlan.directory } + }) + ); + testPlanVersions.push(...populatedTestPlanVersions); + }); + + // Remove any test plans or test plan versions without reports and sort + const sortTestPlanVersions = testPlanVersions => { + return [...testPlanVersions] + .filter(testPlanVersion => testPlanVersion.testPlanReports.length) + .sort((a, b) => { + return b.versionString.localeCompare(a.versionString); + }) + .map(testPlanVersion => { + return { + ...testPlanVersion, + testPlanReports: sortTestPlanReports( + testPlanVersion.testPlanReports + ) + }; + }); + }; + + const sortTestPlanReports = testPlanReports => { + return [...testPlanReports].sort((a, b) => { + if (a.at.name !== b.at.name) { + return a.at.name.localeCompare(b.at.name); + } + if (a.browser.name !== b.browser.name) { + return a.browser.name.localeCompare(b.browser.name); + } + const dateA = new Date( + (a.minimumAtVersion ?? a.exactAtVersion).releasedAt + ); + const dateB = new Date( + (a.minimumAtVersion ?? a.exactAtVersion).releasedAt + ); + return dateB - dateA; + }); + }; + + const testPlans = data.testPlans + .filter(testPlan => { + for (const testPlanVersion of testPlan.testPlanVersions) { + if (testPlanVersion.testPlanReports.length) return true; + } + }) + .map(testPlan => { + return { + ...testPlan, + testPlanVersions: sortTestPlanVersions( + testPlan.testPlanVersions + ) + }; + }) + .sort((a, b) => { + return a.title.localeCompare(b.title); + }); + + const testers = data.users + .filter(user => user.roles.includes('TESTER')) + .sort((a, b) => a.username.localeCompare(b.username)); + + const renderDisclosure = ({ testPlan }) => { + return ( + // TODO: fix the aria-label of this + ( + <> + + {testPlanVersion.versionString} + +   + + {testPlanVersion.phase} + + + ))} + onClick={testPlan.testPlanVersions.map( + testPlanVersion => () => { + const isOpen = + openDisclosuresRef.current[testPlanVersion.id]; + openDisclosuresRef.current[testPlanVersion.id] = + !isOpen; + forceUpdate(); + } + )} + expanded={testPlan.testPlanVersions.map( + testPlanVersion => + openDisclosuresRef.current[testPlanVersion.id] || false + )} + disclosureContainerView={testPlan.testPlanVersions.map( + testPlanVersion => + renderDisclosureContent({ testPlan, testPlanVersion }) + )} + /> + ); + }; + + const renderDisclosureContent = ({ testPlan, testPlanVersion }) => { + return ( + <> + + + + View tests in {testPlanVersion.versionString} + + + + + + + + + + + + + + + + {testPlanVersion.testPlanReports.map( + testPlanReport => + renderRow({ + testPlan, + testPlanVersion, + testPlanReport + }) + )} + +
    Assistive TechnologyBrowserTestersStatusActions
    +
    + + ); + }; + + const renderRow = ({ testPlan, testPlanVersion, testPlanReport }) => { + const percentComplete = calculatePercentComplete(testPlanReport); + const hasBotRun = testPlanReport.draftTestPlanRuns?.some( + ({ tester }) => tester.isBot + ); + + return ( + + + + + + + + + + + + + {} + + {hasBotRun ? ( + + ) : null} + + + + { + await client.refetchQueries({ + include: [ + 'TestQueuePage', + 'TestPlanReportStatusDialog' + ] + }); + + // Refocus on testers assignment dropdown button + const selector = `#assign-testers-${testPlanReport.id} button`; + document.querySelector(selector).focus(); + }} + /> + + + ); + }; + + return ( + + + Test Queue | ARIA-AT + +

    Test Queue

    +

    + {isSignedIn + ? 'Assign yourself a test plan or start executing one that is already assigned to you.' + : 'Select a test plan to view. Your results will not be saved.'} +

    + {isAdmin && ( + + )} + + {!testPlans.length + ? 'There are currently no test plan reports to show.' + : testPlans.map(testPlan => ( + + {/* ID needed for recovering focus after deleting a report */} +

    + {testPlan.title} +

    + {renderDisclosure({ testPlan })} +
    + ))} +
    + ); +}; + +export default TestQueue; diff --git a/client/components/TestQueue2/queries.js b/client/components/TestQueue2/queries.js new file mode 100644 index 000000000..7b5513d36 --- /dev/null +++ b/client/components/TestQueue2/queries.js @@ -0,0 +1,175 @@ +import { gql } from '@apollo/client'; + +export const TEST_QUEUE_PAGE_QUERY = gql` + query TestQueuePage { + me { + id + username + roles + } + users { + id + username + roles + isBot + ats { + id + key + } + } + ats { + id + key + name + atVersions { + id + name + releasedAt + } + browsers { + id + key + name + } + } + testPlans(testPlanVersionPhases: [DRAFT, CANDIDATE, RECOMMENDED]) { + directory + title + testPlanVersions { + id + title + phase + versionString + updatedAt + gitSha + gitMessage + testPlanReports(isFinal: false) { + id + at { + id + key + name + } + browser { + id + key + name + } + minimumAtVersion { + id + name + } + exactAtVersion { + id + name + } + runnableTestsLength + conflictsLength + metrics + draftTestPlanRuns { + id + testResultsLength + initiatedByAutomation + tester { + id + username + isBot + } + testResults { + completedAt + } + } + } + testPlanReportStatuses { + testPlanReport { + metrics + draftTestPlanRuns { + testResults { + completedAt + } + } + } + } + } + } + testPlanVersions { + id + title + phase + gitSha + gitMessage + testPlan { + directory + } + } + testPlanReports { + id + } + } +`; + +export const ASSIGN_TESTER_MUTATION = gql` + mutation AssignTester( + $testReportId: ID! + $testerId: ID! + $testPlanRunId: ID + ) { + testPlanReport(id: $testReportId) { + assignTester(userId: $testerId, testPlanRunId: $testPlanRunId) { + testPlanReport { + draftTestPlanRuns { + initiatedByAutomation + tester { + id + username + isBot + } + } + } + } + } + } +`; + +export const DELETE_TEST_PLAN_RUN = gql` + mutation DeleteTestPlanRun($testReportId: ID!, $testerId: ID!) { + testPlanReport(id: $testReportId) { + deleteTestPlanRun(userId: $testerId) { + testPlanReport { + id + draftTestPlanRuns { + id + tester { + id + username + isBot + } + } + } + } + } + } +`; + +export const MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION = gql` + mutation MarkTestPlanReportAsFinal( + $testPlanReportId: ID! + $primaryTestPlanRunId: ID! + ) { + testPlanReport(id: $testPlanReportId) { + markAsFinal(primaryTestPlanRunId: $primaryTestPlanRunId) { + testPlanReport { + markedFinalAt + } + } + } + } +`; + +export const REMOVE_TEST_PLAN_REPORT_MUTATION = gql` + mutation RemoveTestPlanReport($testPlanReportId: ID!) { + testPlanReport(id: $testPlanReportId) { + deleteTestPlanReport + } + } +`; diff --git a/client/components/TestQueueCompletionStatusListItem/BotTestCompletionStatus/index.js b/client/components/TestQueueCompletionStatusListItem/BotTestCompletionStatus/index.js index 7319020b1..a395cc48e 100644 --- a/client/components/TestQueueCompletionStatusListItem/BotTestCompletionStatus/index.js +++ b/client/components/TestQueueCompletionStatusListItem/BotTestCompletionStatus/index.js @@ -2,7 +2,12 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { useTestPlanRunValidatedAssertionCounts } from '../../../hooks/useTestPlanRunValidatedAssertionCounts'; -const BotTestCompletionStatus = ({ testPlanRun, id, runnableTestsLength }) => { +const BotTestCompletionStatus = ({ + testPlanRun, + id, + runnableTestsLength, + fromTestQueueV2 = false // TODO: Remove when Test Queue v1 is removed +}) => { const { totalValidatedAssertions, totalPossibleAssertions, @@ -17,14 +22,27 @@ const BotTestCompletionStatus = ({ testPlanRun, id, runnableTestsLength }) => { }, [testResultsLength, stopPolling]); return ( -
      -
    • - {`Responses for ${testResultsLength} of ${runnableTestsLength} tests recorded`} -
    • -
    • - {`Verdicts for ${totalValidatedAssertions} of ${totalPossibleAssertions} assertions assigned`} -
    • -
    + <> + {fromTestQueueV2 ? ( +
      +
    • + {`Responses for ${testResultsLength} of ${runnableTestsLength} tests recorded`} +
    • +
    • + {`Verdicts for ${totalValidatedAssertions} of ${totalPossibleAssertions} assertions assigned`} +
    • +
    + ) : ( +
      +
    • + {`Responses for ${testResultsLength} of ${runnableTestsLength} tests recorded`} +
    • +
    • + {`Verdicts for ${totalValidatedAssertions} of ${totalPossibleAssertions} assertions assigned`} +
    • +
    + )} + ); }; @@ -46,7 +64,8 @@ BotTestCompletionStatus.propTypes = { ) }).isRequired, id: PropTypes.string.isRequired, - runnableTestsLength: PropTypes.number.isRequired + runnableTestsLength: PropTypes.number.isRequired, + fromTestQueueV2: PropTypes.bool }; export default BotTestCompletionStatus; diff --git a/client/components/TestQueueCompletionStatusListItem/PreviouslyAutomatedTestCompletionStatus/index.js b/client/components/TestQueueCompletionStatusListItem/PreviouslyAutomatedTestCompletionStatus/index.js index 7ec7cec7f..1ac71b589 100644 --- a/client/components/TestQueueCompletionStatusListItem/PreviouslyAutomatedTestCompletionStatus/index.js +++ b/client/components/TestQueueCompletionStatusListItem/PreviouslyAutomatedTestCompletionStatus/index.js @@ -6,7 +6,8 @@ import { TEST_PLAN_RUN_ASSERTION_RESULTS_QUERY } from '../queries'; const PreviouslyAutomatedTestCompletionStatus = ({ runnableTestsLength, testPlanRunId, - id + id, + fromTestQueueV2 = false // TODO: Remove when Test Queue v1 is removed }) => { const { data: testPlanRunAssertionsQueryResult } = useQuery( TEST_PLAN_RUN_ASSERTION_RESULTS_QUERY, @@ -38,16 +39,25 @@ const PreviouslyAutomatedTestCompletionStatus = ({ }, [testPlanRunAssertionsQueryResult]); return ( -
    - {`${numValidatedTests} of ${runnableTestsLength} tests evaluated`} -
    + <> + {fromTestQueueV2 ? ( + + {`${numValidatedTests} of ${runnableTestsLength} tests evaluated`} + + ) : ( +
    + {`${numValidatedTests} of ${runnableTestsLength} tests evaluated`} +
    + )} + ); }; PreviouslyAutomatedTestCompletionStatus.propTypes = { runnableTestsLength: PropTypes.number.isRequired, testPlanRunId: PropTypes.string.isRequired, - id: PropTypes.string.isRequired + id: PropTypes.string.isRequired, + fromTestQueueV2: PropTypes.bool }; export default PreviouslyAutomatedTestCompletionStatus; diff --git a/client/components/TestQueueRow/TestQueueRow.css b/client/components/TestQueueRow/TestQueueRow.css index 9cab0c6c0..b24d56deb 100644 --- a/client/components/TestQueueRow/TestQueueRow.css +++ b/client/components/TestQueueRow/TestQueueRow.css @@ -77,6 +77,12 @@ button.more-actions:active { width: initial; } -/* tr.test-queue-run-row td:first-child { - padding: 0.75rem; -} */ +.primary-test-run-select { + &[size]:not([size='1']) { + padding: 0 !important; + } + + option { + padding: 0.5rem; + } +} diff --git a/client/components/TestQueueRow/index.jsx b/client/components/TestQueueRow/index.jsx index ce8cbd361..8584d58dc 100644 --- a/client/components/TestQueueRow/index.jsx +++ b/client/components/TestQueueRow/index.jsx @@ -4,16 +4,17 @@ import { useApolloClient, useMutation } from '@apollo/client'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import nextId from 'react-id-generator'; -import { Button, Dropdown } from 'react-bootstrap'; +import { Form, Button, Dropdown } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { TEST_PLAN_REPORT_QUERY, ASSIGN_TESTER_MUTATION, - UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION, + MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION, REMOVE_TEST_PLAN_REPORT_MUTATION, REMOVE_TESTER_MUTATION, REMOVE_TESTER_RESULTS_MUTATION } from '../TestQueue/queries'; +import BasicModal from '../common/BasicModal'; import BasicThemedModal from '../common/BasicThemedModal'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; import './TestQueueRow.css'; @@ -44,14 +45,16 @@ const TestQueueRow = ({ const setAlertMessage = useAriaLiveRegion(); + const [showPrimaryTestPlanRunModal, setShowPrimaryTestPlanRunModal] = + useState(false); const [showThemedModal, setShowThemedModal] = useState(false); const [themedModalType, setThemedModalType] = useState('warning'); const [themedModalTitle, setThemedModalTitle] = useState(''); const [themedModalContent, setThemedModalContent] = useState(<>); const [assignTester] = useMutation(ASSIGN_TESTER_MUTATION); - const [updateTestPlanMarkedFinalAt] = useMutation( - UPDATE_TEST_PLAN_REPORT_APPROVED_AT_MUTATION + const [markTestPlanReportAsFinal] = useMutation( + MARK_TEST_PLAN_REPORT_AS_FINAL_MUTATION ); const [removeTestPlanReport] = useMutation( REMOVE_TEST_PLAN_REPORT_MUTATION @@ -60,6 +63,7 @@ const TestQueueRow = ({ const [removeTesterResults] = useMutation(REMOVE_TESTER_RESULTS_MUTATION); const [testPlanReport, setTestPlanReport] = useState(testPlanReportData); + const [primaryTestPlanRunId, setPrimaryTestPlanRunId] = useState(null); const [isLoading, setIsLoading] = useState(false); const { id, isAdmin, isTester, isVendor, username } = user; @@ -90,6 +94,19 @@ const TestQueueRow = ({ ({ testResultsLength = 0 }) => testResultsLength > 0 ); + const primaryTestPlanRunOptions = draftTestPlanRuns + .slice() + .sort((a, b) => + a.tester.username.toLowerCase() < b.tester.username.toLowerCase() + ? -1 + : 1 + ) + .map(run => ({ + testPlanRunId: run.id, + ...run.tester + })) + .filter(tester => !tester.isBot); + const getTestPlanRunIdByUserId = userId => { return draftTestPlanRuns.find(({ tester }) => tester.id === userId).id; }; @@ -355,7 +372,23 @@ const TestQueueRow = ({ onClick={async () => { focusButtonRef.current = updateTestPlanStatusButtonRef.current; - await updateReportStatus(); + + const primaryTestPlanRunId = + primaryTestPlanRunOptions[0] + .testPlanRunId; + + if (primaryTestPlanRunOptions.length > 1) { + setPrimaryTestPlanRunId( + primaryTestPlanRunId + ); + setShowPrimaryTestPlanRunModal(true); + } else { + // Immediately mark as final with the + // only option + await updateReportMarkedFinal( + primaryTestPlanRunId + ); + } }} > Mark as Final @@ -372,12 +405,71 @@ const TestQueueRow = ({ } }; - const updateReportStatus = async () => { + const handlePrimaryTestRunChange = e => { + const value = e.target.value; + setPrimaryTestPlanRunId(value); + }; + + const renderPrimaryRunSelectionDialog = testers => { + return ( + + When a tester's run is marked as primary, it means + that their output for collected results will be + prioritized and shown on report pages. +
    +
    + A tester's run being marked as primary may also set + the minimum required Assistive Technology Version that + can be used for subsequent reports with that Test Plan + Version and Assistive Technology combination. +
    +
    + + {testers.map(tester => ( + + ))} + + + } + closeLabel="Cancel" + staticBackdrop={true} + actions={[ + { + label: 'Confirm', + onClick: async () => + await updateReportMarkedFinal(primaryTestPlanRunId) + } + ]} + useOnHide + handleClose={() => { + setPrimaryTestPlanRunId(null); + setShowPrimaryTestPlanRunModal(false); + }} + /> + ); + }; + + const updateReportMarkedFinal = async primaryTestPlanRunId => { try { await triggerLoad(async () => { - await updateTestPlanMarkedFinalAt({ + await markTestPlanReportAsFinal({ variables: { - testReportId: testPlanReport.id + testReportId: testPlanReport.id, + primaryTestPlanRunId } }); await triggerPageUpdate(); @@ -438,6 +530,11 @@ const TestQueueRow = ({ {renderAssignedUserToTestPlan()} + + {testPlanReport.minimumAtVersion + ? `${testPlanReport.minimumAtVersion.name} or later` + : testPlanReport.exactAtVersion.name} + {isSignedIn && isTester && (
    @@ -602,6 +699,8 @@ const TestQueueRow = ({ showCloseAction={false} /> )} + {showPrimaryTestPlanRunModal && + renderPrimaryRunSelectionDialog(primaryTestPlanRunOptions)} ); }; diff --git a/client/components/TestRun/index.jsx b/client/components/TestRun/index.jsx index 0f44ed085..db7320fbf 100644 --- a/client/components/TestRun/index.jsx +++ b/client/components/TestRun/index.jsx @@ -368,6 +368,7 @@ const TestRun = () => { )}`, testTitle: currentTest.title, testRowNumber: currentTest.rowNumber, + testSequenceNumber: currentTest.seq, testRenderedUrl: currentTest.renderedUrl, atName: testPlanReport.at.name, browserName: testPlanReport.browser.name, @@ -1390,9 +1391,24 @@ const TestRun = () => { isAdmin={isAdminReviewer} atName={testPlanReport.at.name} atVersion={currentTest.testResult?.atVersion?.name} - atVersions={testPlanReport.at.atVersions.map( - item => item.name - )} + atVersions={testPlanReport.at.atVersions + .filter(item => { + // Only provide at version options that released + // at the same time or later than the minimum + // AT version + let earliestReleasedAt = null; + if (testPlanReport.minimumAtVersion) { + earliestReleasedAt = new Date( + testPlanReport.minimumAtVersion.releasedAt + ); + return ( + new Date(item.releasedAt) >= + earliestReleasedAt + ); + } + return item; + }) + .map(item => item.name)} browserName={testPlanReport.browser.name} browserVersion={ currentTest.testResult?.browserVersion?.name @@ -1402,6 +1418,7 @@ const TestRun = () => { )} patternName={testPlanVersion.title} testerName={tester.username} + exactAtVersion={testPlanReport.exactAtVersion} handleAction={handleAtAndBrowserDetailsModalAction} handleClose={handleAtAndBrowserDetailsModalCloseAction} /> diff --git a/client/components/TestRun/queries.js b/client/components/TestRun/queries.js index 138b77abf..aed6ca3e1 100644 --- a/client/components/TestRun/queries.js +++ b/client/components/TestRun/queries.js @@ -122,8 +122,18 @@ export const TEST_RUN_PAGE_QUERY = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -427,8 +437,18 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -525,8 +545,18 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -721,8 +751,18 @@ export const SAVE_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -818,8 +858,18 @@ export const SAVE_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -1013,8 +1063,18 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name @@ -1110,8 +1170,18 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql` atVersions { id name + releasedAt } } + minimumAtVersion { + id + name + releasedAt + } + exactAtVersion { + id + name + } browser { id name diff --git a/client/components/common/AtAndBrowserDetailsModal/index.jsx b/client/components/common/AtAndBrowserDetailsModal/index.jsx index 873e7292d..11dd21c25 100644 --- a/client/components/common/AtAndBrowserDetailsModal/index.jsx +++ b/client/components/common/AtAndBrowserDetailsModal/index.jsx @@ -51,6 +51,7 @@ const AtAndBrowserDetailsModal = ({ browserVersion = '', patternName = '', // admin related prop testerName = '', // admin related prop + exactAtVersion = null, handleAction = () => {}, handleClose = () => {} }) => { @@ -69,8 +70,9 @@ const AtAndBrowserDetailsModal = ({ const [showExitModal, setShowExitModal] = useState(false); const [isFirstLoad, setIsFirstLoad] = useState(true); - const [updatedAtVersion, setUpdatedAtVersion] = - useState('Select a Version'); + const [updatedAtVersion, setUpdatedAtVersion] = useState( + exactAtVersion ? exactAtVersion.name : 'Select a Version' + ); const [updatedBrowserVersion, setUpdatedBrowserVersion] = useState(''); const [isAtVersionError, setIsAtVersionError] = useState(false); @@ -316,6 +318,27 @@ const AtAndBrowserDetailsModal = ({ )} + {exactAtVersion ? ( + + + + Results collected for this test plan + require{' '} + + {atName} {updatedAtVersion} + + . By continuing, you confirm that + your results are being recorded + using the specified version of the + Assistive Technology. + + + ) : null} Assistive Technology Name @@ -326,42 +349,59 @@ const AtAndBrowserDetailsModal = ({ value={atName} /> - - - Assistive Technology Version - - - - {[ - 'Select a Version', - ...atVersions - ].map(item => ( - - ))} - - {isAtVersionError && ( - - Please select an Assistive - Technology Version. - + {[ + 'Select a Version', + ...atVersions + ].map(item => ( + + ))} + + {isAtVersionError && ( + + Please select an Assistive + Technology Version. + + )} + )} @@ -638,6 +678,10 @@ AtAndBrowserDetailsModal.propTypes = { browserVersions: PropTypes.arrayOf(PropTypes.string), patternName: PropTypes.string, testerName: PropTypes.string, + exactAtVersion: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }), handleClose: PropTypes.func, handleAction: PropTypes.func }; diff --git a/client/components/common/AtBrowserVersion/index.jsx b/client/components/common/AtBrowserVersion/index.jsx new file mode 100644 index 000000000..0d0e49a97 --- /dev/null +++ b/client/components/common/AtBrowserVersion/index.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; + +const VersionContainer = styled.div` + display: inline-block; + flex-wrap: wrap; + background: #f5f5f5; + border-radius: 4px; + padding: 0 5px; + font-weight: bold; + + & span { + font-weight: initial; + display: inline-block; + margin-left: 2px; + } +`; + +const AtVersion = ({ at, minimumAtVersion, exactAtVersion }) => { + const atVersionFormatted = minimumAtVersion + ? `${minimumAtVersion.name} or later` + : exactAtVersion.name; + + return ( + + {at.name}  + {atVersionFormatted} + + ); +}; + +AtVersion.propTypes = { + at: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired, + minimumAtVersion: PropTypes.shape({ name: PropTypes.string.isRequired }), + exactAtVersion: PropTypes.shape({ name: PropTypes.string.isRequired }) +}; + +const BrowserVersion = ({ browser }) => { + return ( + + {browser.name}  + Any version + + ); +}; + +BrowserVersion.propTypes = { + browser: PropTypes.shape({ + name: PropTypes.string.isRequired + }).isRequired +}; + +export { AtVersion, BrowserVersion }; diff --git a/client/components/common/ClippedProgressBar/index.jsx b/client/components/common/ClippedProgressBar/index.jsx index f78ad28ef..057251c89 100644 --- a/client/components/common/ClippedProgressBar/index.jsx +++ b/client/components/common/ClippedProgressBar/index.jsx @@ -2,7 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import './ClippedProgressBar.css'; -const ProgressBar = ({ progress = 0, label = '', clipped = true }) => { +const ProgressBar = ({ + progress = 0, + label = '', + clipped = true, + decorative +}) => { return ( <> {clipped ? ( @@ -13,9 +18,11 @@ const ProgressBar = ({ progress = 0, label = '', clipped = true }) => { clipPath: `inset(0 0 0 ${progress}%)` }} > - {progress}% + {decorative ? null : `${progress}%`} +
    +
    + {decorative ? null : `${progress}%`}
    -
    {progress}%
    ) : (
    @@ -25,7 +32,7 @@ const ProgressBar = ({ progress = 0, label = '', clipped = true }) => { width: `${progress}%` }} > - {progress}% + {decorative ? null : `${progress}%`}
    )} @@ -36,7 +43,8 @@ const ProgressBar = ({ progress = 0, label = '', clipped = true }) => { ProgressBar.propTypes = { progress: PropTypes.number, label: PropTypes.string, - clipped: PropTypes.bool + clipped: PropTypes.bool, + decorative: PropTypes.bool }; export default ProgressBar; diff --git a/client/components/common/DisclosureComponent/index.jsx b/client/components/common/DisclosureComponent/index.jsx index 4fcee9a3e..88ac56902 100644 --- a/client/components/common/DisclosureComponent/index.jsx +++ b/client/components/common/DisclosureComponent/index.jsx @@ -9,12 +9,10 @@ const DisclosureParent = styled.div` border-radius: 3px; width: 100%; - h1 { - margin: 0; - padding: 0; - } - - h3 { + h1, + h2, + h3, + h4 { margin: 0; padding: 0; } @@ -34,7 +32,7 @@ const DisclosureButton = styled.button` position: relative; width: 100%; margin: 0; - padding: 1.25rem; + padding: 1.25rem 40px 1.25rem 1.25rem; text-align: left; font-size: 1rem; font-weight: bold; @@ -99,6 +97,10 @@ const DisclosureComponent = ({ {title.map((_, index) => { const buttonTitle = title[index]; + const labelTitle = + typeof buttonTitle === 'string' + ? buttonTitle + : index; const buttonExpanded = expanded[index]; const buttonOnClick = onClick[index]; const buttonDisclosureContainerView = @@ -108,10 +110,10 @@ const DisclosureComponent = ({ @@ -128,8 +130,8 @@ const DisclosureComponent = ({ diff --git a/client/components/common/RadioBox/index.jsx b/client/components/common/RadioBox/index.jsx new file mode 100644 index 000000000..b1f8f1296 --- /dev/null +++ b/client/components/common/RadioBox/index.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; + +const ContainerDiv = styled.div` + display: flex; +`; + +const Label = styled.label` + border: 1px solid #ced4da; + padding: 0.375rem 0.9rem 0.375rem 0.75rem; + background-color: ${props => + props.applyCheckedStyles ? `#F6F8FA` : 'white'}; + + &:first-of-type { + border-radius: 0.375rem 0 0 0.375rem; + } + &:last-of-type { + border-radius: 0 0.375rem 0.375rem 0; + } + &:not(:last-of-type) { + border-right: none; + } +`; + +const Input = styled.input` + margin-right: 0.375rem; +`; + +const RadioBox = ({ name, labels, selectedLabel, onSelect }) => { + const getOnChange = label => event => { + if (event.target.checked) onSelect(label); + }; + + return ( + + {labels.map(label => { + const isChecked = selectedLabel === label; + return ( + + ); + })} + + ); +}; + +const TextThatWontShiftWhenBold = ({ isBold, children: text }) => { + return ( + + + {text} + + + {text} + + + ); +}; + +TextThatWontShiftWhenBold.propTypes = { + isBold: PropTypes.bool.isRequired, + children: PropTypes.string.isRequired +}; + +RadioBox.propTypes = { + name: PropTypes.string.isRequired, + labels: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, + selectedLabel: PropTypes.string, + onSelect: PropTypes.func.isRequired +}; + +export default RadioBox; diff --git a/client/components/common/ReportStatusSummary/index.jsx b/client/components/common/ReportStatusSummary/index.jsx new file mode 100644 index 000000000..904f81b9e --- /dev/null +++ b/client/components/common/ReportStatusSummary/index.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { convertDateToString } from '../../../utils/formatter'; +import { calculatePercentComplete } from '../../../utils/calculatePercentComplete'; + +const IncompleteStatusReport = styled.span` + min-width: 5rem; + display: inline-block; +`; + +const ReportStatusSummary = ({ + testPlanVersion, + testPlanReport, + fromTestQueue = false +}) => { + 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 = calculatePercentComplete(testPlanReport); + switch (draftTestPlanRuns?.length) { + case 0: + return fromTestQueue ? ( + No testers assigned + ) : ( + 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 + + ); + } + }; + + if (testPlanReport) { + const { markedFinalAt } = testPlanReport; + if (markedFinalAt) { + return renderCompleteReportStatus(testPlanReport); + } else { + return renderPartialCompleteReportStatus(testPlanReport); + } + } + + return Missing; +}; + +ReportStatusSummary.propTypes = { + testPlanVersion: PropTypes.shape({ + id: PropTypes.string.isRequired + }).isRequired, + testPlanReport: PropTypes.shape({ + id: PropTypes.string.isRequired, + markedFinalAt: PropTypes.string, + metrics: PropTypes.object, + draftTestPlanRuns: PropTypes.arrayOf( + PropTypes.shape({ + tester: PropTypes.shape({ + username: PropTypes.string.isRequired + }).isRequired + }) + ).isRequired + }), + fromTestQueue: PropTypes.bool +}; + +export default ReportStatusSummary; diff --git a/client/hooks/useConfirmationModal.js b/client/hooks/useConfirmationModal.js new file mode 100644 index 000000000..a14bbe56e --- /dev/null +++ b/client/hooks/useConfirmationModal.js @@ -0,0 +1,43 @@ +import React, { createContext, useContext, useRef } from 'react'; +import PropTypes from 'prop-types'; +import useForceUpdate from './useForceUpdate'; + +const ConfirmationContext = createContext(); + +const ConfirmationModalProvider = ({ children }) => { + const forceUpdate = useForceUpdate(); + const modalContent = useRef(); + + const showConfirmationModal = newModalContent => { + modalContent.current = newModalContent; + forceUpdate(); + }; + + const hideConfirmationModal = async () => { + modalContent.current = null; + forceUpdate(); + }; + + return ( + + {children} + {modalContent.current} + + ); +}; + +ConfirmationModalProvider.propTypes = { + children: PropTypes.node.isRequired +}; + +const useConfirmationModal = () => { + const { showConfirmationModal, hideConfirmationModal } = + useContext(ConfirmationContext); + + return { showConfirmationModal, hideConfirmationModal }; +}; + +export { ConfirmationModalProvider }; +export default useConfirmationModal; diff --git a/client/hooks/useForceUpdate.js b/client/hooks/useForceUpdate.js new file mode 100644 index 000000000..923186af9 --- /dev/null +++ b/client/hooks/useForceUpdate.js @@ -0,0 +1,10 @@ +import React from 'react'; + +// https://stackoverflow.com/a/53215514 +const useForceUpdate = () => { + const [, updateState] = React.useState(); + const forceUpdate = React.useCallback(() => updateState({}), []); + return forceUpdate; +}; + +export default useForceUpdate; diff --git a/client/hooks/useThemedModal.js b/client/hooks/useThemedModal.js index a85b27b65..f058f474b 100644 --- a/client/hooks/useThemedModal.js +++ b/client/hooks/useThemedModal.js @@ -13,6 +13,9 @@ function useThemedModal({ show, type, title, content }) { const [themedModalType, setThemedModalType] = useState(THEMES.WARNING); const [themedModalTitle, setThemedModalTitle] = useState(''); const [themedModalContent, setThemedModalContent] = useState(<>); + const [themedModalActions, setThemedModalActions] = useState(null); + const [themedModalShowCloseAction, setThemedModalShowCloseAction] = + useState(false); useEffect(() => { setShowThemedModal(showThemedModal || show); @@ -21,13 +24,26 @@ function useThemedModal({ show, type, title, content }) { setThemedModalContent(themedModalContent || content); }); + const hideThemedModal = () => { + setShowThemedModal(false); + setThemedModalType(THEMES.WARNING); + setThemedModalTitle(''); + setThemedModalContent(<>); + setThemedModalActions(null); + setThemedModalShowCloseAction(false); + }; + const onThemedModalClose = () => { setShowThemedModal(false); if (focusElementRef.current) focusElementRef.current.focus(); }; const setFocusRef = focusElement => - (focusElementRef.current = focusElement); + (focusElementRef.current = focusElement?.current || focusElement); + + const focus = () => { + if (focusElementRef.current) focusElementRef.current.focus(); + }; const themedModal = ( ); return { - setFocusRef, themedModal, showThemedModal, setShowThemedModal, setThemedModalType, setThemedModalTitle, - setThemedModalContent + setThemedModalContent, + setThemedModalActions, + setThemedModalShowCloseAction, + focus, + setFocusRef, + hideThemedModal }; } diff --git a/client/index.js b/client/index.js index 57d21061f..537cd22a8 100644 --- a/client/index.js +++ b/client/index.js @@ -7,15 +7,19 @@ import App from './components/App'; import GraphQLProvider from './components/GraphQLProvider'; import { AriaLiveRegionProvider } from './components/providers/AriaLiveRegionProvider'; import { resetCache } from './components/GraphQLProvider/GraphQLProvider'; +import { ConfirmationModalProvider } from './hooks/useConfirmationModal'; const container = document.getElementById('root'); const root = createRoot(container); + root.render( - - - + + + + + ); diff --git a/client/routes/index.js b/client/routes/index.js index a98bc63de..cab1ab6f7 100644 --- a/client/routes/index.js +++ b/client/routes/index.js @@ -8,6 +8,7 @@ import { Reports, Report } from '@components/Reports'; import CandidateReview from '@components/CandidateReview'; import SignupInstructions from '@components/SignupInstructions'; import TestQueue from '@components/TestQueue'; +import TestQueue2 from '@components/TestQueue2'; import TestRun from '@components/TestRun'; import UserSettings from '@components/UserSettings'; import CandidateTestPlanRun from '@components/CandidateReview/CandidateTestPlanRun'; @@ -41,7 +42,9 @@ export default () => ( } /> - } /> + {/* TODO: Deprecate and remove */} + } /> + } /> { await getPage({ role: 'admin', url: '/test-queue' }, async page => { const openTrayIfClosed = async () => { await page.waitForSelector('button ::-p-text(Manage Assistive Technology Versions)'); - const isTrayClosed = !!(await page.$('::-p-text(Select an Assistive Technology and manage its versions)')); + const isTrayClosed = !!(await page.$('::-p-text(Select an assistive technology and manage its versions)')); if (isTrayClosed) { await page.click('button ::-p-text(Manage Assistive Technology Versions)'); - await page.waitForSelector('::-p-text(Select an Assistive Technology and manage its versions)'); + await page.waitForSelector('::-p-text(Select an assistive technology and manage its versions)'); } }; await openTrayIfClosed(); @@ -20,8 +20,8 @@ describe('AT Version UI', () => { await page.type('.modal-body .form-group:nth-child(1) input', '99.0.1'); await page.type('.modal-body .form-group:nth-child(2) input', '01-01-2000'); await page.click('.modal-footer button ::-p-text(Add Version)'); - await page.waitForNetworkIdle(); - await openTrayIfClosed(); + await page.waitForNetworkIdle({ idleTime: 5000 }); + await page.click('.modal-footer button ::-p-text(Ok)'); await page.waitForSelector('.at-versions-container option:nth-child(2) ::-p-text(99.0.1)'); const optionValue = await page.$eval('.at-versions-container option:nth-child(2)', option => option.value); await page.select('.at-versions-container select', optionValue); @@ -32,15 +32,14 @@ describe('AT Version UI', () => { } await page.type('.modal-body .form-group:nth-child(1) input', '99.0.99'); await page.click('.modal-footer button ::-p-text(Save)'); - await page.waitForNetworkIdle(); - await openTrayIfClosed(); + await page.waitForNetworkIdle({ idleTime: 5000 }); + await page.click('.modal-footer button ::-p-text(Ok)'); await page.waitForSelector('.at-versions-container option ::-p-text(99.0.99)'); await page.select('.at-versions-container select', optionValue); await page.click('.at-versions-container button ::-p-text(Remove)'); await page.waitForSelector('.modal-title ::-p-text(Remove JAWS Version 99.0.99)'); await page.click('.modal-footer button ::-p-text(Remove)'); await page.waitForNetworkIdle(); - await openTrayIfClosed(); const option = await page.$('.at-versions-container option ::-p-text(99.0.99)'); expect(option).toBeNull(); }); diff --git a/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js index aaca65dfd..60973e8d7 100644 --- a/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/DataManagementPagePopulatedMock.js @@ -1427,7 +1427,7 @@ export default ( }, browser: { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, issues: [], @@ -2325,7 +2325,7 @@ export default ( }, browser: { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, issues: [], @@ -2541,11 +2541,10 @@ export default ( id: '1', title: 'Alert Example', phase: 'DRAFT', - gitSha: '0928bcf530efcf4faa677285439701537674e014', + gitSha: 'c665367f3742c2b607f7b3c2655782188b93f302', gitMessage: - 'Alert and Radiogroup/activedescendent updates (#865)', - updatedAt: '2022-12-08T21:47:42.000Z', - versionString: 'V22.12.08', + 'Create updated tests for APG design pattern example: Alert (#685)', + updatedAt: '2022-04-14T17:59:42.000Z', draftPhaseReachedAt: '2022-07-06T00:00:00.000Z', candidatePhaseReachedAt: null, recommendedPhaseTargetDate: null, @@ -2553,75 +2552,92 @@ export default ( testPlan: { directory: 'alert' }, - testPlanReports: [ + testPlanReportStatuses: [ { - id: '7', - metrics: {}, - markedFinalAt: null, - isFinal: false, + isRequired: true, at: { - id: '3', - key: 'voiceover_macos', - name: 'VoiceOver for macOS' + id: '1', + key: 'jaws', + name: 'JAWS' }, browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { id: '1', - key: 'firefox', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: { + id: '101', + metrics: {}, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [] + } + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'chrome', name: 'Firefox' }, - issues: [], - draftTestPlanRuns: [] - } - ], - metadata: {} - } - } - } - }, - { - 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: [ + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: null + }, { - id: '6', - metrics: { - testsCount: 7, - supportLevel: 'FAILING', - conflictsCount: 0, - supportPercent: 96, - testsFailedCount: 3, - testsPassedCount: 4, - shouldFormatted: '4 of 4 passed', - mustFormatted: '44 of 46 passed', - shouldAssertionsCount: 4, - mustAssertionsCount: 46, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 4, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 44 + isRequired: false, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' }, - markedFinalAt: '2022-07-06T00:00:00.000Z', - isFinal: true, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, at: { id: '3', key: 'voiceover_macos', @@ -2629,312 +2645,62 @@ export default ( }, browser: { id: '3', - key: 'safari', + key: 'safari_macos', 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' - } - ] - } - ] + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null }, { - id: '12', - metrics: { - testsCount: 14, - supportLevel: 'FULL', - conflictsCount: 0, - supportPercent: 100, - testsFailedCount: 12, - testsPassedCount: 2, - shouldFormatted: false, - mustFormatted: '25 of 25 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 25, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 25 - }, - markedFinalAt: '2022-07-06T00:00:00.000Z', - isFinal: true, + isRequired: false, at: { - id: '1', - key: 'jaws', - name: 'JAWS' + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' }, browser: { id: '2', key: 'chrome', 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' - } - ] - } - ] + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null }, { - id: '13', - metrics: { - testsCount: 14, - supportLevel: 'FULL', - conflictsCount: 0, - supportPercent: 100, - testsFailedCount: 12, - testsPassedCount: 2, - shouldFormatted: false, - mustFormatted: '25 of 25 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 25, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 25 - }, - markedFinalAt: '2022-07-07T00:00:00.000Z', - isFinal: true, + isRequired: false, at: { - id: '2', - key: 'nvda', - name: 'NVDA' + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' }, browser: { - id: '2', - key: 'chrome', - name: 'Chrome' + id: '1', + key: 'firefox', + name: 'Firefox' }, - 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' - } - ] - } - ] + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '7', + metrics: {}, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [] + } } - ], - metadata: {} + ] } } } @@ -2942,447 +2708,4336 @@ export default ( { request: { query: testPlanReportStatusDialogQuery, - variables: { testPlanVersionId: '7' } + variables: { testPlanVersionId: '5' } }, 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, + id: '69', + 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: 'combobox-select-only' + directory: 'checkbox-tri-state' }, - testPlanReports: [ + testPlanReportStatuses: [ { - id: '2', - metrics: { - testsCount: 21, - supportLevel: 'FAILING', - conflictsCount: 5, - supportPercent: 96, - testsFailedCount: 16, - testsPassedCount: 5, - shouldFormatted: '3 of 3 passed', - mustFormatted: '48 of 50 passed', - shouldAssertionsCount: 3, - mustAssertionsCount: 50, - unexpectedBehaviorCount: 3, - unexpectedBehaviorsFormatted: '3 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 3, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 48 - }, - markedFinalAt: null, - isFinal: false, + isRequired: true, at: { - id: '2', - key: 'nvda', - name: 'NVDA' + id: '1', + key: 'jaws', + name: 'JAWS' }, browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { id: '1', - key: 'firefox', - name: 'Firefox' + name: '2021.2111.13' }, - 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' + exactAtVersion: null, + testPlanReport: { + id: '12', + metrics: { + testsCount: 14, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 26, + mustFormatted: '116 of 116 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '34 of 34 passed', + testsFailedCount: 0, + testsPassedCount: 14, + mayAssertionsCount: 0, + mustAssertionsCount: 116, + assertionsFailedCount: 0, + assertionsPassedCount: 150, + shouldAssertionsCount: 34, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 116, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 34, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 26 + }, + isFinal: true, + markedFinalAt: '2022-07-06T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' }, - { - test: { - id: 'MmY0YeyIyIjoiNyJ9jRkZD' - }, - atVersion: { - id: '2', - name: '2020.4' - }, - browserVersion: { - id: '1', - name: '99.0.1' - }, - completedAt: - '2023-08-18T03:17:08.332Z' + testPlanReport: { + id: '12' }, - { - test: { - id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' - }, - atVersion: { - id: '2', - name: '2020.4' - }, - browserVersion: { - id: '1', - name: '99.0.1' + testResults: [ + { + test: { + id: 'YWYzOeyIyIjoiNjkifQTQ0MT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.053Z' }, - completedAt: - '2023-08-18T03:17:08.412Z' - }, - { - test: { - id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + { + test: { + id: 'OGZjNeyIyIjoiNjkifQjQxZW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.149Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'NjM3ZeyIyIjoiNjkifQmUxYz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.242Z' + }, + { + test: { + id: 'ZWQ0MeyIyIjoiNjkifQGZhYT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.321Z' + }, + { + test: { + id: 'ZGI3ZeyIyIjoiNjkifQTc5Mj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.409Z' + }, + { + test: { + id: 'MDZjOeyIyIjoiNjkifQGJkYz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.510Z' + }, + { + test: { + id: 'ZmI3NeyIyIjoiNjkifQzUwMT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.614Z' + }, + { + test: { + id: 'NmY2YeyIyIjoiNjkifQTczOW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.731Z' + }, + { + test: { + id: 'MjIwYeyIyIjoiNjkifQmUzZj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:52.882Z' + }, + { + test: { + id: 'ODg0OeyIyIjoiNjkifQWFlYm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.038Z' + }, + { + test: { + id: 'ZDQ2MeyIyIjoiNjkifQjlmZj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.184Z' + }, + { + test: { + id: 'MjdlYeyIyIjoiNjkifQTgyNj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.317Z' + }, + { + test: { + id: 'OGE5MeyIyIjoiNjkifQGZjOT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.428Z' + }, + { + test: { + id: 'YWNlNeyIyIjoiNjkifQjQzOW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.567Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '13', + metrics: { + testsCount: 14, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 28, + mustFormatted: '124 of 124 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '36 of 36 passed', + testsFailedCount: 0, + testsPassedCount: 14, + mayAssertionsCount: 0, + mustAssertionsCount: 124, + assertionsFailedCount: 0, + assertionsPassedCount: 160, + shouldAssertionsCount: 36, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 124, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 36, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 28, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 28 + }, + isFinal: true, + markedFinalAt: '2022-07-07T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '13' + }, + testResults: [ + { + test: { + id: 'YWYzOeyIyIjoiNjkifQTQ0MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.698Z' + }, + { + test: { + id: 'OGZjNeyIyIjoiNjkifQjQxZW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.814Z' + }, + { + test: { + id: 'NjM3ZeyIyIjoiNjkifQmUxYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:53.921Z' + }, + { + test: { + id: 'ZWQ0MeyIyIjoiNjkifQGZhYT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.038Z' + }, + { + test: { + id: 'ZGI3ZeyIyIjoiNjkifQTc5Mj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.181Z' + }, + { + test: { + id: 'MDZjOeyIyIjoiNjkifQGJkYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.327Z' + }, + { + test: { + id: 'ZmI3NeyIyIjoiNjkifQzUwMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.421Z' + }, + { + test: { + id: 'NmY2YeyIyIjoiNjkifQTczOW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.541Z' + }, + { + test: { + id: 'MjIwYeyIyIjoiNjkifQmUzZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.666Z' + }, + { + test: { + id: 'ODg0OeyIyIjoiNjkifQWFlYm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.794Z' + }, + { + test: { + id: 'ZDQ2MeyIyIjoiNjkifQjlmZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.881Z' + }, + { + test: { + id: 'MjdlYeyIyIjoiNjkifQTgyNj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:54.967Z' + }, + { + test: { + id: 'OGE5MeyIyIjoiNjkifQGZjOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.059Z' + }, + { + test: { + id: 'YWNlNeyIyIjoiNjkifQjQzOW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.140Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + key: 'safari_macos', + name: 'Safari' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '6', + metrics: { + testsCount: 7, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 16, + mustFormatted: '66 of 68 passed', + conflictsCount: 0, + supportPercent: 97, + shouldFormatted: '20 of 20 passed', + testsFailedCount: 2, + testsPassedCount: 5, + mayAssertionsCount: 0, + mustAssertionsCount: 68, + assertionsFailedCount: 2, + assertionsPassedCount: 86, + shouldAssertionsCount: 20, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 2, + mustAssertionsPassedCount: 66, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 20, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 16, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 16 + }, + isFinal: true, + markedFinalAt: '2022-07-06T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' + }, + testPlanReport: { + id: '6' + }, + testResults: [ + { + test: { + id: 'NmUzMeyIyIjoiNjkifQmU0OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.554Z' + }, + { + test: { + id: 'Y2UyYeyIyIjoiNjkifQ2Y1Mz' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.671Z' + }, + { + test: { + id: 'ODc2OeyIyIjoiNjkifQTA1Yz' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.794Z' + }, + { + test: { + id: 'OTgwZeyIyIjoiNjkifQDZjOG' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.909Z' + }, + { + test: { + id: 'ODA3ZeyIyIjoiNjkifQjI4Y2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.997Z' + }, + { + test: { + id: 'OWI4MeyIyIjoiNjkifQzFlZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:46.091Z' + }, + { + test: { + id: 'MzhiZeyIyIjoiNjkifQWE4Nj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:46.189Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + } + ] + } + } + } + }, + { + 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' + }, + testPlanReportStatuses: [ + { + isRequired: true, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '2', + metrics: { + testsCount: 21, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 24, + mustFormatted: '118 of 122 passed', + conflictsCount: 3, + supportPercent: 97, + shouldFormatted: '34 of 36 passed', + testsFailedCount: 6, + testsPassedCount: 15, + mayAssertionsCount: 0, + mustAssertionsCount: 122, + assertionsFailedCount: 6, + assertionsPassedCount: 152, + shouldAssertionsCount: 36, + unexpectedBehaviorCount: 3, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 4, + mustAssertionsPassedCount: 118, + shouldAssertionsFailedCount: 2, + shouldAssertionsPassedCount: 34, + unexpectedBehaviorsFormatted: '3 found', + severeImpactFailedAssertionCount: 1, + severeImpactPassedAssertionCount: 23, + moderateImpactFailedAssertionCount: 2, + moderateImpactPassedAssertionCount: 22 + }, + isFinal: false, + markedFinalAt: null, + issues: [ + { + link: 'https://github.com/bocoup/aria-at/issues/128#issue-2157878584', + isOpen: true, + feedbackType: 'FEEDBACK' + } + ], + 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: + '2024-04-25T16:44:38.949Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.070Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.180Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.292Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.406Z' + }, + { + 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: + '2024-04-25T16:44:39.640Z' + }, + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.761Z' + }, + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:39.888Z' + }, + { + test: { + id: 'MmZkNeyIyIjoiNyJ9zIwN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.006Z' + }, + { + test: { + id: 'ZmQwOeyIyIjoiNyJ9DEzYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.122Z' + }, + { + test: { + id: 'MGViNeyIyIjoiNyJ9GQ3MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.234Z' + }, + { + test: { + id: 'YTg5MeyIyIjoiNyJ9WEzOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.355Z' + }, + { + test: { + id: 'NTRjMeyIyIjoiNyJ9zQ0OD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.467Z' + }, + { + test: { + id: 'MjRlZeyIyIjoiNyJ9DcyY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.588Z' + }, + { + test: { + id: 'YWQzNeyIyIjoiNyJ9mE2Nm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.712Z' + }, + { + test: { + id: 'OTYxOeyIyIjoiNyJ9TdmYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.826Z' + }, + { + test: { + id: 'MjgzNeyIyIjoiNyJ9TZjNz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:40.948Z' + }, + { + test: { + id: 'NWNiZeyIyIjoiNyJ9jI2MD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:41.075Z' + } + ] + }, + { + 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: + '2024-04-25T16:44:36.666Z' + }, + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:36.793Z' + }, + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:36.914Z' + }, + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.031Z' + }, + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.150Z' + }, + { + 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: + '2024-04-25T16:44:37.384Z' + }, + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.512Z' + }, + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.638Z' + }, + { + test: { + id: 'MmZkNeyIyIjoiNyJ9zIwN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.760Z' + }, + { + test: { + id: 'ZmQwOeyIyIjoiNyJ9DEzYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:37.883Z' + }, + { + test: { + id: 'MGViNeyIyIjoiNyJ9GQ3MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.014Z' + }, + { + test: { + id: 'YTg5MeyIyIjoiNyJ9WEzOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.145Z' + }, + { + test: { + id: 'NTRjMeyIyIjoiNyJ9zQ0OD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.268Z' + }, + { + test: { + id: 'MjRlZeyIyIjoiNyJ9DcyY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.382Z' + }, + { + test: { + id: 'YWQzNeyIyIjoiNyJ9mE2Nm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.481Z' + }, + { + test: { + id: 'OTYxOeyIyIjoiNyJ9TdmYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.596Z' + }, + { + test: { + id: 'MjgzNeyIyIjoiNyJ9TZjNz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.701Z' + }, + { + test: { + id: 'NWNiZeyIyIjoiNyJ9jI2MD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:38.811Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + key: 'safari_macos', + name: 'Safari' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '26' } + }, + result: { + data: { + testPlanVersion: { + id: '24', + title: 'Modal Dialog Example', + phase: 'CANDIDATE', + gitSha: '5fe7afd82fe51c185b8661276105190a59d47322', + gitMessage: 'Task 7: delete incorrect instructions (#733)', + updatedAt: '2022-05-26T16:10:10.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' + }, + testPlanReportStatuses: [ + { + isRequired: true, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: { + id: '3', + metrics: { + testsCount: 18, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 26, + mustFormatted: '115 of 117 passed', + conflictsCount: 0, + supportPercent: 98, + shouldFormatted: '25 of 26 passed', + testsFailedCount: 2, + testsPassedCount: 16, + mayAssertionsCount: 0, + mustAssertionsCount: 117, + assertionsFailedCount: 3, + assertionsPassedCount: 140, + shouldAssertionsCount: 26, + unexpectedBehaviorCount: 1, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 2, + mustAssertionsPassedCount: 115, + shouldAssertionsFailedCount: 1, + shouldAssertionsPassedCount: 25, + unexpectedBehaviorsFormatted: '1 found', + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 1, + moderateImpactPassedAssertionCount: 25 + }, + isFinal: true, + markedFinalAt: '2022-07-06T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '3' + }, + testResults: [ + { + test: { + id: 'ZjE0NeyIyIjoiMjQifQmI0NT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.177Z' + }, + { + test: { + id: 'YjZlNeyIyIjoiMjQifQTc5ZW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.256Z' + }, + { + test: { + id: 'NWM0MeyIyIjoiMjQifQzhiYz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.349Z' + }, + { + test: { + id: 'YzM0ZeyIyIjoiMjQifQmRmMz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.419Z' + }, + { + test: { + id: 'ZjVjMeyIyIjoiMjQifQDRhY2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.496Z' + }, + { + test: { + id: 'YmUzMeyIyIjoiMjQifQmRmNm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.574Z' + }, + { + test: { + id: 'ZGJmMeyIyIjoiMjQifQzU5Yz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.661Z' + }, + { + test: { + id: 'Nzg1OeyIyIjoiMjQifQTYxM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.741Z' + }, + { + test: { + id: 'ZTI0MeyIyIjoiMjQifQzM4YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.823Z' + }, + { + test: { + id: 'MzY5ZeyIyIjoiMjQifQmQ0OT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:41.950Z' + }, + { + test: { + id: 'ZjVmYeyIyIjoiMjQifQjJjYW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.044Z' + }, + { + test: { + id: 'ZTMwNeyIyIjoiMjQifQzI5Nz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.112Z' + }, + { + test: { + id: 'NGY0MeyIyIjoiMjQifQ2FjMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.191Z' + }, + { + test: { + id: 'OTI0OeyIyIjoiMjQifQTU1ZT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.271Z' + }, + { + test: { + id: 'MDRhMeyIyIjoiMjQifQWEzMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.344Z' + }, + { + test: { + id: 'ZThlZeyIyIjoiMjQifQjQ2Nz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.412Z' + }, + { + test: { + id: 'NjhjMeyIyIjoiMjQifQGE0ND' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.482Z' + }, + { + test: { + id: 'YTAzZeyIyIjoiMjQifQTc5ZD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.575Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: null, + exactAtVersion: { + id: '1', + name: '2021.2111.13' + }, + testPlanReport: { + id: '105', + metrics: {}, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [] + } + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: { + id: '8', + metrics: { + testsCount: 18, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 26, + mustFormatted: '117 of 117 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '26 of 26 passed', + testsFailedCount: 0, + testsPassedCount: 18, + mayAssertionsCount: 0, + mustAssertionsCount: 117, + assertionsFailedCount: 0, + assertionsPassedCount: 143, + shouldAssertionsCount: 26, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 117, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 26, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 26 + }, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '8' + }, + testResults: [ + { + test: { + id: 'ZjE0NeyIyIjoiMjQifQmI0NT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.300Z' + }, + { + test: { + id: 'YjZlNeyIyIjoiMjQifQTc5ZW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.396Z' + }, + { + test: { + id: 'NWM0MeyIyIjoiMjQifQzhiYz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.490Z' + }, + { + test: { + id: 'YzM0ZeyIyIjoiMjQifQmRmMz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.572Z' + }, + { + test: { + id: 'ZjVjMeyIyIjoiMjQifQDRhY2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.656Z' + }, + { + test: { + id: 'YmUzMeyIyIjoiMjQifQmRmNm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.743Z' + }, + { + test: { + id: 'ZGJmMeyIyIjoiMjQifQzU5Yz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.828Z' + }, + { + test: { + id: 'Nzg1OeyIyIjoiMjQifQTYxM2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.912Z' + }, + { + test: { + id: 'ZTI0MeyIyIjoiMjQifQzM4YT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:46.990Z' + }, + { + test: { + id: 'MzY5ZeyIyIjoiMjQifQmQ0OT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.087Z' + }, + { + test: { + id: 'ZjVmYeyIyIjoiMjQifQjJjYW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.215Z' + }, + { + test: { + id: 'ZTMwNeyIyIjoiMjQifQzI5Nz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.355Z' + }, + { + test: { + id: 'NGY0MeyIyIjoiMjQifQ2FjMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.484Z' + }, + { + test: { + id: 'OTI0OeyIyIjoiMjQifQTU1ZT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.614Z' + }, + { + test: { + id: 'MDRhMeyIyIjoiMjQifQWEzMj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.738Z' + }, + { + test: { + id: 'ZThlZeyIyIjoiMjQifQjQ2Nz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.862Z' + }, + { + test: { + id: 'NjhjMeyIyIjoiMjQifQGE0ND' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:47.981Z' + }, + { + test: { + id: 'YTAzZeyIyIjoiMjQifQTc5ZD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.105Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '4', + metrics: { + testsCount: 18, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 26, + mustFormatted: '115 of 117 passed', + conflictsCount: 0, + supportPercent: 98, + shouldFormatted: '25 of 26 passed', + testsFailedCount: 2, + testsPassedCount: 16, + mayAssertionsCount: 0, + mustAssertionsCount: 117, + assertionsFailedCount: 3, + assertionsPassedCount: 140, + shouldAssertionsCount: 26, + unexpectedBehaviorCount: 1, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 2, + mustAssertionsPassedCount: 115, + shouldAssertionsFailedCount: 1, + shouldAssertionsPassedCount: 25, + unexpectedBehaviorsFormatted: '1 found', + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 1, + moderateImpactPassedAssertionCount: 25 + }, + isFinal: true, + markedFinalAt: '2022-07-06T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '4' + }, + testResults: [ + { + test: { + id: 'ZjE0NeyIyIjoiMjQifQmI0NT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.683Z' + }, + { + test: { + id: 'YjZlNeyIyIjoiMjQifQTc5ZW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.772Z' + }, + { + test: { + id: 'NWM0MeyIyIjoiMjQifQzhiYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.848Z' + }, + { + test: { + id: 'YzM0ZeyIyIjoiMjQifQmRmMz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:42.928Z' + }, + { + test: { + id: 'ZjVjMeyIyIjoiMjQifQDRhY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.005Z' + }, + { + test: { + id: 'YmUzMeyIyIjoiMjQifQmRmNm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.093Z' + }, + { + test: { + id: 'ZGJmMeyIyIjoiMjQifQzU5Yz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.173Z' + }, + { + test: { + id: 'Nzg1OeyIyIjoiMjQifQTYxM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.245Z' + }, + { + test: { + id: 'ZTI0MeyIyIjoiMjQifQzM4YT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.318Z' + }, + { + test: { + id: 'MzY5ZeyIyIjoiMjQifQmQ0OT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.399Z' + }, + { + test: { + id: 'ZjVmYeyIyIjoiMjQifQjJjYW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.473Z' + }, + { + test: { + id: 'ZTMwNeyIyIjoiMjQifQzI5Nz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.554Z' + }, + { + test: { + id: 'NGY0MeyIyIjoiMjQifQ2FjMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.643Z' + }, + { + test: { + id: 'OTI0OeyIyIjoiMjQifQTU1ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.738Z' + }, + { + test: { + id: 'MDRhMeyIyIjoiMjQifQWEzMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.844Z' + }, + { + test: { + id: 'ZThlZeyIyIjoiMjQifQjQ2Nz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:43.950Z' + }, + { + test: { + id: 'NjhjMeyIyIjoiMjQifQGE0ND' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:44.078Z' + }, + { + test: { + id: 'YTAzZeyIyIjoiMjQifQTc5ZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:44.219Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '9', + metrics: { + testsCount: 18, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 26, + mustFormatted: '117 of 117 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '26 of 26 passed', + testsFailedCount: 0, + testsPassedCount: 18, + mayAssertionsCount: 0, + mustAssertionsCount: 117, + assertionsFailedCount: 0, + assertionsPassedCount: 143, + shouldAssertionsCount: 26, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 117, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 26, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 26 + }, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '9' + }, + testResults: [ + { + test: { + id: 'ZjE0NeyIyIjoiMjQifQmI0NT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.229Z' + }, + { + test: { + id: 'YjZlNeyIyIjoiMjQifQTc5ZW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.339Z' + }, + { + test: { + id: 'NWM0MeyIyIjoiMjQifQzhiYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.439Z' + }, + { + test: { + id: 'YzM0ZeyIyIjoiMjQifQmRmMz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.537Z' + }, + { + test: { + id: 'ZjVjMeyIyIjoiMjQifQDRhY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.636Z' + }, + { + test: { + id: 'YmUzMeyIyIjoiMjQifQmRmNm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.736Z' + }, + { + test: { + id: 'ZGJmMeyIyIjoiMjQifQzU5Yz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.824Z' + }, + { + test: { + id: 'Nzg1OeyIyIjoiMjQifQTYxM2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.901Z' + }, + { + test: { + id: 'ZTI0MeyIyIjoiMjQifQzM4YT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:48.978Z' + }, + { + test: { + id: 'MzY5ZeyIyIjoiMjQifQmQ0OT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.070Z' + }, + { + test: { + id: 'ZjVmYeyIyIjoiMjQifQjJjYW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.161Z' + }, + { + test: { + id: 'ZTMwNeyIyIjoiMjQifQzI5Nz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.264Z' + }, + { + test: { + id: 'NGY0MeyIyIjoiMjQifQ2FjMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.387Z' + }, + { + test: { + id: 'OTI0OeyIyIjoiMjQifQTU1ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.527Z' + }, + { + test: { + id: 'MDRhMeyIyIjoiMjQifQWEzMj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.646Z' + }, + { + test: { + id: 'ZThlZeyIyIjoiMjQifQjQ2Nz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.747Z' + }, + { + test: { + id: 'NjhjMeyIyIjoiMjQifQGE0ND' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.847Z' + }, + { + test: { + id: 'YTAzZeyIyIjoiMjQifQTc5ZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:49.951Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + key: 'safari_macos', + name: 'Safari' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '5', + metrics: { + testsCount: 11, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 20, + mustFormatted: '88 of 90 passed', + conflictsCount: 0, + supportPercent: 98, + shouldFormatted: '19 of 20 passed', + testsFailedCount: 2, + testsPassedCount: 9, + mayAssertionsCount: 0, + mustAssertionsCount: 90, + assertionsFailedCount: 3, + assertionsPassedCount: 107, + shouldAssertionsCount: 20, + unexpectedBehaviorCount: 1, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 2, + mustAssertionsPassedCount: 88, + shouldAssertionsFailedCount: 1, + shouldAssertionsPassedCount: 19, + unexpectedBehaviorsFormatted: '1 found', + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 20, + moderateImpactFailedAssertionCount: 1, + moderateImpactPassedAssertionCount: 19 + }, + isFinal: true, + markedFinalAt: '2022-07-06T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '5' + }, + testResults: [ + { + test: { + id: 'NjM0MeyIyIjoiMjQifQTdiZG' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.402Z' + }, + { + test: { + id: 'YWYzOeyIyIjoiMjQifQDBjN2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.489Z' + }, + { + test: { + id: 'ZmJjYeyIyIjoiMjQifQWJiNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.594Z' + }, + { + test: { + id: 'MjU2NeyIyIjoiMjQifQTk2YW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.694Z' + }, + { + test: { + id: 'MWNlNeyIyIjoiMjQifQTRhNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.781Z' + }, + { + test: { + id: 'YzFlYeyIyIjoiMjQifQjE5Yj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.877Z' + }, + { + test: { + id: 'N2UwMeyIyIjoiMjQifQTQ1OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:44.966Z' + }, + { + test: { + id: 'OTYwOeyIyIjoiMjQifQTE3ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.061Z' + }, + { + test: { + id: 'OWI2MeyIyIjoiMjQifQmE0ZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.165Z' + }, + { + test: { + id: 'YTU0MeyIyIjoiMjQifQjNhNj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.296Z' + }, + { + test: { + id: 'NTM4MeyIyIjoiMjQifQGVlNm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:45.433Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '11', + metrics: { + testsCount: 11, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 20, + mustFormatted: '90 of 90 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '20 of 20 passed', + testsFailedCount: 0, + testsPassedCount: 11, + mayAssertionsCount: 0, + mustAssertionsCount: 90, + assertionsFailedCount: 0, + assertionsPassedCount: 110, + shouldAssertionsCount: 20, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 90, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 20, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 20, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 20 + }, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '11' + }, + testResults: [ + { + test: { + id: 'NjM0MeyIyIjoiMjQifQTdiZG' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.034Z' + }, + { + test: { + id: 'YWYzOeyIyIjoiMjQifQDBjN2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.120Z' + }, + { + test: { + id: 'ZmJjYeyIyIjoiMjQifQWJiNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.205Z' + }, + { + test: { + id: 'MjU2NeyIyIjoiMjQifQTk2YW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.313Z' + }, + { + test: { + id: 'MWNlNeyIyIjoiMjQifQTRhNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.410Z' + }, + { + test: { + id: 'YzFlYeyIyIjoiMjQifQjE5Yj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.489Z' + }, + { + test: { + id: 'N2UwMeyIyIjoiMjQifQTQ1OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.561Z' + }, + { + test: { + id: 'OTYwOeyIyIjoiMjQifQTE3ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.648Z' + }, + { + test: { + id: 'OWI2MeyIyIjoiMjQifQmE0ZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.736Z' + }, + { + test: { + id: 'YTU0MeyIyIjoiMjQifQjNhNj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.832Z' + }, + { + test: { + id: 'NTM4MeyIyIjoiMjQifQGVlNm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:51.954Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '10', + metrics: { + testsCount: 11, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 20, + mustFormatted: '90 of 90 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '20 of 20 passed', + testsFailedCount: 0, + testsPassedCount: 11, + mayAssertionsCount: 0, + mustAssertionsCount: 90, + assertionsFailedCount: 0, + assertionsPassedCount: 110, + shouldAssertionsCount: 20, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 90, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 20, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 20, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 20 + }, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' + }, + testPlanReport: { + id: '10' + }, + testResults: [ + { + test: { + id: 'NjM0MeyIyIjoiMjQifQTdiZG' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.048Z' + }, + { + test: { + id: 'YWYzOeyIyIjoiMjQifQDBjN2' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.128Z' + }, + { + test: { + id: 'ZmJjYeyIyIjoiMjQifQWJiNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.203Z' + }, + { + test: { + id: 'MjU2NeyIyIjoiMjQifQTk2YW' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.278Z' + }, + { + test: { + id: 'MWNlNeyIyIjoiMjQifQTRhNT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.355Z' + }, + { + test: { + id: 'YzFlYeyIyIjoiMjQifQjE5Yj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.426Z' + }, + { + test: { + id: 'N2UwMeyIyIjoiMjQifQTQ1OT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.508Z' + }, + { + test: { + id: 'OTYwOeyIyIjoiMjQifQTE3ND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.591Z' + }, + { + test: { + id: 'OWI2MeyIyIjoiMjQifQmE0ZD' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.687Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'YTU0MeyIyIjoiMjQifQjNhNj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.788Z' }, - completedAt: - '2023-08-18T03:17:08.501Z' + { + test: { + id: 'NTM4MeyIyIjoiMjQifQGVlNm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: + '2024-04-25T16:44:50.914Z' + } + ] + } + ] + } + } + ] + } + } + } + }, + { + request: { + query: testPlanReportStatusDialogQuery, + variables: { testPlanVersionId: '34' } + }, + result: { + data: { + testPlanVersion: { + id: '31', + 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' + }, + testPlanReportStatuses: [ + { + isRequired: true, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: { + id: '1', + metrics: { + testsCount: 16, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 26, + mustFormatted: '86 of 88 passed', + conflictsCount: 0, + supportPercent: 98, + shouldFormatted: '25 of 26 passed', + testsFailedCount: 6, + testsPassedCount: 10, + mayAssertionsCount: 0, + mustAssertionsCount: 88, + assertionsFailedCount: 3, + assertionsPassedCount: 111, + shouldAssertionsCount: 26, + unexpectedBehaviorCount: 1, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 2, + mustAssertionsPassedCount: 86, + shouldAssertionsFailedCount: 1, + shouldAssertionsPassedCount: 25, + unexpectedBehaviorsFormatted: '1 found', + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 26, + moderateImpactFailedAssertionCount: 1, + moderateImpactPassedAssertionCount: 25 + }, + isFinal: false, + markedFinalAt: null, + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' }, - { - test: { - id: 'MjRmNeyIyIjoiNyJ92MyMT' + testPlanReport: { + id: '1' + }, + testResults: [ + { + test: { + id: 'MTExZeyIyIjoiMzEifQWZhZG' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.281Z' + }, + { + test: { + id: 'MzJkZeyIyIjoiMzEifQTAzMm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null + }, + { + test: { + id: 'NDBjMeyIyIjoiMzEifQjc1NT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MjE2MeyIyIjoiMzEifQ2M0NW' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: null }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MWZiZeyIyIjoiMzEifQjhhYz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.541Z' }, - completedAt: - '2023-08-18T03:17:08.593Z' - }, - { - test: { - id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + { + test: { + id: 'NmI4NeyIyIjoiMzEifQDU2OD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.636Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'YmExNeyIyIjoiMzEifQWE5Nj' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.730Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'YzA3NeyIyIjoiMzEifQGZhYT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.831Z' }, - completedAt: null - }, - { - test: { - id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + { + test: { + id: 'YmYxOeyIyIjoiMzEifQDAxY2' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:35.928Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'YzIwOeyIyIjoiMzEifQGE2Yz' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.034Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'YWMwNeyIyIjoiMzEifQDQ5MG' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.126Z' }, - completedAt: - '2023-08-18T03:17:08.811Z' - }, - { - test: { - id: 'YjZkYeyIyIjoiNyJ9WIxZm' + { + test: { + id: 'MjQyMeyIyIjoiMzEifQWExMm' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.210Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'NDFiYeyIyIjoiMzEifQzg4MD' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.332Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'M2RmNeyIyIjoiMzEifQzQ0MG' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.409Z' }, - 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' - } - ] + { + test: { + id: 'ODhlYeyIyIjoiMzEifQmVmMT' + }, + atVersion: { + id: '1', + name: '2021.2111.13' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:36.510Z' + } + ] + } + ] + } + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '14', + metrics: { + testsCount: 16, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 36, + mustFormatted: '128 of 128 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '36 of 36 passed', + testsFailedCount: 0, + testsPassedCount: 16, + mayAssertionsCount: 0, + mustAssertionsCount: 128, + assertionsFailedCount: 0, + assertionsPassedCount: 164, + shouldAssertionsCount: 36, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 128, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 36, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 36, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 36 }, - { - 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' + isFinal: true, + markedFinalAt: '2022-07-07T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' }, - { - test: { - id: 'MmY0YeyIyIjoiNyJ9jRkZD' - }, - atVersion: { - id: '2', - name: '2020.4' - }, - browserVersion: { - id: '1', - name: '99.0.1' - }, - completedAt: - '2023-08-18T03:17:07.813Z' + testPlanReport: { + id: '14' }, - { - test: { - id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + testResults: [ + { + test: { + id: 'MTExZeyIyIjoiMzEifQWZhZG' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.237Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MzJkZeyIyIjoiMzEifQTAzMm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.347Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'NDBjMeyIyIjoiMzEifQjc1NT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.423Z' }, - completedAt: - '2023-08-18T03:17:07.914Z' - }, - { - test: { - id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + { + test: { + id: 'MjE2MeyIyIjoiMzEifQ2M0NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.518Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MmZmNeyIyIjoiMzEifQ2IxOG' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.613Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MWZiZeyIyIjoiMzEifQjhhYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.721Z' }, - completedAt: - '2023-08-18T03:17:07.988Z' - }, - { - test: { - id: 'MjRmNeyIyIjoiNyJ92MyMT' + { + test: { + id: 'NmI4NeyIyIjoiMzEifQDU2OD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.838Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'YmExNeyIyIjoiMzEifQWE5Nj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:55.965Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'YzA3NeyIyIjoiMzEifQGZhYT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.079Z' }, - 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 - } - ] - } - ] - } - ], - metadata: {} - } - } - } - }, - { - 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, - shouldFormatted: false, - mustFormatted: '14 of 14 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 14, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 14 - }, - markedFinalAt: null, - isFinal: false, - at: { - id: '3', - key: 'voiceover_macos', - name: 'VoiceOver for macOS' - }, - browser: { - id: '1', - key: 'firefox', - name: 'Firefox' - }, - issues: [], - draftTestPlanRuns: [ - { - tester: { - username: 'esmeralda-baggins' - }, - testPlanReport: { - id: '10' - }, - testResults: [ - { - test: { - id: 'MzlmYeyIyIjoiMjYifQzIxY2' + { + test: { + id: 'YmYxOeyIyIjoiMzEifQDAxY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.177Z' }, - atVersion: { - id: '3', - name: '11.6 (20G165)' + { + test: { + id: 'YzIwOeyIyIjoiMzEifQGE2Yz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.286Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'YWMwNeyIyIjoiMzEifQDQ5MG' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.389Z' }, - completedAt: - '2023-08-18T03:17:11.295Z' - }, - { - test: { - id: 'N2FkZeyIyIjoiMjYifQDQ5NT' + { + test: { + id: 'MjQyMeyIyIjoiMzEifQWExMm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.509Z' }, - atVersion: { - id: '3', - name: '11.6 (20G165)' + { + test: { + id: 'NDFiYeyIyIjoiMzEifQzg4MD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.639Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'M2RmNeyIyIjoiMzEifQzQ0MG' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.783Z' }, - 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' - } - ] - } - ] + { + test: { + id: 'ODhlYeyIyIjoiMzEifQmVmMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '2', + name: '99.0.4844.84' + }, + completedAt: + '2024-04-25T16:44:56.935Z' + } + ] + } + ] + } }, { - id: '9', - metrics: { - testsCount: 18, - supportLevel: 'FULL', - conflictsCount: 0, - supportPercent: 100, - testsFailedCount: 16, - testsPassedCount: 2, - shouldFormatted: false, - mustFormatted: '16 of 16 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 16, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 16 - }, - markedFinalAt: null, - isFinal: false, + isRequired: false, at: { id: '2', key: 'nvda', @@ -3393,208 +7048,200 @@ export default ( key: 'firefox', 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' - } - ] - } - ] + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: null }, { - id: '3', - metrics: { - testsCount: 18, - supportLevel: 'FAILING', - conflictsCount: 0, - supportPercent: 88, - testsFailedCount: 16, - testsPassedCount: 2, - shouldFormatted: false, - mustFormatted: '14 of 16 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 16, - unexpectedBehaviorCount: 1, - unexpectedBehaviorsFormatted: '1 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 14 - }, - markedFinalAt: '2022-07-06T00:00:00.000Z', - isFinal: true, + isRequired: true, at: { - id: '1', - key: 'jaws', - name: 'JAWS' + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' }, browser: { - id: '2', - key: 'chrome', - name: 'Chrome' + id: '3', + key: 'safari_macos', + name: 'Safari' }, - 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' + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: { + id: '15', + metrics: { + testsCount: 8, + mayFormatted: false, + supportLevel: 'FULL', + commandsCount: 22, + mustFormatted: '76 of 76 passed', + conflictsCount: 0, + supportPercent: 100, + shouldFormatted: '22 of 22 passed', + testsFailedCount: 0, + testsPassedCount: 8, + mayAssertionsCount: 0, + mustAssertionsCount: 76, + assertionsFailedCount: 0, + assertionsPassedCount: 98, + shouldAssertionsCount: 22, + unexpectedBehaviorCount: 0, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 0, + mustAssertionsPassedCount: 76, + shouldAssertionsFailedCount: 0, + shouldAssertionsPassedCount: 22, + unexpectedBehaviorsFormatted: false, + severeImpactFailedAssertionCount: 0, + severeImpactPassedAssertionCount: 22, + moderateImpactFailedAssertionCount: 0, + moderateImpactPassedAssertionCount: 22 + }, + isFinal: true, + markedFinalAt: '2022-07-07T00:00:00.000Z', + issues: [], + draftTestPlanRuns: [ + { + tester: { + username: 'esmeralda-baggins' }, - { - test: { - id: 'NWVkNeyIyIjoiMjYifQTZkOT' - }, - atVersion: { - id: '1', - name: '2021.2111.13' - }, - browserVersion: { - id: '2', - name: '99.0.4844.84' + testPlanReport: { + id: '15' + }, + testResults: [ + { + test: { + id: 'NWUyZeyIyIjoiMzEifQDVkND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.086Z' }, - completedAt: - '2023-08-18T03:17:09.134Z' - }, - { - test: { - id: 'NWM4NeyIyIjoiMjYifQDEwM2' + { + test: { + id: 'N2I0YeyIyIjoiMzEifQjEwYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.206Z' }, - atVersion: { - id: '1', - name: '2021.2111.13' + { + test: { + id: 'NmZjOeyIyIjoiMzEifQGY5ZT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.334Z' }, - browserVersion: { - id: '2', - name: '99.0.4844.84' + { + test: { + id: 'YmU1MeyIyIjoiMzEifQzBiYj' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.464Z' }, - completedAt: - '2023-08-18T03:17:09.202Z' - }, - { - test: { - id: 'NGFiZeyIyIjoiMjYifQWZiYW' + { + test: { + id: 'MGQyYeyIyIjoiMzEifQzcxZm' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.599Z' }, - atVersion: { - id: '1', - name: '2021.2111.13' + { + test: { + id: 'ZmI0YeyIyIjoiMzEifQTU2Nz' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.734Z' }, - browserVersion: { - id: '2', - name: '99.0.4844.84' + { + test: { + id: 'YmRjZeyIyIjoiMzEifQGQyND' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:57.880Z' }, - 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' - } - ] - } - ] + { + test: { + id: 'YWFmMeyIyIjoiMzEifQzMwMT' + }, + atVersion: { + id: '3', + name: '11.6 (20G165)' + }, + browserVersion: { + id: '3', + name: '14.1.2' + }, + completedAt: + '2024-04-25T16:44:58.035Z' + } + ] + } + ] + } }, { - id: '11', - metrics: { - testsCount: 11, - supportLevel: 'FULL', - conflictsCount: 0, - supportPercent: 100, - testsFailedCount: 9, - testsPassedCount: 2, - shouldFormatted: false, - mustFormatted: '14 of 14 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 14, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 14 - }, - markedFinalAt: null, - isFinal: false, + isRequired: false, at: { id: '3', key: 'voiceover_macos', @@ -3605,635 +7252,33 @@ export default ( key: 'chrome', 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, - shouldFormatted: false, - mustFormatted: '20 of 22 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 22, - unexpectedBehaviorCount: 1, - unexpectedBehaviorsFormatted: '1 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 20 - }, - markedFinalAt: '2022-07-06T00:00:00.000Z', - isFinal: true, - at: { - id: '2', - key: 'nvda', - name: 'NVDA' - }, - browser: { - id: '2', - key: 'chrome', - name: 'Chrome' + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' }, - 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' - } - ] - } - ] + exactAtVersion: null, + testPlanReport: null }, { - id: '5', - metrics: { - testsCount: 11, - supportLevel: 'FAILING', - conflictsCount: 0, - supportPercent: 92, - testsFailedCount: 8, - testsPassedCount: 3, - shouldFormatted: false, - mustFormatted: '23 of 25 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 25, - unexpectedBehaviorCount: 1, - unexpectedBehaviorsFormatted: '1 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 23 - }, - markedFinalAt: '2022-07-06T00:00:00.000Z', - isFinal: true, + isRequired: false, at: { id: '3', + key: 'voiceover_macos', 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, - shouldFormatted: false, - mustFormatted: '16 of 16 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 16, - unexpectedBehaviorCount: 0, - unexpectedBehaviorsFormatted: false, - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 0, - mustAssertionsPassedCount: 16 - }, - markedFinalAt: null, - isFinal: false, - at: { - id: '1', - key: 'jaws', - name: 'JAWS' - }, browser: { id: '1', key: 'firefox', 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' - } - ] - } - ] - } - ], - metadata: {} - } - } - } - }, - { - 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, - shouldFormatted: false, - mustFormatted: '28 of 30 passed', - shouldAssertionsCount: 0, - mustAssertionsCount: 30, - unexpectedBehaviorCount: 1, - unexpectedBehaviorsFormatted: '1 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 0, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 28 - }, - markedFinalAt: null, - isFinal: false, - at: { - id: '1', - key: 'jaws', - name: 'JAWS' - }, - browser: { - id: '2', - key: 'chrome', - name: 'Chrome' + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' }, - 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' - } - ] - } - ] + exactAtVersion: null, + testPlanReport: null } - ], - metadata: {} + ] } } } diff --git a/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js b/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js index 54b84d016..32a9a824b 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestPlanReportStatusDialogMock.js @@ -12,28 +12,66 @@ export const mockedTestPlanVersion = { testPlan: { directory: 'combobox-select-only' }, - testPlanReports: [ + testPlanReportStatuses: [ { - id: '2', - metrics: { - testsCount: 21, - supportLevel: 'FAILING', - conflictsCount: 5, - supportPercent: 96, - testsFailedCount: 16, - testsPassedCount: 5, - shouldFormatted: '3 of 3 passed', - mustFormatted: '48 of 50 passed', - shouldAssertionsCount: 3, - mustAssertionsCount: 50, - unexpectedBehaviorCount: 3, - unexpectedBehaviorsFormatted: '3 found', - shouldAssertionsFailedCount: 0, - shouldAssertionsPassedCount: 3, - mustAssertionsFailedCount: 2, - mustAssertionsPassedCount: 48 + isRequired: true, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '1', + key: 'jaws', + name: 'JAWS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '1', + name: '2021.2111.13' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: true, + at: { + id: '2', + key: 'nvda', + name: 'NVDA' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '2', + name: '2020.4' }, - markedFinalAt: null, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, at: { id: '2', key: 'nvda', @@ -44,239 +82,663 @@ export const mockedTestPlanVersion = { key: 'firefox', 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' + minimumAtVersion: { + id: '2', + name: '2020.4' + }, + exactAtVersion: null, + testPlanReport: { + id: '2', + metrics: { + testsCount: 21, + mayFormatted: false, + supportLevel: 'FAILING', + commandsCount: 24, + mustFormatted: '118 of 122 passed', + conflictsCount: 3, + supportPercent: 97, + shouldFormatted: '34 of 36 passed', + testsFailedCount: 6, + testsPassedCount: 15, + mayAssertionsCount: 0, + mustAssertionsCount: 122, + assertionsFailedCount: 6, + assertionsPassedCount: 152, + shouldAssertionsCount: 36, + unexpectedBehaviorCount: 3, + mayAssertionsFailedCount: 0, + mayAssertionsPassedCount: 0, + mustAssertionsFailedCount: 4, + mustAssertionsPassedCount: 118, + shouldAssertionsFailedCount: 2, + shouldAssertionsPassedCount: 34, + unexpectedBehaviorsFormatted: '3 found', + severeImpactFailedAssertionCount: 1, + severeImpactPassedAssertionCount: 23, + moderateImpactFailedAssertionCount: 2, + moderateImpactPassedAssertionCount: 22 + }, + isFinal: false, + markedFinalAt: null, + issues: [ + { + link: 'https://github.com/bocoup/aria-at/issues/128#issue-2157878584', + isOpen: true, + feedbackType: 'FEEDBACK' + } + ], + draftTestPlanRuns: [ + { + tester: { + username: 'tom-proudfeet' }, - { - test: { - id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' - }, - atVersion: { - id: '2', - name: '2020.4' - }, - browserVersion: { - id: '1', - name: '99.0.1' - }, - completedAt: '2023-08-18T03:17:08.412Z' + testPlanReport: { + id: '2' }, - { - test: { - id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + testResults: [ + { + test: { + id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.949Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.070Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.180Z' }, - completedAt: '2023-08-18T03:17:08.501Z' - }, - { - test: { - id: 'MjRmNeyIyIjoiNyJ92MyMT' + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.292Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.406Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null }, - completedAt: '2023-08-18T03:17:08.593Z' - }, - { - test: { - id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + { + test: { + id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.640Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.761Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:39.888Z' }, - completedAt: null - }, - { - test: { - id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + { + test: { + id: 'MmZkNeyIyIjoiNyJ9zIwN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.006Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'ZmQwOeyIyIjoiNyJ9DEzYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.122Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MGViNeyIyIjoiNyJ9GQ3MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.234Z' }, - completedAt: '2023-08-18T03:17:08.811Z' - }, - { - test: { - id: 'YjZkYeyIyIjoiNyJ9WIxZm' + { + test: { + id: 'YTg5MeyIyIjoiNyJ9WEzOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.355Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'NTRjMeyIyIjoiNyJ9zQ0OD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.467Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MjRlZeyIyIjoiNyJ9DcyY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.588Z' }, - completedAt: '2023-08-18T03:17:08.902Z' - }, - { - test: { - id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + { + test: { + id: 'YWQzNeyIyIjoiNyJ9mE2Nm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.712Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'OTYxOeyIyIjoiNyJ9TdmYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.826Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MjgzNeyIyIjoiNyJ9TZjNz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:40.948Z' }, - completedAt: '2023-08-18T03:17:08.996Z' - } - ] - }, - { - tester: { - username: 'esmeralda-baggins' - }, - testPlanReport: { - id: '2' + { + test: { + id: 'NWNiZeyIyIjoiNyJ9jI2MD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:41.075Z' + } + ] }, - testResults: [ - { - test: { - id: 'Nzg5NeyIyIjoiNyJ9zNjZj' + { + 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: '2024-04-25T16:44:36.666Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MmY0YeyIyIjoiNyJ9jRkZD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:36.793Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:36.914Z' }, - completedAt: '2023-08-18T03:17:07.718Z' - }, - { - test: { - id: 'MmY0YeyIyIjoiNyJ9jRkZD' + { + test: { + id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.031Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'MjRmNeyIyIjoiNyJ92MyMT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.150Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: null }, - completedAt: '2023-08-18T03:17:07.813Z' - }, - { - test: { - id: 'ZjUwNeyIyIjoiNyJ9mE2ZT' + { + test: { + id: 'YWFiNeyIyIjoiNyJ9zE2Zj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.384Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'YjZkYeyIyIjoiNyJ9WIxZm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.512Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'ZmIzMeyIyIjoiNyJ9TQ1NW' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.638Z' }, - completedAt: '2023-08-18T03:17:07.914Z' - }, - { - test: { - id: 'MDNiMeyIyIjoiNyJ9Dk1MT' + { + test: { + id: 'MmZkNeyIyIjoiNyJ9zIwN2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.760Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'ZmQwOeyIyIjoiNyJ9DEzYz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:37.883Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MGViNeyIyIjoiNyJ9GQ3MT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.014Z' }, - completedAt: '2023-08-18T03:17:07.988Z' - }, - { - test: { - id: 'MjRmNeyIyIjoiNyJ92MyMT' + { + test: { + id: 'YTg5MeyIyIjoiNyJ9WEzOT' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.145Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'NTRjMeyIyIjoiNyJ9zQ0OD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.268Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MjRlZeyIyIjoiNyJ9DcyY2' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.382Z' }, - completedAt: '2023-08-18T03:17:08.074Z' - }, - { - test: { - id: 'ZmVlMeyIyIjoiNyJ9mUyYj' + { + test: { + id: 'YWQzNeyIyIjoiNyJ9mE2Nm' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.481Z' }, - atVersion: { - id: '2', - name: '2020.4' + { + test: { + id: 'OTYxOeyIyIjoiNyJ9TdmYj' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.596Z' }, - browserVersion: { - id: '1', - name: '99.0.1' + { + test: { + id: 'MjgzNeyIyIjoiNyJ9TZjNz' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.701Z' }, - completedAt: null - } - ] - } - ] + { + test: { + id: 'NWNiZeyIyIjoiNyJ9jI2MD' + }, + atVersion: { + id: '2', + name: '2020.4' + }, + browserVersion: { + id: '1', + name: '99.0.1' + }, + completedAt: '2024-04-25T16:44:38.811Z' + } + ] + } + ] + } + }, + { + isRequired: true, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '3', + key: 'safari_macos', + name: 'Safari' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '2', + key: 'chrome', + name: 'Chrome' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null + }, + { + isRequired: false, + at: { + id: '3', + key: 'voiceover_macos', + name: 'VoiceOver for macOS' + }, + browser: { + id: '1', + key: 'firefox', + name: 'Firefox' + }, + minimumAtVersion: { + id: '3', + name: '11.6 (20G165)' + }, + exactAtVersion: null, + testPlanReport: null } ] }; @@ -307,92 +769,7 @@ export default ( }, result: { data: { - testPlanVersion: mockedTestPlanVersion, - ats: [ - { - id: '1', - key: 'jaws', - name: 'JAWS', - atVersions: [ - { - id: '1', - name: '2021.2111.13', - releasedAt: '2021-11-01T04:00:00.000Z' - } - ], - browsers: [ - { - id: '2', - key: 'chrome', - name: 'Chrome' - }, - { - id: '1', - key: 'firefox', - name: 'Firefox' - } - ], - candidateBrowsers: [{ id: '2' }], - recommendedBrowsers: [{ id: '1' }, { id: '2' }] - }, - { - id: '2', - key: 'nvda', - name: 'NVDA', - atVersions: [ - { - id: '2', - name: '2020.4', - releasedAt: '2021-02-19T05:00:00.000Z' - } - ], - browsers: [ - { - id: '2', - key: 'chrome', - name: 'Chrome' - }, - { - id: '1', - key: 'firefox', - name: 'Firefox' - } - ], - candidateBrowsers: [{ id: '2' }], - recommendedBrowsers: [{ id: '1' }, { id: '2' }] - }, - { - id: '3', - key: 'voiceover_macos', - name: 'VoiceOver for macOS', - atVersions: [ - { - id: '3', - name: '11.6 (20G165)', - releasedAt: '2019-09-01T04:00:00.000Z' - } - ], - browsers: [ - { - id: '2', - key: 'chrome', - name: 'Chrome' - }, - { - id: '1', - key: 'firefox', - name: 'Firefox' - }, - { - id: '3', - key: 'safari', - name: 'Safari' - } - ], - candidateBrowsers: [{ id: '3' }], - recommendedBrowsers: [{ id: '2' }, { id: '3' }] - } - ] + testPlanVersion: mockedTestPlanVersion } } }, @@ -427,7 +804,12 @@ export default ( name: 'Firefox' } } - ] + ], + metadata: { + exampleUrl: 'https://fakeurl.com/exampleUrl', + designPatternUrl: 'https://fakeurl.com/designPattern', + testFormatVersion: 1 + } }, oldTestPlanVersions: [] } diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js index 0625ca224..7012bd518 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js @@ -26,7 +26,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -91,7 +91,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -136,7 +136,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -254,6 +254,8 @@ export default testQueuePageQuery => [ runnableTestsLength: 17, markedFinalAt: null, at: { id: '1', name: 'JAWS', key: 'jaws' }, + minimumAtVersion: { id: '1', name: '2024.3321.1' }, + exactAtVersion: null, browser: { id: '2', name: 'Chrome', key: 'chrome' }, testPlanVersion: { id: '1', @@ -288,6 +290,8 @@ export default testQueuePageQuery => [ name: 'VoiceOver for macOS', key: 'voiceover_macos' }, + minimumAtVersion: { id: '3', name: '14.5' }, + exactAtVersion: null, browser: { id: '3', name: 'Safari', @@ -322,6 +326,8 @@ export default testQueuePageQuery => [ runnableTestsLength: 17, markedFinalAt: null, at: { id: '2', name: 'NVDA', key: 'nvda' }, + minimumAtVersion: null, + exactAtVersion: { id: '2', name: '2024.2' }, browser: { id: '1', name: 'Firefox', key: 'firefox' }, testPlanVersion: { id: '1', diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js index 06dfcc0f6..13afebebd 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js @@ -103,7 +103,12 @@ export default (testPlanReportAtBrowserQuery, existingTestPlanReportsQuery) => [ key: 'chrome' } } - ] + ], + metadata: { + exampleUrl: 'https://fakeurl.com/exampleUrl', + designPatternUrl: 'https://fakeurl.com/designPattern', + testFormatVersion: 1 + } }, oldTestPlanVersions: [] } diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js index dcf29263d..f4be09013 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js @@ -26,7 +26,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -91,7 +91,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -136,7 +136,7 @@ export default testQueuePageQuery => [ browsers: [ { id: '3', - key: 'safari', + key: 'safari_macos', name: 'Safari' }, { @@ -258,6 +258,8 @@ export default testQueuePageQuery => [ key: 'nvda', name: 'NVDA' }, + minimumAtVersion: { id: '1', name: '2024.3321.1' }, + exactAtVersion: null, browser: { id: '1', key: 'firefox', @@ -308,6 +310,8 @@ export default testQueuePageQuery => [ key: 'jaws', name: 'JAWS' }, + minimumAtVersion: { id: '3', name: '14.5' }, + exactAtVersion: null, browser: { id: '1', key: 'firefox', @@ -348,6 +352,8 @@ export default testQueuePageQuery => [ key: 'voiceover_macos', name: 'VoiceOver for macOS' }, + minimumAtVersion: null, + exactAtVersion: { id: '2', name: '2024.2' }, browser: { id: '1', key: 'firefox', diff --git a/client/tests/calculateTestPlanReportCompletionPercentage.test.js b/client/tests/calculatePercentComplete.test.js similarity index 73% rename from client/tests/calculateTestPlanReportCompletionPercentage.test.js rename to client/tests/calculatePercentComplete.test.js index dd72bb904..03cda0adf 100644 --- a/client/tests/calculateTestPlanReportCompletionPercentage.test.js +++ b/client/tests/calculatePercentComplete.test.js @@ -1,15 +1,13 @@ -import { calculateTestPlanReportCompletionPercentage } from '../components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage'; +import { calculatePercentComplete } from '../utils/calculatePercentComplete'; -describe('calculateTestPlanReportCompletionPercentage', () => { +describe('calculatePercentComplete', () => { const testResult = (id, completedAt = null) => ({ id, completedAt }); test('returns 0 when metrics or draftTestPlanRuns is not defined', () => { - expect(calculateTestPlanReportCompletionPercentage({})).toBe(0); + expect(calculatePercentComplete({})).toBe(0); + expect(calculatePercentComplete({ metrics: {} })).toBe(0); expect( - calculateTestPlanReportCompletionPercentage({ metrics: {} }) - ).toBe(0); - expect( - calculateTestPlanReportCompletionPercentage({ + calculatePercentComplete({ draftTestPlanRuns: [] }) ).toBe(0); @@ -17,7 +15,7 @@ describe('calculateTestPlanReportCompletionPercentage', () => { test('returns 0 when draftTestPlanRuns is empty', () => { expect( - calculateTestPlanReportCompletionPercentage({ + calculatePercentComplete({ metrics: { testsCount: 5 }, draftTestPlanRuns: [] }) @@ -38,7 +36,7 @@ describe('calculateTestPlanReportCompletionPercentage', () => { ]; expect( - calculateTestPlanReportCompletionPercentage({ + calculatePercentComplete({ metrics, draftTestPlanRuns }) @@ -61,7 +59,7 @@ describe('calculateTestPlanReportCompletionPercentage', () => { // (NUMBER_COMPLETED_TESTS_BY_ALL_TESTERS / (NUMBER_ASSIGNED_TESTERS * NUMBER_TESTS_IN_PLAN)) * 100 // (5 / (2 * 5)) * 100 = 50 expect( - calculateTestPlanReportCompletionPercentage({ + calculatePercentComplete({ metrics, draftTestPlanRuns }) diff --git a/client/tests/smokeTest.test.js b/client/tests/smokeTest.test.js index 9042bb094..3818b1d3b 100644 --- a/client/tests/smokeTest.test.js +++ b/client/tests/smokeTest.test.js @@ -1,14 +1,14 @@ import getPage from './util/getPage'; describe('smoke test', () => { - it('end-to-end tests can simultaneously sign in with all roles', async () => { + it('end-to-end tests can simultaneously sign in with all roles [old]', async () => { await Promise.all([ - getPage({ role: 'admin', url: '/test-queue' }, async page => { + getPage({ role: 'admin', url: '/test-queue-old' }, async page => { // Only admins can remove rows from the test queue await page.waitForSelector('td.actions ::-p-text(Remove)'); }), - getPage({ role: 'tester', url: '/test-queue' }, async page => { + getPage({ role: 'tester', url: '/test-queue-old' }, async page => { // Testers can assign themselves await page.waitForSelector('table ::-p-text(Assign Yourself)'); const adminOnlyRemoveButton = await page.$( @@ -18,7 +18,7 @@ describe('smoke test', () => { }), getPage( - { role: 'vendor', url: '/test-queue' }, + { role: 'vendor', url: '/test-queue-old' }, async (page, { baseUrl }) => { // Vendors get the same test queue as signed-out users await page.waitForSelector( @@ -30,13 +30,60 @@ describe('smoke test', () => { } ), - getPage({ role: false, url: '/test-queue' }, async page => { + getPage({ role: false, url: '/test-queue-old' }, async page => { // Signed-out users can only view tests, not run them await page.waitForSelector('td.actions ::-p-text(View tests)'); }) ]); }); + it('end-to-end tests can simultaneously sign in with all roles', async () => { + await Promise.all([ + getPage({ role: 'admin', url: '/test-queue' }, async page => { + // Only admins can remove rows from the test queue + await page.waitForSelector( + 'td [type="button"] ::-p-text(Delete Report)' + ); + }), + + getPage({ role: 'tester', url: '/test-queue' }, async page => { + // Testers can assign themselves + await page.waitForSelector('table ::-p-text(Assign Yourself)'); + const adminOnlyRemoveButton = await page.$( + 'td [type="button"] ::-p-text(Delete Report)' + ); + expect(adminOnlyRemoveButton).toBe(null); + }), + + getPage( + { role: 'vendor', url: '/test-queue' }, + async (page, { baseUrl }) => { + // Vendors get the same test queue as signed-out users + await page.waitForSelector('button ::-p-text(V22.04.14)'); + await page.click('button ::-p-text(V22.04.14)'); + + await page.waitForSelector('td [role="button"]'); + const buttonText = await page.$eval( + 'td [role="button"]', + button => button.textContent + ); + expect(buttonText).toEqual('View Tests'); + + // Unlike signed-out users, they will get tables on this page + await page.goto(`${baseUrl}/candidate-review`); + await page.waitForSelector('table'); + } + ), + + getPage({ role: false, url: '/test-queue' }, async page => { + // Signed-out users can only view tests, not run them + await page.waitForSelector( + 'td [role="button"] ::-p-text(View Tests)' + ); + }) + ]); + }); + it('loads various pages without crashing', async () => { await Promise.all([ getPage({ role: false, url: '/' }, async page => { diff --git a/client/tests/util/getPage.js b/client/tests/util/getPage.js index ed30a7f46..6ce14f783 100644 --- a/client/tests/util/getPage.js +++ b/client/tests/util/getPage.js @@ -38,6 +38,22 @@ const startServer = async serverOrClient => { } ); + server.on('error', error => { + throw new Error('Error raised by startServer process', { + cause: error + }); + }); + + server.on('exit', (code, signal) => { + if (code) { + console.error('startServer exited with code', code); + } else if (signal) { + console.error('startServer was killed with signal', signal); + } else { + console.info('startServer exited with no errors.'); // eslint-disable-line no-console + } + }); + const killServer = async () => { await new Promise((resolve, reject) => { treeKill(server.pid, error => { diff --git a/client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js b/client/utils/calculatePercentComplete.js similarity index 84% rename from client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js rename to client/utils/calculatePercentComplete.js index 2df1ec9a0..f437e0a3c 100644 --- a/client/components/TestPlanReportStatusDialog/calculateTestPlanReportCompletionPercentage.js +++ b/client/utils/calculatePercentComplete.js @@ -1,7 +1,4 @@ -export const calculateTestPlanReportCompletionPercentage = ({ - metrics, - draftTestPlanRuns -}) => { +export const calculatePercentComplete = ({ metrics, draftTestPlanRuns }) => { if (!metrics || !draftTestPlanRuns) return 0; const assignedUserCount = draftTestPlanRuns.length || 1; const totalTestsPossible = metrics.testsCount * assignedUserCount; diff --git a/client/utils/createIssueLink.js b/client/utils/createIssueLink.js index 253d17517..4085b398f 100644 --- a/client/utils/createIssueLink.js +++ b/client/utils/createIssueLink.js @@ -9,6 +9,28 @@ const atLabelMap = { NVDA: 'nvda' }; +/** + * Creates a link to open a new issue on the GitHub repository. + * + * @param {Object} options - Options for creating the issue link + * @param {boolean} [options.isCandidateReview=false] - Whether this is a candidate review + * @param {boolean} [options.isCandidateReviewChangesRequested=false] - Whether changes are requested for a candidate review + * @param {string} [options.testPlanDirectory] - The directory of the test plan + * @param {string} [options.testPlanTitle] - The title of the test plan + * @param {string} [options.versionString] - The version string + * @param {string|null} [options.testTitle=null] - The title of the test + * @param {number|null} [options.testSequenceNumber=null] - The sequence number of the test. This is the number displayed to test runners + * @param {number|null} [options.testRowNumber=null] - The row number of the test in aria-at + * @param {string|null} [options.testRenderedUrl=null] - The rendered URL of the test + * @param {string} options.atName - The name of the assistive technology + * @param {string|null} [options.atVersionName=null] - The version name of the assistive technology + * @param {string|null} [options.browserName=null] - The name of the browser + * @param {string|null} [options.browserVersionName=null] - The version name of the browser + * @param {string|null} [options.conflictMarkdown=null] - The conflict markdown + * @param {string|null} [options.reportLink=null] - The link to the report + * @returns {string} The URL for creating a new issue on the GitHub repository + * @throws {Error} If required parameters are missing + */ const createIssueLink = ({ isCandidateReview = false, isCandidateReviewChangesRequested = false, @@ -16,6 +38,7 @@ const createIssueLink = ({ testPlanTitle, versionString, testTitle = null, + testSequenceNumber = null, testRowNumber = null, testRenderedUrl = null, atName, @@ -29,7 +52,12 @@ const createIssueLink = ({ throw new Error('Cannot create issue link due to missing parameters'); } - const hasTest = !!(testTitle && testRowNumber && testRenderedUrl); + const hasTest = !!( + testTitle && + testSequenceNumber && + testRowNumber && + testRenderedUrl + ); let title; if (hasTest) { @@ -44,7 +72,7 @@ const createIssueLink = ({ title = `${titleStart}: "${testTitle}" (${testPlanTitle}, ` + - `Test ${testRowNumber}, ${versionString})`; + `Test ${testSequenceNumber}, ${versionString})`; } else { title = `${atName} General Feedback: ${testPlanTitle} ${versionString}`; } @@ -99,6 +127,7 @@ const createIssueLink = ({ atName, browserName, testRowNumber, + testSequenceNumber, isCandidateReview, isCandidateReviewChangesRequested }); @@ -121,6 +150,19 @@ const createIssueLink = ({ ); }; +/** + * Returns a link to search for existing issues on the GitHub repository based on the provided parameters. + * + * @param {Object} options - Options for generating the issue search link + * @param {boolean} [options.isCandidateReview=false] - Whether this is a candidate review + * @param {boolean} [options.isCandidateReviewChangesRequested=false] - Whether changes are requested for a candidate review + * @param {string|null} [options.username=null] - The username of the author + * @param {string} options.atName - The name of the assistive technology + * @param {string} options.testPlanTitle - The title of the test plan + * @param {string} options.versionString - The version string + * @param {number|null} [options.testSequenceNumber=null] - The sequence number of the test, this is the test number displayed to test runners + * @returns {string} The URL for searching issues on the GitHub repository + */ export const getIssueSearchLink = ({ isCandidateReview = false, isCandidateReviewChangesRequested = false, @@ -128,7 +170,7 @@ export const getIssueSearchLink = ({ atName, testPlanTitle, versionString, - testRowNumber = null + testSequenceNumber = null }) => { let atKey; if (atName === 'JAWS' || atName === 'NVDA') { @@ -146,7 +188,7 @@ export const getIssueSearchLink = ({ username ? `author:${username}` : '', `label:${atKey}`, `"${testPlanTitle}"`, - testRowNumber ? `Test ${testRowNumber}` : '', + testSequenceNumber ? `Test ${testSequenceNumber}` : '', versionString ] .filter(str => str) diff --git a/client/utils/evaluateAuth.js b/client/utils/evaluateAuth.js index 6c41f5c40..34fcc145a 100644 --- a/client/utils/evaluateAuth.js +++ b/client/utils/evaluateAuth.js @@ -9,19 +9,21 @@ * @param {string[]} data.roles - currently logged in user's assigned roles * @returns {Auth} - evaluated auth object */ -export const evaluateAuth = (data = {}) => { - const roles = data.roles || []; +export const evaluateAuth = user => { + if (!user) user = {}; + + let roles = user.roles ?? []; return { // calculated booleans isAdmin: roles.includes('ADMIN'), isTester: roles.includes('TESTER'), isVendor: roles.includes('VENDOR'), - isSignedIn: !!data.username, + isSignedIn: !!user.username, // user object values - id: data.id || null, - username: data.username || null, + id: user.id ?? null, + username: user.username ?? null, roles }; }; diff --git a/package.json b/package.json index 8dc42e996..430f2a578 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aria-at-app", - "version": "1.3.1", + "version": "1.4.0", "description": "Run ARIA-AT tests and report results", "main": "server/index.js", "private": true, diff --git a/server/graphql-schema.js b/server/graphql-schema.js index 6d16220c4..440c04f9f 100644 --- a/server/graphql-schema.js +++ b/server/graphql-schema.js @@ -368,11 +368,11 @@ const graphqlSchema = gql` version was imported from the ARIA-AT repo. Used to version the test plan over time. """ - gitSha: String! # TODO: remove if using version labels + gitSha: String! """ Git commit message corresponding to the git sha's commit. """ - gitMessage: String! # TODO: remove if using version labels + gitMessage: String! """ 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 @@ -401,8 +401,7 @@ const graphqlSchema = gql` """ tests: [Test]! """ - The TestPlanReports attached to the TestPlanVersion. There will always - be a unique combination of AT + Browser + TestPlanVersion. + The TestPlanReports attached to the TestPlanVersion. isFinal is used to check if a TestPlanReport has been "Marked as Final", indicated by TestPlanReport.markedFinalAt existence. @@ -411,6 +410,77 @@ const graphqlSchema = gql` False value indicates to return the reports which have no markedFinalAt date. """ testPlanReports(isFinal: Boolean): [TestPlanReport]! + """ + A list of existing or missing TestPlanReports that may be collected. + """ + testPlanReportStatuses: [TestPlanReportStatus]! + """ + For each report under this TestPlanVersion, if the report's combination + is indicated as required and the report is marked as final at the time + the TestPlanVersion is updated to RECOMMENDED then by checking the + testers' runs which have been marked as primary, the earliest found AT + version for the respective ATs should be considered as the + first required AT version or "earliestAtVersion". + + The "earliest" is determined by comparing the recorded AtVersions' + releasedAt value. + + Required Reports definition and combinations are defined at + https://github.com/w3c/aria-at-app/wiki/Business-Logic-and-Processes#required-reports. + + Primary Test Plan Run is defined at + https://github.com/w3c/aria-at-app/wiki/Business-Logic-and-Processes#primary-test-plan-run. + + After this TestPlanVersion is updated to RECOMMENDED, this should be + used to ensure subsequent reports created under the TestPlanVersion + should only being capturing results for AT Versions which are the same + as or were released after the "earliestAtVersion". + """ + earliestAtVersion(atId: ID!): AtVersion + } + + """ + An existing or missing TestPlanReport that can be collected for a given + TestPlanVersion. + """ + type TestPlanReportStatus { + """ + Whether the TestPlanReport is actually required during the given + TestPlanVersion phase. + """ + isRequired: Boolean! + """ + The report's AT, which will be populated even if the TestPlanReport is + missing. + """ + at: At! + """ + The version of the AT that should be used for the report will be + specified either as an exactAtVersion or minimumAtVersion and will be + populated even if the TestPlanReport is missing. + + During the TestPlanVersion's draft and candidate phases, the looser + requirement of minimumAtVersion will be used to reduce the amount of + data collected before consensus is achieved. + + During the recommended phase all reports will be associated with an + exactAtVersion, enabling large-scale data collection for all versions + of the AT as they are released. + """ + exactAtVersion: AtVersion + """ + See exactAtVersion for more information. + """ + minimumAtVersion: AtVersion + """ + The report's browser, which will be populated even if the + TestPlanReport is missing. + """ + browser: Browser! + """ + The TestPlanReport, which may not currently exist. + """ + testPlanReport: TestPlanReport } """ @@ -979,6 +1049,22 @@ const graphqlSchema = gql` """ at: At! """ + Either a minimumAtVersion or exactAtVersion will be available. The + minimumAtVersion, when defined, is the oldest version of the AT that + testers are allowed to use when collecting results. + """ + minimumAtVersion: AtVersion + """ + Either a minimumAtVersion or exactAtVersion will be available. The + exactAtVersion, when defined, is the only version of the AT that + testers are allowed to use when collecting results. Note that when a + TestPlanVersion reaches the recommended stage, all its reports will + automatically switch from having a minimumAtVersion to an + exactAtVersion. See the earliestAtVersion field of TestPlanVersion for + more information. + """ + exactAtVersion: AtVersion + """ The unique AT Versions used when collecting results for this report. """ atVersions: [AtVersion]! @@ -1055,6 +1141,15 @@ const graphqlSchema = gql` Indicated by TestPlanReport.markedFinalAt existence, after a report has been "marked as final". """ isFinal: Boolean! + """ + The AtVersion to display for a TestPlanReport only when the + TestPlanVersion is RECOMMENDED. + + If this TestPlanReport was created with an "exactAtVersionId" being set, + it will use the matching AtVersion, otherwise it will use the + TestPlanVersion.earliestAtVersion as a default. + """ + recommendedAtVersion: AtVersion } """ @@ -1063,8 +1158,10 @@ const graphqlSchema = gql` input TestPlanReportInput { testPlanVersionId: ID! atId: ID! + exactAtVersionId: ID + minimumAtVersionId: ID browserId: ID! - copyResultsFromTestPlanReportId: ID + copyResultsFromTestPlanVersionId: ID } """ @@ -1142,7 +1239,7 @@ const graphqlSchema = gql` """ Get all TestPlans. """ - testPlans: [TestPlan]! + testPlans(testPlanVersionPhases: [TestPlanVersionPhase]): [TestPlan]! """ Load a particular TestPlan by ID. """ @@ -1274,23 +1371,17 @@ const graphqlSchema = gql` 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. + + Also optionally set a "primary test plan run" so a specific tester's output + will be shown for on the report pages over another. """ - markAsFinal: PopulatedData! + markAsFinal(primaryTestPlanRunId: ID): PopulatedData! """ Remove the TestPlanReport's markedFinalAt date. This allows the TestPlanReport to be worked on in the Test Queue page again if was previously marked as final. """ unmarkAsFinal: PopulatedData! """ - Update the report to a specific TestPlanVersion id. - """ - updateTestPlanReportTestPlanVersion( - """ - The TestPlanReport to update. - """ - input: TestPlanReportInput! - ): PopulatedData! - """ Move the vendor review status from READY to IN PROGRESS or IN PROGRESS to APPROVED """ @@ -1383,25 +1474,6 @@ const graphqlSchema = gql` retryCanceledCollections: CollectionJob! } - """ - Generic response to findOrCreate mutations, which allow you to dictate an - expectation of what you want to exist, and it will be made so. It allows you - to check whether new database records were created. - """ - type FindOrCreateResult { - """ - The data that was found or created, as well as any implicit - associations. For example, if you find or create a TestPlanReport, this - will include the TestPlanReport as well as the TestPlanVersion and - TestPlan. - """ - populatedData: PopulatedData! - """ - There will be one array item per database record created. - """ - created: [PopulatedData]! - } - type Mutation { """ Get the available mutations for the given AT. @@ -1416,17 +1488,18 @@ const graphqlSchema = gql` """ browser(id: ID!): BrowserOperations! """ - 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. + Adds an empty report to the test queue, a container for related test + results. Each report must be scoped to a specific TestPlanVersion, AT + and Browser. Optionally, either a minimum or exact AT version + requirement can be included to constrain the versions testers are + allowed to use to run the tests. """ - findOrCreateTestPlanReport( + createTestPlanReport( """ - The TestPlanReport to find or create. + The TestPlanReport to create. """ input: TestPlanReportInput! - ): FindOrCreateResult! + ): PopulatedData! """ Get the available mutations for the given TestPlanReport. """ diff --git a/server/handlebars/embed/public/style.css b/server/handlebars/embed/public/style.css index 6899c4b74..7361be738 100644 --- a/server/handlebars/embed/public/style.css +++ b/server/handlebars/embed/public/style.css @@ -30,6 +30,7 @@ a:focus-visible { details { margin-bottom: 1em; } + details summary:focus-visible { outline-offset: -2px; outline: 2px solid #3a86d1; @@ -97,7 +98,6 @@ details > summary > h4 { position: relative; padding-left: var(--left-right-padding); padding-right: var(--left-right-padding); - font-family: Arial, Helvetica, sans-serif; font-size: 1em; color: #60470c; @@ -233,19 +233,23 @@ table tbody tr td { background-color: #175a6a; margin-right: 6px; } + #view-report-button:hover { background-color: #024e5c; border-color: #024e5c; } + #view-report-button:focus-visible { margin-left: 4px; padding: 0 8px 0 12px; } + #embed-button { color: #175a6a; background-color: white; margin-left: 4px; } + #embed-button:hover { background-color: #f4f4f4; } @@ -256,11 +260,13 @@ table tbody tr td { padding: 0 12px; line-height: 36px; } + #view-report-button:focus-visible, #embed-button:focus-visible { outline-offset: 2px; outline: 2px solid #3a86d1; } + #view-report-button svg, #embed-button svg { width: 24px; diff --git a/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js b/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js deleted file mode 100644 index 307759c23..000000000 --- a/server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js +++ /dev/null @@ -1,634 +0,0 @@ -'use strict'; - -const { - TEST_PLAN_REPORT_ATTRIBUTES, - TEST_PLAN_VERSION_ATTRIBUTES -} = require('../models/services/helpers'); -const scenariosResolver = require('../resolvers/Test/scenariosResolver'); -const { - getTestPlanReportById, - getOrCreateTestPlanReport, - updateTestPlanReportById -} = require('../models/services/TestPlanReportService'); -const populateData = require('../services/PopulatedData/populateData'); -const { - createTestPlanRun, - getTestPlanRunById -} = require('../models/services/TestPlanRunService'); -const { hashTests } = require('../util/aria'); -const testResultsResolver = require('../resolvers/TestPlanRun/testResultsResolver'); -const submitTestResultResolver = require('../resolvers/TestResultOperations/submitTestResultResolver'); -const saveTestResultResolver = require('../resolvers/TestResultOperations/saveTestResultResolver'); -const findOrCreateTestResultResolver = require('../resolvers/TestPlanRunOperations/findOrCreateTestResultResolver'); -const getGraphQLContext = require('../graphql-context'); - -const testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES.filter( - attr => attr !== 'versionString' -); - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - 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 - }; - } - ), - 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 = getGraphQLContext({ - req: { - transaction, - session: { 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, atIds, id, scenarios, title }) => { - return { - id, - title, - ats: atIds.map(atId => ({ - id: atId - })), - scenarios: scenariosResolver( - { scenarios }, - { atId }, - context - ).map(({ commandIds }) => { - return { - commands: commandIds.map(commandId => ({ - text: commandId - })) - }; - }), - assertions: assertions.map( - ({ priority, text }) => ({ - priority, - text - }) - ) - }; - } - ) - }; - - const currentTestPlanReport = await getTestPlanReportById({ - id: testPlanReportId, - testPlanReportAttributes, - testPlanVersionAttributes, - transaction - }); - - for ( - let i = 0; - i < currentTestPlanReport.testPlanRuns.length; - i++ - ) { - const testPlanRunId = - currentTestPlanReport.testPlanRuns[i].id; - const testPlanRun = await getTestPlanRunById({ - id: testPlanRunId, - testPlanReportAttributes, - testPlanVersionAttributes, - transaction - }); - // testPlanReport = testPlanRun?.testPlanReport; - - testPlanRun.testResults = await testResultsResolver( - 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 - })), - scenarios: scenariosResolver( - { - scenarios: - test.scenarios - }, - { atId }, - context - ).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( - ({ - passed - }) => ({ - passed - }) - ), - 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({ - where: { - testPlanVersionId: newTestPlanVersionId, - atId, - browserId - }, - testPlanReportAttributes, - testPlanVersionAttributes, - transaction - }); - - const candidatePhaseReachedAt = - currentTestPlanReport.candidatePhaseReachedAt; - const recommendedPhaseReachedAt = - currentTestPlanReport.recommendedPhaseReachedAt; - const recommendedPhaseTargetDate = - currentTestPlanReport.recommendedPhaseTargetDate; - const vendorReviewStatus = - currentTestPlanReport.vendorReviewStatus; - - await updateTestPlanReportById({ - id: foundOrCreatedTestPlanReport.id, - values: { - candidatePhaseReachedAt, - recommendedPhaseReachedAt, - recommendedPhaseTargetDate, - vendorReviewStatus - }, - testPlanReportAttributes, - testPlanVersionAttributes, - transaction - }); - - // 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({ - values: { - testPlanReportId: foundOrCreatedTestPlanReport.id, - testerUserId: testPlanRun.tester.id - }, - testPlanReportAttributes, - testPlanVersionAttributes, - transaction - }); - - 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 findOrCreateTestResultResolver( - { parentContext: { id: testPlanRunId } }, - { testId, atVersionId, browserVersionId }, - context - ); - - const copiedTestResultInput = copyTestResult( - testResultSkeleton, - testResult - ); - - let savedData; - if (testResult.completedAt) { - savedData = await submitTestResultResolver( - { - parentContext: { - id: copiedTestResultInput.id - } - }, - { input: copiedTestResultInput }, - context - ); - } else { - savedData = await saveTestResultResolver( - { - 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/20240312122457-testPlanReportAtVersions.js b/server/migrations/20240312122457-testPlanReportAtVersions.js new file mode 100644 index 000000000..0ad3c78a7 --- /dev/null +++ b/server/migrations/20240312122457-testPlanReportAtVersions.js @@ -0,0 +1,72 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'TestPlanReport', + 'exactAtVersionId', + { type: Sequelize.DataTypes.INTEGER }, + { transaction } + ); + + await queryInterface.addColumn( + 'TestPlanReport', + 'minimumAtVersionId', + { type: Sequelize.DataTypes.INTEGER }, + { transaction } + ); + + const atVersions = await queryInterface.sequelize.query( + ` + SELECT + id, + "atId", + "releasedAt" + FROM + "AtVersion" + ORDER BY + "releasedAt" ASC + `, + { type: Sequelize.QueryTypes.SELECT, transaction } + ); + + const oldAtVersions = {}; + atVersions.forEach(atVersion => { + if (!oldAtVersions[atVersion.atId]) { + oldAtVersions[atVersion.atId] = atVersion.id; + } + }); + + for (const [atId, atVersionId] of Object.entries(oldAtVersions)) { + await queryInterface.sequelize.query( + ` + UPDATE + "TestPlanReport" + SET + "minimumAtVersionId" = ? + WHERE + "atId" = ? + `, + { replacements: [atVersionId, atId], transaction } + ); + } + }); + }, + + async down(queryInterface /* , Sequelize */) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn( + 'TestPlanReport', + 'exactAtVersionId', + { transaction } + ); + await queryInterface.removeColumn( + 'TestPlanReport', + 'minimumAtVersionId', + { transaction } + ); + }); + } +}; diff --git a/server/migrations/20240313145124-addIsPrimaryColumnToTestPlanRun.js b/server/migrations/20240313145124-addIsPrimaryColumnToTestPlanRun.js new file mode 100644 index 000000000..bf9e86dfc --- /dev/null +++ b/server/migrations/20240313145124-addIsPrimaryColumnToTestPlanRun.js @@ -0,0 +1,27 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.addColumn( + 'TestPlanRun', + 'isPrimary', + { + type: Sequelize.DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false + }, + { transaction } + ); + }); + }, + + async down(queryInterface) { + return queryInterface.sequelize.transaction(async transaction => { + await queryInterface.removeColumn('TestPlanRun', 'isPrimary', { + transaction + }); + }); + } +}; diff --git a/server/migrations/20240319195950-updateMetrics.js b/server/migrations/20240516195950-updateMetrics.js similarity index 100% rename from server/migrations/20240319195950-updateMetrics.js rename to server/migrations/20240516195950-updateMetrics.js diff --git a/server/models/TestPlanReport.js b/server/models/TestPlanReport.js index 504355cb0..c23d1b538 100644 --- a/server/models/TestPlanReport.js +++ b/server/models/TestPlanReport.js @@ -14,6 +14,14 @@ module.exports = function (sequelize, DataTypes) { testPlanId: { type: DataTypes.INTEGER }, atId: { type: DataTypes.INTEGER }, browserId: { type: DataTypes.INTEGER }, + exactAtVersionId: { + type: DataTypes.INTEGER, + allowNull: true + }, + minimumAtVersionId: { + type: DataTypes.INTEGER, + allowNull: true + }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW diff --git a/server/models/TestPlanRun.js b/server/models/TestPlanRun.js index 88b45f12a..24ab17fef 100644 --- a/server/models/TestPlanRun.js +++ b/server/models/TestPlanRun.js @@ -17,6 +17,11 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false + }, + isPrimary: { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false } }, { diff --git a/server/models/loaders/AtLoader.js b/server/models/loaders/AtLoader.js index 06ea3e671..f0a1b65a9 100644 --- a/server/models/loaders/AtLoader.js +++ b/server/models/loaders/AtLoader.js @@ -16,7 +16,7 @@ const AtLoader = () => { activePromise = getAts({ transaction }).then(ats => { // Sort date of atVersions subarray in desc order by releasedAt date - ats.forEach(item => + ats.sort((a, b) => a.name.localeCompare(b.name)).forEach(item => item.atVersions.sort((a, b) => b.releasedAt - a.releasedAt) ); diff --git a/server/models/loaders/BrowserLoader.js b/server/models/loaders/BrowserLoader.js index 59e38fbce..db04d8a4b 100644 --- a/server/models/loaders/BrowserLoader.js +++ b/server/models/loaders/BrowserLoader.js @@ -15,15 +15,17 @@ const BrowserLoader = () => { } activePromise = getBrowsers({ transaction }).then(browsers => { - browsers = browsers.map(browser => ({ - ...browser.dataValues, - candidateAts: browser.ats.filter( - at => at.AtBrowsers.isCandidate - ), - recommendedAts: browser.ats.filter( - at => at.AtBrowsers.isRecommended - ) - })); + browsers = browsers + .sort((a, b) => a.name.localeCompare(b.name)) + .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/AtService.js b/server/models/services/AtService.js index 27508ad7f..bc96c7f2e 100644 --- a/server/models/services/AtService.js +++ b/server/models/services/AtService.js @@ -390,6 +390,14 @@ const removeAtVersionById = async ({ id, truncate = false, transaction }) => { }); }; +/** + * Returns all the unique AT Versions used when collecting results from testers + * for a Test Plan Report + * @param {number} testPlanReportId - id of the test plan report + * @param {object} options + * @param {*} options.transaction - Sequelize transaction + * @returns {Promise<*>} + */ const getUniqueAtVersionsForReport = async ( testPlanReportId, { transaction } diff --git a/server/models/services/TestPlanReportService.js b/server/models/services/TestPlanReportService.js index 8fd9eee90..61cef19d6 100644 --- a/server/models/services/TestPlanReportService.js +++ b/server/models/services/TestPlanReportService.js @@ -215,7 +215,13 @@ const getTestPlanReports = async ({ * @returns {Promise<*>} */ const createTestPlanReport = async ({ - values: { testPlanVersionId, atId, browserId }, + values: { + testPlanVersionId, + atId, + exactAtVersionId, + minimumAtVersionId, + browserId + }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -234,6 +240,8 @@ const createTestPlanReport = async ({ testPlanVersionId, atId, browserId, + exactAtVersionId, + minimumAtVersionId, testPlanId: testPlanVersion.testPlanId }, transaction @@ -272,7 +280,14 @@ const createTestPlanReport = async ({ */ const updateTestPlanReportById = async ({ id, - values: { metrics, testPlanVersionId, vendorReviewStatus, markedFinalAt }, + values: { + metrics, + testPlanVersionId, + vendorReviewStatus, + minimumAtVersionId, + exactAtVersionId, + markedFinalAt + }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -288,6 +303,8 @@ const updateTestPlanReportById = async ({ metrics, testPlanVersionId, vendorReviewStatus, + minimumAtVersionId, + exactAtVersionId, markedFinalAt }, transaction @@ -341,7 +358,13 @@ const removeTestPlanReportById = async ({ * @returns {Promise<[*, [*]]>} */ const getOrCreateTestPlanReport = async ({ - where: { testPlanVersionId, atId, browserId }, + where: { + testPlanVersionId, + atId, + exactAtVersionId, + minimumAtVersionId, + browserId + }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -356,7 +379,13 @@ const getOrCreateTestPlanReport = async ({ { get: getTestPlanReports, create: createTestPlanReport, - values: { testPlanVersionId, atId, browserId }, + values: { + testPlanVersionId, + atId, + ...(minimumAtVersionId ? { minimumAtVersionId } : {}), + ...(exactAtVersionId ? { exactAtVersionId } : {}), + browserId + }, returnAttributes: { testPlanReportAttributes, testPlanRunAttributes: [], diff --git a/server/models/services/TestPlanRunService.js b/server/models/services/TestPlanRunService.js index 6cb0666ea..43271d7c1 100644 --- a/server/models/services/TestPlanRunService.js +++ b/server/models/services/TestPlanRunService.js @@ -316,7 +316,7 @@ const createTestPlanRun = async ({ */ const updateTestPlanRunById = async ({ id, - values: { testerUserId, testResults }, + values: { testerUserId, testResults, isPrimary }, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, nestedTestPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, @@ -329,7 +329,7 @@ const updateTestPlanRunById = async ({ }) => { await ModelService.update(TestPlanRun, { where: { id }, - values: { testResults, testerUserId }, + values: { testResults, testerUserId, isPrimary }, transaction }); return ModelService.getById(TestPlanRun, { diff --git a/server/models/services/TestResultReadService.js b/server/models/services/TestResultReadService.js index 96fefb1b0..a834136f9 100644 --- a/server/models/services/TestResultReadService.js +++ b/server/models/services/TestResultReadService.js @@ -18,7 +18,7 @@ const getTestResults = async ({ testPlanRun, context }) => { const { atLoader, browserLoader, transaction } = context; const { testPlanReport } = testPlanRun; - const tests = getTests(testPlanReport); + const tests = getTests(testPlanRun); const ats = await atLoader.getAll({ transaction }); const browsers = await browserLoader.getAll({ transaction }); diff --git a/server/models/services/TestsService.js b/server/models/services/TestsService.js index 03fa55187..58e839357 100644 --- a/server/models/services/TestsService.js +++ b/server/models/services/TestsService.js @@ -5,12 +5,20 @@ const { } = require('../../resolvers/helpers/retrieveCommands'); const getTests = parentRecord => { - const isTestPlanVersion = !!parentRecord.tests; - const testPlanReport = isTestPlanVersion ? null : parentRecord; - const testPlanVersion = isTestPlanVersion - ? parentRecord - : testPlanReport.testPlanVersion; + let testPlanVersion; + let testPlanReport; + if (parentRecord.tests) { + testPlanVersion = parentRecord; + } else if (parentRecord.testPlanRuns) { + testPlanReport = parentRecord; + testPlanVersion = parentRecord.testPlanVersion; + } else if (parentRecord.testResults) { + testPlanReport = parentRecord.testPlanReport; + testPlanVersion = parentRecord.testPlanReport.testPlanVersion; + } + const inferredAtId = testPlanReport?.atId; + const isV2 = testPlanVersion.metadata?.testFormatVersion === 2; // Populate nested At and Command fields diff --git a/server/resolvers/TestPlanReport/browserResolver.js b/server/resolvers/TestPlanReport/browserResolver.js index b5c28a009..ce0192288 100644 --- a/server/resolvers/TestPlanReport/browserResolver.js +++ b/server/resolvers/TestPlanReport/browserResolver.js @@ -3,7 +3,7 @@ const browserResolver = async (testPlanReport, _, context) => { const browsers = await browserLoader.getAll({ transaction }); - return browsers.find(browser => browser.id === testPlanReport.browser.id); + return browsers.find(browser => browser.id === testPlanReport.browserId); }; module.exports = browserResolver; diff --git a/server/resolvers/TestPlanReport/exactAtVersionResolver.js b/server/resolvers/TestPlanReport/exactAtVersionResolver.js new file mode 100644 index 000000000..a3e156abf --- /dev/null +++ b/server/resolvers/TestPlanReport/exactAtVersionResolver.js @@ -0,0 +1,17 @@ +const exactAtVersionResolver = async (testPlanReport, _, context) => { + if (!testPlanReport.exactAtVersionId) return null; + + const { transaction, atLoader } = context; + + const ats = await atLoader.getAll({ transaction }); + + const at = ats.find(at => at.id === testPlanReport.atId); + + const atVersion = at.atVersions.find( + atVersion => atVersion.id === testPlanReport.exactAtVersionId + ); + + return atVersion; +}; + +module.exports = exactAtVersionResolver; diff --git a/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js b/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js index fae66da6c..7134f56fc 100644 --- a/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js +++ b/server/resolvers/TestPlanReport/finalizedTestResultsResolver.js @@ -5,8 +5,10 @@ const finalizedTestResultsResolver = async (testPlanReport, _, context) => { return null; } - // Since conflicts are now resolved, all testPlanRuns are interchangeable. - const testPlanRun = testPlanReport.testPlanRuns[0]; + // Return the primary test plan run, otherwise pick the first TestPlanRun found. + const testPlanRun = + testPlanReport.testPlanRuns.find(({ isPrimary }) => isPrimary) || + testPlanReport.testPlanRuns[0]; return testResultsResolver( { diff --git a/server/resolvers/TestPlanReport/index.js b/server/resolvers/TestPlanReport/index.js index 7f714a3bf..decd92ec6 100644 --- a/server/resolvers/TestPlanReport/index.js +++ b/server/resolvers/TestPlanReport/index.js @@ -9,7 +9,10 @@ const atVersions = require('./atVersionsResolver'); const at = require('./atResolver'); const browser = require('./browserResolver'); const latestAtVersionReleasedAt = require('./latestAtVersionReleasedAtResolver'); +const recommendedAtVersion = require('./recommendedAtVersionResolver'); const isFinal = require('./isFinalResolver'); +const exactAtVersion = require('./exactAtVersionResolver'); +const minimumAtVersion = require('./minimumAtVersionResolver'); module.exports = { runnableTests, @@ -21,7 +24,10 @@ module.exports = { issues, atVersions, at, + exactAtVersion, + minimumAtVersion, browser, latestAtVersionReleasedAt, + recommendedAtVersion, isFinal }; diff --git a/server/resolvers/TestPlanReport/minimumAtVersionResolver.js b/server/resolvers/TestPlanReport/minimumAtVersionResolver.js new file mode 100644 index 000000000..c8fe6a317 --- /dev/null +++ b/server/resolvers/TestPlanReport/minimumAtVersionResolver.js @@ -0,0 +1,17 @@ +const minimumAtVersionResolver = async (testPlanReport, _, context) => { + if (!testPlanReport.minimumAtVersionId) return null; + + const { transaction, atLoader } = context; + + const ats = await atLoader.getAll({ transaction }); + + const at = ats.find(at => at.id === testPlanReport.atId); + + const atVersion = at.atVersions.find( + atVersion => atVersion.id === testPlanReport.minimumAtVersionId + ); + + return atVersion; +}; + +module.exports = minimumAtVersionResolver; diff --git a/server/resolvers/TestPlanReport/recommendedAtVersionResolver.js b/server/resolvers/TestPlanReport/recommendedAtVersionResolver.js new file mode 100644 index 000000000..52197eaa2 --- /dev/null +++ b/server/resolvers/TestPlanReport/recommendedAtVersionResolver.js @@ -0,0 +1,45 @@ +const { getAtVersionById } = require('../../models/services/AtService'); +const { + getTestPlanVersionById +} = require('../../models/services/TestPlanVersionService'); +const earliestAtVersionResolver = require('../TestPlanVersion/earliestAtVersionResolver'); + +const recommendedAtVersionResolver = async (testPlanReport, _, context) => { + const { transaction } = context; + + let testPlanVersion; + if (testPlanReport.testPlanVersion) { + testPlanVersion = testPlanReport.testPlanVersion; + } else { + testPlanVersion = await getTestPlanVersionById({ + id: testPlanReport.testPlanVersionId, + testPlanVersionAttributes: ['id', 'phase'], + testPlanReportAttributes: [], + testPlanRunAttributes: [], + atAttributes: [], + browserAttributes: [], + userAttributes: [], + transaction + }); + } + const phase = testPlanVersion.phase; + + if (!testPlanReport.markedFinalAt || phase !== 'RECOMMENDED') return null; + + // If report was created with exact version being required, display that + if (testPlanReport.exactAtVersionId) { + return getAtVersionById({ + id: testPlanReport.exactAtVersionId, + transaction + }); + } + + // Otherwise return the earliest At version used to record results + return earliestAtVersionResolver( + testPlanVersion, + { atId: testPlanReport.atId }, + context + ); +}; + +module.exports = recommendedAtVersionResolver; diff --git a/server/resolvers/TestPlanReportOperations/index.js b/server/resolvers/TestPlanReportOperations/index.js index bb33b0145..e01ff7579 100644 --- a/server/resolvers/TestPlanReportOperations/index.js +++ b/server/resolvers/TestPlanReportOperations/index.js @@ -4,7 +4,6 @@ const markAsFinal = require('./markAsFinalResolver'); const unmarkAsFinal = require('./unmarkAsFinalResolver'); const deleteTestPlanReport = require('./deleteTestPlanReportResolver'); const promoteVendorReviewStatus = require('./promoteVendorReviewStatusResolver'); -const updateTestPlanReportTestPlanVersion = require('./updateTestPlanReportTestPlanVersionResolver'); module.exports = { assignTester, @@ -12,6 +11,5 @@ module.exports = { markAsFinal, unmarkAsFinal, deleteTestPlanReport, - promoteVendorReviewStatus, - updateTestPlanReportTestPlanVersion + promoteVendorReviewStatus }; diff --git a/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js b/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js index 2f26efbc9..b16baf6e7 100644 --- a/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js +++ b/server/resolvers/TestPlanReportOperations/markAsFinalResolver.js @@ -3,13 +3,16 @@ const { getTestPlanReportById, updateTestPlanReportById } = require('../../models/services/TestPlanReportService'); +const { + updateTestPlanRunById +} = require('../../models/services/TestPlanRunService'); const runnableTestsResolver = require('../TestPlanReport/runnableTestsResolver'); const populateData = require('../../services/PopulatedData/populateData'); const conflictsResolver = require('../TestPlanReport/conflictsResolver'); const markAsFinalResolver = async ( { parentContext: { id: testPlanReportId } }, - _, + { primaryTestPlanRunId }, context ) => { const { user, transaction } = context; @@ -44,6 +47,31 @@ const markAsFinalResolver = async ( ); } + // Clear any other isPrimary status for attached testPlanRuns + for (const testPlanRun of testPlanReport.testPlanRuns) { + const { id } = testPlanRun; + + await updateTestPlanRunById({ + id, + values: { isPrimary: false }, + transaction + }); + } + + if (primaryTestPlanRunId) { + const primaryTestPlanRunIdExists = testPlanReport.testPlanRuns.find( + ({ id }) => id === Number(primaryTestPlanRunId) + ); + + if (primaryTestPlanRunIdExists) { + await updateTestPlanRunById({ + id: primaryTestPlanRunId, + values: { isPrimary: true }, + transaction + }); + } + } + await updateTestPlanReportById({ id: testPlanReportId, values: { markedFinalAt: new Date() }, diff --git a/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js b/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js deleted file mode 100644 index 8b17b3d65..000000000 --- a/server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js +++ /dev/null @@ -1,341 +0,0 @@ -const hash = require('object-hash'); -const { omit } = require('lodash'); -const { AuthenticationError } = require('apollo-server-express'); -const { - getTestPlanReportById, - getOrCreateTestPlanReport, - updateTestPlanReportById -} = require('../../models/services/TestPlanReportService'); -const { - getTestPlanVersionById -} = require('../../models/services/TestPlanVersionService'); -const populateData = require('../../services/PopulatedData/populateData'); -const scenariosResolver = require('../Test/scenariosResolver'); -const { - createTestPlanRun -} = require('../../models/services/TestPlanRunService'); -const submitTestResultResolver = require('../TestResultOperations/submitTestResultResolver'); -const saveTestResultResolver = require('../TestResultOperations/saveTestResultResolver'); -const testResultsResolver = require('../TestPlanRun/testResultsResolver'); -const findOrCreateTestResultResolver = require('../TestPlanRunOperations/findOrCreateTestResultResolver'); - -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 - }; - } - ), - unexpectedBehaviors: scenarioResult.unexpectedBehaviors - }; - } - ) - }; -}; - -const updateTestPlanReportTestPlanVersionResolver = async ( - { parentContext: { id: testPlanReportId } }, - { input }, // { testPlanVersionId, atId, browserId } - context -) => { - const { user, transaction } = 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({ id: newTestPlanVersionId, transaction }) - ).toJSON(); - const newTestPlanVersion = { - id: newTestPlanVersionData.id, - tests: newTestPlanVersionData.tests.map( - ({ assertions, atIds, id, scenarios, title }) => { - return { - id, - title, - ats: atIds.map(atId => ({ - id: atId - })), - scenarios: scenariosResolver( - { scenarios }, - { atId }, - context - ).map(({ commandIds }) => { - return { - commands: commandIds.map(commandId => ({ - text: commandId - })) - }; - }), - assertions: assertions.map(({ priority, text }) => ({ - priority, - text - })) - }; - } - ) - }; - - const currentTestPlanReport = ( - await getTestPlanReportById({ id: testPlanReportId, transaction }) - ).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 testResultsResolver( - 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 - })), - scenarios: scenariosResolver( - { scenarios: test.scenarios }, - { atId }, - context - ).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( - ({ passed }) => ({ - passed - }) - ), - 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({ where: input, transaction }); - - const candidatePhaseReachedAt = - currentTestPlanReport.candidatePhaseReachedAt; - const recommendedPhaseReachedAt = - currentTestPlanReport.recommendedPhaseReachedAt; - const recommendedPhaseTargetDate = - currentTestPlanReport.recommendedPhaseTargetDate; - const vendorReviewStatus = currentTestPlanReport.vendorReviewStatus; - - await updateTestPlanReportById({ - id: foundOrCreatedTestPlanReport.id, - values: { - candidatePhaseReachedAt, - recommendedPhaseReachedAt, - recommendedPhaseTargetDate, - vendorReviewStatus - }, - transaction - }); - - 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({ - values: { - testPlanReportId: foundOrCreatedTestPlanReport.id, - testerUserId: testPlanRun.tester.id - }, - transaction - }); - - 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 findOrCreateTestResultResolver( - { parentContext: { id: testPlanRunId } }, - { testId, atVersionId, browserVersionId }, - context - ); - - const copiedTestResultInput = copyTestResult( - testResultSkeleton, - testResult - ); - - let savedData; - if (testResult.completedAt) { - savedData = await submitTestResultResolver( - { parentContext: { id: copiedTestResultInput.id } }, - { input: copiedTestResultInput }, - context - ); - } else { - savedData = await saveTestResultResolver( - { 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 }); -}; - -module.exports = updateTestPlanReportTestPlanVersionResolver; diff --git a/server/resolvers/TestPlanVersion/earliestAtVersionResolver.js b/server/resolvers/TestPlanVersion/earliestAtVersionResolver.js new file mode 100644 index 000000000..eb9ca5358 --- /dev/null +++ b/server/resolvers/TestPlanVersion/earliestAtVersionResolver.js @@ -0,0 +1,71 @@ +const { getAts, getAtVersionById } = require('../../models/services/AtService'); +const { + getTestPlanReports +} = require('../../models/services/TestPlanReportService'); + +const earliestAtVersionResolver = async ( + testPlanVersion, + { atId }, + context +) => { + const { transaction } = context; + + let reports = []; + if (testPlanVersion.testPlanReports) { + reports = [...testPlanVersion.testPlanReports]; + } else { + reports = await getTestPlanReports({ + where: { testPlanVersionId: testPlanVersion.id }, + transaction + }); + } + + if (!reports.length || testPlanVersion.phase !== 'RECOMMENDED') { + return null; + } + + // To track the required reports for RECOMMENDED phase + const ats = await getAts({ transaction }); + + let earliestAtVersion = null; + for (const testPlanReport of reports.filter( + testPlanReport => + testPlanReport.atId == atId && testPlanReport.markedFinalAt + )) { + const browserId = testPlanReport.browserId; + + // Need to check if is required report for a primary test plan run + const isRequiredReport = ats + .find(at => at.id == atId) + .browsers.find(browser => browser.id == browserId) + .AtBrowsers.isRecommended; + + if (isRequiredReport) { + const primaryRun = + testPlanReport.testPlanRuns.find( + ({ isPrimary }) => isPrimary + ) || testPlanReport.testPlanRuns[0]; + const primaryRunAtVersionId = primaryRun.testResults[0].atVersionId; + const atVersion = await getAtVersionById({ + id: primaryRunAtVersionId, + transaction + }); + + if ( + !earliestAtVersion || + new Date(atVersion.releasedAt) < + new Date(earliestAtVersion.releasedAt) + ) { + earliestAtVersion = { + id: atVersion.id, + name: atVersion.name, + releasedAt: atVersion.releasedAt + }; + } + } + } + + return earliestAtVersion; +}; + +module.exports = earliestAtVersionResolver; diff --git a/server/resolvers/TestPlanVersion/index.js b/server/resolvers/TestPlanVersion/index.js index d281e24c0..a8a55b0ff 100644 --- a/server/resolvers/TestPlanVersion/index.js +++ b/server/resolvers/TestPlanVersion/index.js @@ -3,13 +3,17 @@ const gitMessage = require('./gitMessageResolver'); const tests = require('./testsResolver'); const testPlanReports = require('./testPlanReportsResolver'); const recommendedPhaseTargetDate = require('./recommendedPhaseTargetDateResolver'); +const testPlanReportStatuses = require('./testPlanReportStatusesResolver'); +const earliestAtVersion = require('./earliestAtVersionResolver'); const TestPlanVersion = { testPlan, gitMessage, tests, + recommendedPhaseTargetDate, testPlanReports, - recommendedPhaseTargetDate + testPlanReportStatuses, + earliestAtVersion }; module.exports = TestPlanVersion; diff --git a/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.js b/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.js new file mode 100644 index 000000000..d07154139 --- /dev/null +++ b/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.js @@ -0,0 +1,191 @@ +const atResolver = require('../TestPlanReport/atResolver'); +const browserResolver = require('../TestPlanReport/browserResolver'); +const exactAtVersionResolver = require('../TestPlanReport/exactAtVersionResolver'); +const minimumAtVersionResolver = require('../TestPlanReport/minimumAtVersionResolver'); + +const testPlanReportStatusesResolver = async (testPlanVersion, _, context) => { + const { transaction, atLoader } = context; + + const ats = await atLoader.getAll({ transaction }); + + const { testPlanReports, phase } = testPlanVersion; + + const indexedTestPlanReports = await indexTestPlanReports( + testPlanReports ?? [], + context + ); + + const populateTestPlanVersion = testPlanReport => { + return { ...testPlanReport, testPlanVersion }; + }; + + const unsortedStatuses = []; + + for (const at of ats) { + for (const browser of at.browsers) { + let isRequiredAtBrowser = false; + if (phase === 'DRAFT' || phase === 'CANDIDATE') { + isRequiredAtBrowser = at.candidateBrowsers.some( + candidateBrowser => candidateBrowser.id === browser.id + ); + } else if (phase === 'RECOMMENDED') { + isRequiredAtBrowser = at.recommendedBrowsers.some( + recommendedBrowser => recommendedBrowser.id === browser.id + ); + } + + const atVersions = at.atVersions.sort((a, b) => { + return new Date(a.releasedAt) - new Date(b.releasedAt); + }); + + const hasNoReports = + Object.keys(indexedTestPlanReports?.[at.id]?.[browser.id] ?? {}) + .length === 0; + + if (hasNoReports) { + if (phase !== 'RECOMMENDED') { + const earliestAtVersion = at.atVersions[0]; + + unsortedStatuses.push({ + isRequired: isRequiredAtBrowser, + at, + browser, + minimumAtVersion: earliestAtVersion, + exactAtVersion: null, + testPlanReport: null + }); + + continue; + } + + const latestAtVersion = atVersions[atVersions.length - 1]; + + unsortedStatuses.push({ + isRequired: isRequiredAtBrowser, + at, + browser, + exactAtVersion: latestAtVersion, + testPlanReport: null + }); + + continue; + } + + let isFirstAtBrowserInstance = true; + let minimumAtVersionFound = false; + + Object.values(indexedTestPlanReports[at.id][browser.id]).forEach( + testPlanReports => { + testPlanReports.forEach(testPlanReport => { + let isRequired = false; + if (isRequiredAtBrowser && isFirstAtBrowserInstance) { + isFirstAtBrowserInstance = false; + isRequired = true; + } + + if (testPlanReport.minimumAtVersion) { + minimumAtVersionFound = true; + } + + unsortedStatuses.push({ + isRequired, + at, + browser, + minimumAtVersion: testPlanReport.minimumAtVersion, + exactAtVersion: testPlanReport.exactAtVersion, + testPlanReport: + populateTestPlanVersion(testPlanReport) + }); + }); + } + ); + + const latestAtVersion = atVersions[atVersions.length - 1]; + + if ( + !minimumAtVersionFound && + !indexedTestPlanReports[at.id][browser.id][latestAtVersion.id] + ) { + unsortedStatuses.push({ + isRequired: false, + at, + browser, + minimumAtVersion: null, + exactAtVersion: latestAtVersion, + testPlanReport: null + }); + } + } + } + + const statuses = unsortedStatuses.sort((a, b) => { + if (a.at.name !== b.at.name) return a.at.name.localeCompare(b.at.name); + if (a.browser.name !== b.browser.name) { + return a.browser.name.localeCompare(b.browser.name); + } + if (a.isRequired !== b.isRequired) return a.isRequired ? -1 : 1; + const dateA = (a.minimumAtVersion ?? a.exactAtVersion).releasedAt; + const dateB = (b.minimumAtVersion ?? b.exactAtVersion).releasedAt; + return new Date(dateA) - new Date(dateB); + }); + + return statuses; +}; + +const indexTestPlanReports = async (unpopulatedTestPlanReports, context) => { + const unsortedTestPlanReports = await Promise.all( + unpopulatedTestPlanReports.map(async report => { + const [at, browser, minimumAtVersion, exactAtVersion] = + await Promise.all([ + atResolver(report, null, context), + browserResolver(report, null, context), + minimumAtVersionResolver(report, null, context), + exactAtVersionResolver(report, null, context) + ]); + + return { + ...report.dataValues, + at, + browser, + minimumAtVersion, + exactAtVersion + }; + }) + ); + + const releaseOrderTestPlanReports = unsortedTestPlanReports.sort((a, b) => { + const dateA = (a.minimumAtVersion ?? a.exactAtVersion).releasedAt; + const dateB = (b.minimumAtVersion ?? b.exactAtVersion).releasedAt; + if (dateA !== dateB) return new Date(dateB) - new Date(dateA); + + // With duplicate reports list the oldest first + return new Date(a.createdAt) - new Date(b.createdAt); + }); + + const indexedTestPlanReports = {}; + + releaseOrderTestPlanReports.forEach(testPlanReport => { + const { at, browser, minimumAtVersion, exactAtVersion } = + testPlanReport; + + if (!indexedTestPlanReports[at.id]) indexedTestPlanReports[at.id] = {}; + if (!indexedTestPlanReports[at.id][browser.id]) { + indexedTestPlanReports[at.id][browser.id] = {}; + } + + const atVersion = minimumAtVersion ?? exactAtVersion; + + if (!indexedTestPlanReports[at.id][browser.id][atVersion.id]) { + // Must be an array to accommodate duplicates, which are allowed + indexedTestPlanReports[at.id][browser.id][atVersion.id] = []; + } + + indexedTestPlanReports[at.id][browser.id][atVersion.id].push( + testPlanReport + ); + }); + + return indexedTestPlanReports; +}; + +module.exports = testPlanReportStatusesResolver; diff --git a/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.test.js b/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.test.js new file mode 100644 index 000000000..8d3c9a4b2 --- /dev/null +++ b/server/resolvers/TestPlanVersion/testPlanReportStatusesResolver.test.js @@ -0,0 +1,272 @@ +const { uniq: unique } = require('lodash'); +const getGraphQLContext = require('../../graphql-context'); +const testPlanReportStatusesResolver = require('./testPlanReportStatusesResolver'); +const db = require('../../models'); +const dbCleaner = require('../../tests/util/db-cleaner'); +const { createAtVersion } = require('../../models/services/AtService'); + +const getDate = dayAdjustment => { + return new Date( + Number(new Date()) + dayAdjustment * 1000 * 60 * 60 * 24 + ).toISOString(); +}; + +describe('testPlanReportStatusesResolver', () => { + const getFakeTestPlanReport = values => { + const atVersionId = + values.exactAtVersionId ?? values.minimumAtVersionId; + + const fake = { + ...values, + testPlanRuns: [{ isPrimary: true, testResults: [{ atVersionId }] }] + }; + + fake.dataValues = fake; + + return fake; + }; + + let context; + + beforeEach(() => { + context = getGraphQLContext({ req: { transaction: false } }); + }); + + afterAll(async () => { + // Closing the DB connection allows Jest to exit successfully. + await db.sequelize.close(); + }); + + it('uses minimumAtVersions when no testPlanReports exist', async () => { + const testPlanVersionWithoutReports = { + phase: 'DRAFT', + testPlanReports: [] + }; + + const statuses = await testPlanReportStatusesResolver( + testPlanVersionWithoutReports, + null, + context + ); + + const atsInOrder = unique(statuses.map(status => status.at.name)); + const jawsBrowsersInOrder = unique( + statuses + .filter(status => status.at.id === 1) + .map(status => status.browser.name) + ); + const voiceoverBrowsersInOrder = unique( + statuses + .filter(status => status.at.id === 3) + .map(status => status.browser.name) + ); + const exactAtVersions = statuses.filter( + status => status.exactAtVersion + ); + const requiredStatuses = statuses.filter(status => status.isRequired); + + expect(statuses.length).toBe(7); + expect(atsInOrder).toEqual(['JAWS', 'NVDA', 'VoiceOver for macOS']); + expect(jawsBrowsersInOrder).toEqual(['Chrome', 'Firefox']); + expect(voiceoverBrowsersInOrder).toEqual([ + 'Chrome', + 'Firefox', + 'Safari' + ]); + expect(exactAtVersions).toEqual([]); + expect(requiredStatuses.length).toBe(3); + }); + + it('matches up testPlanReports when they exist', async () => { + const testPlanVersion = { + phase: 'DRAFT', + testPlanReports: [ + getFakeTestPlanReport({ + atId: 1, + browserId: 2, + minimumAtVersionId: 1 + }) + ] + }; + + const statuses = await testPlanReportStatusesResolver( + testPlanVersion, + null, + context + ); + + const statusesWithReport = statuses.filter( + status => status.testPlanReport + ); + + expect(statuses.length).toBe(7); + expect(statusesWithReport.length).toBe(1); + expect(statusesWithReport[0].isRequired).toBe(true); + expect(statusesWithReport[0].at.name).toBe('JAWS'); + expect(statusesWithReport[0].browser.name).toBe('Chrome'); + expect(statusesWithReport[0].minimumAtVersion.id).toBe(1); + }); + + it('supports duplicate at / browser / atVersions', async () => { + const testPlanVersion = { + phase: 'DRAFT', + testPlanReports: [ + getFakeTestPlanReport({ + atId: 1, + browserId: 2, + minimumAtVersionId: 1 + }), + getFakeTestPlanReport({ + atId: 1, + browserId: 2, + minimumAtVersionId: 1 + }) + ] + }; + + const statuses = await testPlanReportStatusesResolver( + testPlanVersion, + null, + context + ); + + const statusesWithReport = statuses.filter( + status => status.testPlanReport + ); + + expect(statuses.length).toBe(8); + expect(statusesWithReport.length).toBe(2); + expect(statusesWithReport[0].isRequired).toBe(true); + expect(statusesWithReport[0].at.name).toBe('JAWS'); + expect(statusesWithReport[0].browser.name).toBe('Chrome'); + expect(statusesWithReport[0].minimumAtVersion.id).toBe(1); + expect(statusesWithReport[1].isRequired).toBe(false); + expect(statusesWithReport[1].at.name).toBe('JAWS'); + expect(statusesWithReport[1].browser.name).toBe('Chrome'); + expect(statusesWithReport[1].minimumAtVersion.id).toBe(1); + }); + + it('marks the latest version as missing with exact AT versions', async () => { + await dbCleaner(async transaction => { + const context = getGraphQLContext({ req: { transaction } }); + + await createAtVersion({ + values: { atId: 1, name: 'v2', releasedAt: getDate(-20) }, + transaction + }); + + const v3 = await createAtVersion({ + values: { atId: 1, name: 'v3', releasedAt: getDate(-15) }, + transaction + }); + + await createAtVersion({ + values: { atId: 1, name: 'v4', releasedAt: getDate(-10) }, + transaction + }); + + await createAtVersion({ + values: { atId: 1, name: 'v5', releasedAt: getDate(-5) }, + transaction + }); + + const testPlanVersion = { + phase: 'RECOMMENDED', + testPlanReports: [ + getFakeTestPlanReport({ + atId: 1, + browserId: 2, + exactAtVersionId: v3.id + }) + ] + }; + + const statuses = await testPlanReportStatusesResolver( + testPlanVersion, + null, + context + ); + + const jawsChromeStatuses = statuses.filter( + status => + status.at.name === 'JAWS' && + status.browser.name === 'Chrome' + ); + + // Should be undefined since it's older than the oldest report + const v2Status = jawsChromeStatuses.find( + status => status.exactAtVersion.name == 'v2' + ); + expect(v2Status).toBe(undefined); + + // Should be defined since there is a report + expect(jawsChromeStatuses[0].exactAtVersion.name).toBe('v3'); + expect(jawsChromeStatuses[0].isRequired).toBe(true); + expect(jawsChromeStatuses[0].testPlanReport).toBeTruthy(); + + // Should be undefined since there is no report and it's not the latest + const v4Status = jawsChromeStatuses.find( + status => status.exactAtVersion.name == 'v4' + ); + expect(v4Status).toBe(undefined); + + // Should be defined since it's the latest AT version + expect(jawsChromeStatuses[1].exactAtVersion.name).toBe('v5'); + expect(jawsChromeStatuses[1].isRequired).toBe(false); + expect(jawsChromeStatuses[1].testPlanReport).toBe(null); + + expect(jawsChromeStatuses.length).toBe(2); + }); + }); + + it('ignores latest AT version when minimum AT version is set', async () => { + await dbCleaner(async transaction => { + const context = getGraphQLContext({ req: { transaction } }); + + const v2 = await createAtVersion({ + values: { atId: 1, name: 'v2', releasedAt: getDate(-20) }, + transaction + }); + + await createAtVersion({ + values: { atId: 1, name: 'v3', releasedAt: getDate(-15) }, + transaction + }); + + const testPlanVersion = { + phase: 'CANDIDATE', + testPlanReports: [ + getFakeTestPlanReport({ + atId: 1, + browserId: 2, + minimumAtVersionId: v2.id + }) + ] + }; + + const statuses = await testPlanReportStatusesResolver( + testPlanVersion, + null, + context + ); + + const jawsChromeStatuses = statuses.filter( + status => + status.at.name === 'JAWS' && + status.browser.name === 'Chrome' + ); + + expect(jawsChromeStatuses[0].minimumAtVersion.name).toBe('v2'); + expect(jawsChromeStatuses[0].isRequired).toBe(true); + expect(jawsChromeStatuses[0].testPlanReport).toBeTruthy(); + + // Should be undefined since there is a minimumAtVersion + const v3Status = jawsChromeStatuses.find( + status => status.minimumAtVersion.name == 'v3' + ); + expect(v3Status).toBe(undefined); + + expect(jawsChromeStatuses.length).toBe(1); + }); + }); +}); diff --git a/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js index 294550842..2475423b8 100644 --- a/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js +++ b/server/resolvers/TestPlanVersionOperations/updatePhaseResolver.js @@ -15,6 +15,7 @@ const { } = require('../../models/services/TestPlanVersionService'); const AtLoader = require('../../models/loaders/AtLoader'); const processCopiedReports = require('../helpers/processCopiedReports'); +const earliestAtVersionResolver = require('../TestPlanVersion/earliestAtVersionResolver'); const updatePhaseResolver = async ( { parentContext: { id: testPlanVersionId } }, @@ -337,11 +338,36 @@ const updatePhaseResolver = async ( transaction }); } - await updateTestPlanVersionById({ + + const testPlanVersion = await updateTestPlanVersionById({ id: testPlanVersionId, values: updateParams, transaction }); + + // From the point the TestPlanVersion has achieved consensus + // and reached the recommended phase, extensive data collection + // can begin and the loose requirement of a mininumAtVersion + // will no longer be allowed. + if (testPlanVersion.phase === 'RECOMMENDED') { + for (const testPlanReport of testPlanVersion.testPlanReports) { + const atVersion = await earliestAtVersionResolver( + testPlanVersion, + { atId: testPlanReport.at.id }, + context + ); + + await updateTestPlanReportById({ + id: testPlanReport.id, + values: { + minimumAtVersionId: null, + exactAtVersionId: atVersion.id + }, + transaction + }); + } + } + return populateData({ testPlanVersionId }, { context }); }; diff --git a/server/resolvers/createTestPlanReportResolver.js b/server/resolvers/createTestPlanReportResolver.js new file mode 100644 index 000000000..854ef6246 --- /dev/null +++ b/server/resolvers/createTestPlanReportResolver.js @@ -0,0 +1,56 @@ +const { AuthenticationError, UserInputError } = require('apollo-server-errors'); +const { + createTestPlanReport +} = require('../models/services/TestPlanReportService'); +const populateData = require('../services/PopulatedData/populateData'); +const processCopiedReports = require('./helpers/processCopiedReports'); + +const createTestPlanReportResolver = async (_, { input }, context) => { + const { user, transaction } = context; + + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + if (!(input.exactAtVersionId || input.minimumAtVersionId)) { + throw new UserInputError( + 'Missing either an exact AT version or a minimum AT version' + ); + } + + // Pull back report from TestPlanVersion in advanced phase and run through processCopiedReports if not deprecated + const { copyResultsFromTestPlanVersionId, ...values } = input; + + let testPlanReport; + + if (copyResultsFromTestPlanVersionId) { + const { updatedTestPlanReports } = await processCopiedReports({ + oldTestPlanVersionId: copyResultsFromTestPlanVersionId, + newTestPlanVersionId: input.testPlanVersionId, + newTestPlanReports: [], + atBrowserCombinationsToInclude: [ + { atId: input.atId, browserId: input.browserId } + ], + context + }); + + if (updatedTestPlanReports?.length) { + // Expecting only to get back the single requested combination + testPlanReport = updatedTestPlanReports[0]; + } else { + testPlanReport = await createTestPlanReport({ + values, + transaction + }); + } + } else { + testPlanReport = await createTestPlanReport({ values, transaction }); + } + + const locationOfData = { testPlanReportId: testPlanReport.id }; + const preloaded = { testPlanReport }; + + return populateData(locationOfData, { preloaded, context }); +}; + +module.exports = createTestPlanReportResolver; diff --git a/server/resolvers/findOrCreateTestPlanReportResolver.js b/server/resolvers/findOrCreateTestPlanReportResolver.js deleted file mode 100644 index a056fe21a..000000000 --- a/server/resolvers/findOrCreateTestPlanReportResolver.js +++ /dev/null @@ -1,77 +0,0 @@ -const { AuthenticationError } = require('apollo-server-errors'); -const { - getOrCreateTestPlanReport -} = require('../models/services/TestPlanReportService'); -const populateData = require('../services/PopulatedData/populateData'); -const processCopiedReports = require('./helpers/processCopiedReports'); - -const findOrCreateTestPlanReportResolver = async (_, { input }, context) => { - const { user, transaction } = context; - - if (!user?.roles.find(role => role.name === 'ADMIN')) { - throw new AuthenticationError(); - } - - // Pull back report from TestPlanVersion in advanced phase and run through processCopiedReports if not deprecated - const { copyResultsFromTestPlanReportId } = input; - - if (copyResultsFromTestPlanReportId) { - const { updatedTestPlanReports } = await processCopiedReports({ - oldTestPlanVersionId: copyResultsFromTestPlanReportId, - newTestPlanVersionId: input.testPlanVersionId, - newTestPlanReports: [], - atBrowserCombinationsToInclude: [ - { atId: input.atId, browserId: input.browserId } - ], - context - }); - - if (updatedTestPlanReports?.length) { - // Expecting only to get back the single requested combination - const [testPlanReport] = updatedTestPlanReports; - - const locationOfData = { - testPlanReportId: testPlanReport.id - }; - const preloaded = { - testPlanReport - }; - const createdLocationsOfData = [locationOfData]; - - return { - populatedData: await populateData(locationOfData, { - preloaded, - context - }), - created: await Promise.all( - createdLocationsOfData.map(createdLocationOfData => - populateData(createdLocationOfData, { - preloaded, - context - }) - ) - ) - }; - } - } - - const [testPlanReport, createdLocationsOfData] = - await getOrCreateTestPlanReport({ where: input, transaction }); - - const locationOfData = { testPlanReportId: testPlanReport.id }; - const preloaded = { testPlanReport }; - - return { - populatedData: await populateData(locationOfData, { - preloaded, - context - }), - created: await Promise.all( - createdLocationsOfData.map(createdLocationOfData => - populateData(createdLocationOfData, { preloaded, context }) - ) - ) - }; -}; - -module.exports = findOrCreateTestPlanReportResolver; diff --git a/server/resolvers/helpers/deriveAttributesFromCustomField.js b/server/resolvers/helpers/deriveAttributesFromCustomField.js index d03381e15..f08350cbf 100644 --- a/server/resolvers/helpers/deriveAttributesFromCustomField.js +++ b/server/resolvers/helpers/deriveAttributesFromCustomField.js @@ -52,6 +52,7 @@ const deriveAttributesFromCustomField = (fieldName, customFields) => { fields.includes('testPlan.directory') ) derived.push('directory'); + if (fields.includes('earliestAtVersion')) derived.push('phase'); break; } case 'testPlanReport': { @@ -60,6 +61,10 @@ const deriveAttributesFromCustomField = (fieldName, customFields) => { if (fields.includes('testPlanVersion')) derived.push('testPlanVersionId'); if (fields.includes('isFinal')) derived.push('markedFinalAt'); + if (fields.includes('recommendedAtVersion')) { + derived.push('testPlanVersionId'); + derived.push('markedFinalAt'); + } break; } case 'draftTestPlanRuns': { diff --git a/server/resolvers/helpers/processCopiedReports.js b/server/resolvers/helpers/processCopiedReports.js index 52e059648..9f2b90e20 100644 --- a/server/resolvers/helpers/processCopiedReports.js +++ b/server/resolvers/helpers/processCopiedReports.js @@ -3,8 +3,8 @@ const { } = require('../../models/services/TestPlanVersionService'); const { getTestPlanReports, - getOrCreateTestPlanReport, - updateTestPlanReportById + updateTestPlanReportById, + getOrCreateTestPlanReport } = require('../../models/services/TestPlanReportService'); const { hashTest } = require('../../util/aria'); const { @@ -342,6 +342,8 @@ const processCopiedReports = async ({ where: { testPlanVersionId: newTestPlanVersionId, atId: oldTestPlanReport.atId, + minimumAtVersionId: oldTestPlanReport.minimumAtVersionId, + exactAtVersionId: oldTestPlanReport.exactAtVersionId, browserId: oldTestPlanReport.browserId }, transaction diff --git a/server/resolvers/index.js b/server/resolvers/index.js index 5cf7dece6..fbb74542c 100644 --- a/server/resolvers/index.js +++ b/server/resolvers/index.js @@ -10,7 +10,7 @@ const testPlanVersion = require('./testPlanVersionResolver'); const testPlanVersions = require('./testPlanVersionsResolver'); const testPlanRun = require('./testPlanRunResolver'); const testPlanRuns = require('./testPlanRunsResolver'); -const findOrCreateTestPlanReport = require('./findOrCreateTestPlanReportResolver'); +const createTestPlanReport = require('./createTestPlanReportResolver'); const addViewer = require('./addViewerResolver'); const mutateAt = require('./mutateAtResolver'); const mutateAtVersion = require('./mutateAtVersionResolver'); @@ -74,7 +74,7 @@ const resolvers = { testResult: mutateTestResult, testPlanVersion: mutateTestPlanVersion, collectionJob: mutateCollectionJob, - findOrCreateTestPlanReport, + createTestPlanReport, updateMe, addViewer, updateCollectionJob, diff --git a/server/resolvers/testPlanReportsResolver.js b/server/resolvers/testPlanReportsResolver.js index 5fab775a1..e927f7d7c 100644 --- a/server/resolvers/testPlanReportsResolver.js +++ b/server/resolvers/testPlanReportsResolver.js @@ -52,6 +52,16 @@ const testPlanReportsResolver = async ( if (testPlanReportRawAttributes.includes('conflictsLength')) testPlanReportAttributes.push('metrics'); + if (testPlanReportRawAttributes.includes('minimumAtVersion')) { + testPlanReportAttributes.push('atId'); + testPlanReportAttributes.push('minimumAtVersionId'); + } + + if (testPlanReportRawAttributes.includes('exactAtVersion')) { + testPlanReportAttributes.push('atId'); + testPlanReportAttributes.push('exactAtVersionId'); + } + if (isFinal === undefined) { // Do nothing } else testPlanReportAttributes.push('markedFinalAt'); diff --git a/server/resolvers/testPlansResolver.js b/server/resolvers/testPlansResolver.js index 5d8273ff9..6dd8ee200 100644 --- a/server/resolvers/testPlansResolver.js +++ b/server/resolvers/testPlansResolver.js @@ -1,55 +1,22 @@ const { getTestPlans } = require('../models/services/TestPlanService'); -const retrieveAttributes = require('./helpers/retrieveAttributes'); -const { TEST_PLAN_VERSION_ATTRIBUTES } = require('../models/services/helpers'); -const testPlans = async (_, __, context, info) => { +const testPlans = async (_, { testPlanVersionPhases }, context) => { const { transaction } = context; - const requestedFields = - info.fieldNodes[0] && - info.fieldNodes[0].selectionSet.selections.map( - selection => selection.name.value - ); - const includeLatestTestPlanVersion = requestedFields.includes( - 'latestTestPlanVersion' - ); - - const { attributes: latestTestPlanVersionAttributes } = retrieveAttributes( - 'latestTestPlanVersion', - TEST_PLAN_VERSION_ATTRIBUTES, - info, - true - ); - - const { attributes: testPlanVersionsAttributes } = retrieveAttributes( - 'testPlanVersions', - TEST_PLAN_VERSION_ATTRIBUTES, - info, - true - ); - - const combinedTestPlanVersionAttributes = [ - ...new Set([ - ...latestTestPlanVersionAttributes, - ...testPlanVersionsAttributes - ]) - ]; - - const hasAssociations = combinedTestPlanVersionAttributes.length !== 0; - const plans = await getTestPlans({ - includeLatestTestPlanVersion, - testPlanVersionAttributes: combinedTestPlanVersionAttributes, - testPlanReportAttributes: hasAssociations ? null : [], - atAttributes: hasAssociations ? null : [], - browserAttributes: hasAssociations ? null : [], - testPlanRunAttributes: hasAssociations ? null : [], - userAttributes: hasAssociations ? null : [], + includeLatestTestPlanVersion: true, pagination: { order: [['testPlanVersions', 'updatedAt', 'DESC']] }, transaction }); + return plans.map(p => { - return { ...p.dataValues }; + return { + ...p.dataValues, + testPlanVersions: p.testPlanVersions?.filter(testPlanVersion => { + if (!testPlanVersionPhases) return true; + return testPlanVersionPhases.includes(testPlanVersion.phase); + }) + }; }); }; diff --git a/server/scripts/import-tests/index.js b/server/scripts/import-tests/index.js index a48d7e74b..f113ee816 100644 --- a/server/scripts/import-tests/index.js +++ b/server/scripts/import-tests/index.js @@ -53,10 +53,14 @@ const builtTestsDirectory = path.resolve(gitCloneDirectory, 'build', 'tests'); const testsDirectory = path.resolve(gitCloneDirectory, 'tests'); const gitRun = (args, cwd = gitCloneDirectory) => { - return spawn - .sync('git', args.split(' '), { cwd }) - .stdout.toString() - .trimEnd(); + const gitRunOutput = spawn.sync('git', args.split(' '), { cwd }); + + if (gitRunOutput.error) { + console.info(`'git ${args}' failed with error ${gitRunOutput.error}`); + process.exit(1); + } + + return gitRunOutput.stdout.toString().trimEnd(); }; const importTestPlanVersions = async transaction => { @@ -85,12 +89,23 @@ const buildTestsAndCreateTestPlanVersions = async (commit, { transaction }) => { const installOutput = spawn.sync('npm', ['install'], { cwd: gitCloneDirectory }); + + if (installOutput.error) { + console.info(`'npm install' failed with error ${installOutput.error}`); + process.exit(1); + } console.log('`npm install` output', installOutput.stdout.toString()); console.log('Running `npm run build` ...\n'); const buildOutput = spawn.sync('npm', ['run', 'build'], { cwd: gitCloneDirectory }); + + if (buildOutput.error) { + console.info(`'npm run build' failed with error ${buildOutput.error}`); + process.exit(1); + } + console.log('`npm run build` output', buildOutput.stdout.toString()); importHarness(); @@ -260,7 +275,18 @@ const cloneRepo = async () => { fse.ensureDirSync(gitCloneDirectory); console.info('Cloning aria-at repo ...'); - spawn.sync('git', ['clone', ariaAtRepo, gitCloneDirectory]); + const cloneOutput = spawn.sync('git', [ + 'clone', + ariaAtRepo, + gitCloneDirectory + ]); + + if (cloneOutput.error) { + console.info( + `git clone ${ariaAtRepo} ${gitCloneDirectory} failed with error ${cloneOutput.error}` + ); + process.exit(1); + } console.info('Cloning aria-at repo complete.'); }; diff --git a/server/scripts/populate-test-data/pg_dump_test_data.sql b/server/scripts/populate-test-data/pg_dump_test_data.sql index 5c7466100..6a1302143 100644 --- a/server/scripts/populate-test-data/pg_dump_test_data.sql +++ b/server/scripts/populate-test-data/pg_dump_test_data.sql @@ -60,25 +60,25 @@ $$; -- Data for Name: TestPlanReport; Type: TABLE DATA; Schema: public; Owner: atr -- -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId") VALUES (1, get_test_plan_version_id(text 'Toggle Button', '1'), '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', '1'), '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', '1'), '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', '1'), '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', '1'), '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)', '1'), '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', '1'), '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', '1'), '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', '1'), '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', '1'), '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', '1'), '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)', '1'), '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)', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 2, 2, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (14, get_test_plan_version_id(text 'Toggle Button', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 2, 2, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (15, get_test_plan_version_id(text 'Toggle Button', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 3, 3, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (16, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 1, 2, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (17, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 2, 2, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "browserId", "vendorReviewStatus") VALUES (18, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 3, 3, 'READY'); -INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "browserId", "vendorReviewStatus") VALUES (19, get_test_plan_version_id(text 'Modal Dialog Example', '2'), '2024-05-14 14:18:23.602-05', 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId") VALUES (1, get_test_plan_version_id(text 'Toggle Button', '1'), '2021-05-14 14:18:23.602-05', 1, 1, 2); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId") VALUES (2, get_test_plan_version_id(text 'Select Only Combobox Example', '1'), '2021-05-14 14:18:23.602-05', 2, 2, 1); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (3, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', '2022-07-06', 1, 1, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (4, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', '2022-07-06', 2, 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (5, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', '2022-07-06', 3, 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (6, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)', '1'), '2021-05-14 14:18:23.602-05', '2022-07-06', 3, 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId") VALUES (7, get_test_plan_version_id(text 'Alert Example', '1'), '2021-05-14 14:18:23.602-05', 3, 3, 1); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (8, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', 1, 1, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (9, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', 2, 2, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (10, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', 3, 3, 1, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (11, get_test_plan_version_id(text 'Modal Dialog Example', '1'), '2021-05-14 14:18:23.602-05', 3, 3, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "exactAtVersionId", "browserId", "vendorReviewStatus") VALUES (12, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)', '1'), '2021-05-14 14:18:23.602-05', '2022-07-06', 1, 1, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "exactAtVersionId", "browserId", "vendorReviewStatus") VALUES (13, get_test_plan_version_id(text 'Checkbox Example (Mixed-State)', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 2, 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (14, get_test_plan_version_id(text 'Toggle Button', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 2, 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (15, get_test_plan_version_id(text 'Toggle Button', '1'), '2021-05-14 14:18:23.602-05', '2022-07-07', 3, 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (16, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 1, 1, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (17, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 2, 2, 2, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "markedFinalAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (18, get_test_plan_version_id(text 'Command Button Example', '2'), '2023-12-13 14:18:23.602-05', '2023-12-14', 3, 3, 3, 'READY'); +INSERT INTO "TestPlanReport" (id, "testPlanVersionId", "createdAt", "atId", "minimumAtVersionId", "browserId", "vendorReviewStatus") VALUES (19, get_test_plan_version_id(text 'Modal Dialog Example', '2'), '2024-05-14 14:18:23.602-05', 2, 2, 2, 'READY'); -- -- Data for Name: TestPlanVersion; Type: TABLE DATA; Schema: public; Owner: atr diff --git a/server/tests/integration/automation-scheduler.test.js b/server/tests/integration/automation-scheduler.test.js index a8c6c9c69..c89b1a77b 100644 --- a/server/tests/integration/automation-scheduler.test.js +++ b/server/tests/integration/automation-scheduler.test.js @@ -1058,7 +1058,7 @@ describe('Automation controller', () => { // must be markedAsFinal to have historical results const finalizedTestPlanVersion = await markAsFinalResolver( { parentContext: { id: testPlanReportId } }, - null, + {}, context ); diff --git a/server/tests/integration/dataManagement.test.js b/server/tests/integration/dataManagement.test.js index f9fd7aeff..a91348f76 100644 --- a/server/tests/integration/dataManagement.test.js +++ b/server/tests/integration/dataManagement.test.js @@ -11,6 +11,22 @@ afterAll(async () => { await db.sequelize.close(); }, 20000); +const atsQuery = ({ transaction }) => { + return query( + gql` + query { + ats { + id + atVersions { + id + } + } + } + `, + { transaction } + ); +}; + const testPlanVersionsQuery = ({ transaction, directory = `""`, @@ -120,6 +136,12 @@ const updateVersionToPhaseQuery = ( at { id } + minimumAtVersion { + id + } + exactAtVersion { + id + } browser { id } @@ -177,40 +199,37 @@ const updateVersionToPhaseQuery = ( ); }; -const findOrCreateTestPlanReportQuery = ( +const createTestPlanReportQuery = ( testPlanVersionId, atId, + minimumAtVersionId, browserId, { transaction } ) => { return mutate( gql` mutation { - findOrCreateTestPlanReport(input: { + createTestPlanReport(input: { testPlanVersionId: ${testPlanVersionId} atId: ${atId} + minimumAtVersionId: ${minimumAtVersionId} browserId: ${browserId} }) { - populatedData { - testPlanReport { + testPlanReport { + id + at { id - at { - id - } - browser { - id - } } - testPlanVersion { + browser { id - phase - testPlanReports { - id - } } } - created { - locationOfData + testPlanVersion { + id + phase + testPlanReports { + id + } } } } @@ -578,6 +597,8 @@ describe('data management', () => { it('updates test plan version and copies all but one report from previous version', async () => { await dbCleaner(async transaction => { + const { ats } = await atsQuery({ transaction }); + // oldModalDialogVersion has reports for JAWS + Chrome, // NVDA + Chrome, VO + Safari + additional non-required // reports in CANDIDATE @@ -615,14 +636,13 @@ describe('data management', () => { }); const { - findOrCreateTestPlanReport: { - populatedData: { - testPlanVersion: newModalDialogVersionInDraft - } + createTestPlanReport: { + testPlanVersion: newModalDialogVersionInDraft } - } = await findOrCreateTestPlanReportQuery( + } = await createTestPlanReportQuery( newModalDialogVersion.id, 3, + ats.find(at => at.id == 3).atVersions[0].id, 1, { transaction } ); @@ -686,6 +706,8 @@ describe('data management', () => { it('updates test plan version but has new reports that are required and not yet marked as final', async () => { await dbCleaner(async transaction => { + const { ats } = await atsQuery({ transaction }); + // oldModalDialogVersion has reports for JAWS + Chrome, // NVDA + Chrome, VO + Safari + additional non-required // reports in CANDIDATE @@ -716,14 +738,13 @@ describe('data management', () => { }); const { - findOrCreateTestPlanReport: { - populatedData: { - testPlanVersion: newModalDialogVersionInDraft - } + createTestPlanReport: { + testPlanVersion: newModalDialogVersionInDraft } - } = await findOrCreateTestPlanReportQuery( + } = await createTestPlanReportQuery( newModalDialogVersion.id, 3, + ats.find(at => at.id == 3).atVersions[0].id, 3, { transaction } ); diff --git a/server/tests/integration/graphql.test.js b/server/tests/integration/graphql.test.js index d6d9744fa..e48f37c64 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -151,11 +151,6 @@ describe('graphql', () => { ['PopulatedData', 'browserVersion'], ['TestPlanReport', 'issues'], ['TestPlanReport', 'vendorReviewStatus'], - ['TestPlanReportOperations', 'updateTestPlanReportTestPlanVersion'], - ['TestPlanVersion', 'candidatePhaseReachedAt'], - ['TestPlanVersion', 'recommendedPhaseReachedAt'], - ['TestPlanVersion', 'recommendedPhaseTargetDate'], - ['TestPlanVersion', 'deprecatedAt'], ['Test', 'viewers'], ['Command', 'atOperatingMode'], // TODO: Include when v2 test format CI tests are done ['CollectionJob', 'testPlanRun'], @@ -386,6 +381,44 @@ describe('graphql', () => { directory } } + recommendedTestPlanVersion: testPlanVersion(id: 69) { + __typename + id + testPlanReports { + __typename + id + recommendedAtVersion { + __typename + id + name + releasedAt + } + } + earliestAtVersion(atId: 1) { + id + name + releasedAt + } + testPlanReportStatuses { + __typename + isRequired + at { + id + } + exactAtVersion { + id + } + minimumAtVersion { + id + } + browser { + id + } + testPlanReport { + id + } + } + } conflictTestPlanReport: testPlanReport(id: 2) { __typename id @@ -507,6 +540,15 @@ describe('graphql', () => { releasedAt } markedFinalAt + minimumAtVersion { + id + } + } + recommendedPhaseTestPlanReport: testPlanReport(id: 12) { + __typename + exactAtVersion { + id + } } testPlanReports { id @@ -607,20 +649,15 @@ describe('graphql', () => { $browserVersionId: ID! ) { __typename - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: 2 atId: 2 + minimumAtVersionId: 2 browserId: 2 } ) { __typename - populatedData { - locationOfData - } - created { - locationOfData - } } testPlanReport(id: 1) { __typename @@ -1010,7 +1047,3 @@ const getMutationInputs = async () => { browserVersionId: browserVersion.id }; }; - -/* Add the phrase to the assertion query. It will not work unless phrase is returned. -Find a test plan version that does have a phrase (V2). -*/ diff --git a/server/tests/integration/testPlanRun.test.js b/server/tests/integration/testPlanRun.test.js index a760e4a39..182740b82 100644 --- a/server/tests/integration/testPlanRun.test.js +++ b/server/tests/integration/testPlanRun.test.js @@ -41,19 +41,18 @@ const prepopulateTestPlanReport = async ({ transaction }) => { const mutationResult = await mutate( gql` mutation { - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: ${testPlanVersionId} atId: 1 + minimumAtVersionId: 1 browserId: 1 } ) { - populatedData { - testPlanReport { + testPlanReport { + id + runnableTests { id - runnableTests { - id - } } } } @@ -61,8 +60,7 @@ const prepopulateTestPlanReport = async ({ transaction }) => { `, { transaction } ); - const { testPlanReport } = - mutationResult.findOrCreateTestPlanReport.populatedData; + const { testPlanReport } = mutationResult.createTestPlanReport; testPlanReportId = testPlanReport.id; testId = testPlanReport.runnableTests[1].id; }; diff --git a/server/tests/integration/testQueue.test.js b/server/tests/integration/testQueue.test.js index 2277aece8..6f20adb73 100644 --- a/server/tests/integration/testQueue.test.js +++ b/server/tests/integration/testQueue.test.js @@ -278,57 +278,42 @@ describe('test queue', () => { // A1 const testPlanVersionId = '1'; const atId = '1'; + const minimumAtVersionId = '1'; const browserId = '1'; - const mutationToTest = async () => { - const result = await mutate( - gql` - mutation { - findOrCreateTestPlanReport(input: { - testPlanVersionId: ${testPlanVersionId} - atId: ${atId} - browserId: ${browserId} - }) { - populatedData { - testPlanReport { - id - at { - id - } - browser { - id - } - } - testPlanVersion { - id - phase - } + + // A2 + const result = await mutate( + gql` + mutation { + createTestPlanReport(input: { + testPlanVersionId: ${testPlanVersionId} + atId: ${atId} + minimumAtVersionId: ${minimumAtVersionId} + browserId: ${browserId} + }) { + testPlanReport { + id + at { + id } - created { - locationOfData + browser { + id } } + testPlanVersion { + id + phase + } } - `, - { transaction } - ); - const { - populatedData: { testPlanReport, testPlanVersion }, - created - } = result.findOrCreateTestPlanReport; - - return { - testPlanReport, - testPlanVersion, - created - }; - }; - - // A2 - const first = await mutationToTest(); - const second = await mutationToTest(); + } + `, + { transaction } + ); + const { testPlanReport, testPlanVersion } = + result.createTestPlanReport; // A3 - expect(first.testPlanReport).toEqual( + expect(testPlanReport).toEqual( expect.objectContaining({ id: expect.anything(), at: expect.objectContaining({ @@ -339,25 +324,12 @@ describe('test queue', () => { }) }) ); - expect(first.testPlanVersion).toEqual( + expect(testPlanVersion).toEqual( expect.objectContaining({ id: testPlanVersionId, phase: 'DRAFT' }) ); - expect(first.created.length).toBe(1); - - expect(second.testPlanReport).toEqual( - expect.objectContaining({ - id: first.testPlanReport.id - }) - ); - expect(second.testPlanVersion).toEqual( - expect.objectContaining({ - id: first.testPlanVersion.id - }) - ); - expect(second.created.length).toBe(0); }); }); diff --git a/server/tests/models/services/TestPlanReportService.test.js b/server/tests/models/services/TestPlanReportService.test.js index 78dea6a28..756fa7efe 100644 --- a/server/tests/models/services/TestPlanReportService.test.js +++ b/server/tests/models/services/TestPlanReportService.test.js @@ -157,49 +157,4 @@ describe('TestPlanReportModel Data Checks', () => { expect(deletedTestPlanReport).toBeNull(); }); }); - - it('should getOrCreate TestPlanReport', async () => { - await dbCleaner(async transaction => { - // A1 - const _testPlanVersionId = 2; - const _atId = 2; - const _browserId = 1; - - // A2 - const [testPlanReport, created] = - await TestPlanReportService.getOrCreateTestPlanReport({ - where: { - testPlanVersionId: _testPlanVersionId, - atId: _atId, - browserId: _browserId - }, - transaction - }); - - // A3 - expect(testPlanReport).toEqual( - expect.objectContaining({ - id: expect.anything(), - testPlanVersion: expect.objectContaining({ - id: _testPlanVersionId - }), - at: expect.objectContaining({ - id: _atId - }), - browser: expect.objectContaining({ - id: _browserId - }) - }) - ); - expect(created.length).toBe(1); - expect(created).toEqual( - expect.arrayContaining([ - // TestPlanReport - expect.objectContaining({ - testPlanReportId: testPlanReport.id - }) - ]) - ); - }); - }); }); diff --git a/vendors.txt b/vendors.txt index 5cc0e291c..a48d48696 100644 --- a/vendors.txt +++ b/vendors.txt @@ -3,4 +3,5 @@ # ex. warp2nowhere|vispero RFischer-FS|vispero stevefaulkner|vispero -BrettLewisVispero|vispero \ No newline at end of file +BrettLewisVispero|vispero +cookiecrook|apple \ No newline at end of file