-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Test Run / Navigator polling updates for collection jobs (#1125)
* Test Run / Navigator polling updates for collection jobs * fix test, add updates incoming warning * fix status messages * minute adjustment for error * isJobStatusFinal * Shared update context for CollectionJob, let smaller portions of page rerender * fix small render error for test run * Working for not logged in users again * Add smoke tests for /test-plan-report/15 as anon and admin
- Loading branch information
Showing
8 changed files
with
510 additions
and
279 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import PropTypes from 'prop-types'; | ||
import React, { createContext, useState, useEffect } from 'react'; | ||
import { useLazyQuery } from '@apollo/client'; | ||
import { COLLECTION_JOB_UPDATES_QUERY } from './queries'; | ||
import { isJobStatusFinal } from '../../utils/collectionJobStatus'; | ||
const pollInterval = 5000; | ||
|
||
export const Context = createContext({ | ||
state: { | ||
collectionJob: null | ||
}, | ||
actions: {} | ||
}); | ||
|
||
export const Provider = ({ children, testPlanRun }) => { | ||
if (!testPlanRun) { | ||
// Anonymous page / not working, just viewing the tests, no need for the | ||
// provider to provide any data or updates, but to be consistent we will | ||
// still wrap with a provider with static data | ||
return ( | ||
<Context.Provider value={{ state: {}, actions: {} }}> | ||
{children} | ||
</Context.Provider> | ||
); | ||
} | ||
const { id: testPlanRunId, collectionJob: initialCollectionJob } = | ||
testPlanRun; | ||
const [providerValue, setProviderValue] = useState({ | ||
state: { collectionJob: initialCollectionJob }, | ||
actions: {} | ||
}); | ||
|
||
const [, { data: collectionJobUpdateData, startPolling, stopPolling }] = | ||
testPlanRunId | ||
? useLazyQuery(COLLECTION_JOB_UPDATES_QUERY, { | ||
fetchPolicy: 'cache-and-network', | ||
variables: { collectionJobId: initialCollectionJob?.id }, | ||
pollInterval | ||
}) | ||
: {}; | ||
|
||
// control the data flow, turn on polling if this is a collection job report | ||
// that still has possible updates. | ||
useEffect(() => { | ||
// use the colllection job from the polling update first priority | ||
// otherwise, default to the first data fetch from the API | ||
const collectionJob = | ||
collectionJobUpdateData?.collectionJob ?? initialCollectionJob; | ||
const status = collectionJob?.status; | ||
if (collectionJob && !isJobStatusFinal(status)) { | ||
startPolling(pollInterval); | ||
} else { | ||
stopPolling(); | ||
} | ||
setProviderValue({ state: { collectionJob }, actions: {} }); | ||
}, [collectionJobUpdateData]); | ||
|
||
return ( | ||
<Context.Provider value={providerValue}>{children}</Context.Provider> | ||
); | ||
}; | ||
|
||
Provider.propTypes = { | ||
children: PropTypes.node, | ||
testPlanRun: PropTypes.shape({ | ||
id: PropTypes.string, | ||
collectionJob: PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
status: PropTypes.string.isRequired, | ||
testStatus: PropTypes.arrayOf(PropTypes.object).isRequired | ||
}) | ||
}) | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import React, { useContext } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { Button } from 'react-bootstrap'; | ||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||
import { | ||
faEdit, | ||
faCheck, | ||
faExclamationCircle, | ||
faRobot | ||
} from '@fortawesome/free-solid-svg-icons'; | ||
import { Context } from './CollectionJobContext'; | ||
import { | ||
COLLECTION_JOB_STATUS, | ||
isJobStatusFinal | ||
} from '../../utils/collectionJobStatus'; | ||
|
||
const TestRunHeading = ({ | ||
at, | ||
browser, | ||
editAtBrowserDetailsButtonRef, | ||
handleEditAtBrowserDetailsClick, | ||
isSignedIn, | ||
openAsUser, | ||
showEditAtBrowser, | ||
testPlanTitle, | ||
testResults, | ||
testCount | ||
}) => { | ||
const { | ||
state: { collectionJob } | ||
} = useContext(Context); | ||
|
||
const renderTestsCompletedInfoBox = () => { | ||
let isReviewingBot = Boolean(openAsUser?.isBot); | ||
let content; | ||
|
||
if (isReviewingBot) { | ||
const countTestResults = testResults.reduce( | ||
(acc, { scenarioResults }) => | ||
acc + | ||
(scenarioResults && | ||
scenarioResults.every(({ output }) => !!output) | ||
? 1 | ||
: 0), | ||
0 | ||
); | ||
const countCompleteCollection = collectionJob.testStatus.reduce( | ||
(acc, { status }) => | ||
acc + (status === COLLECTION_JOB_STATUS.COMPLETED ? 1 : 0), | ||
0 | ||
); | ||
|
||
content = ( | ||
<> | ||
<p> | ||
<b>{`${Math.max( | ||
countTestResults, | ||
countCompleteCollection | ||
)} of ${testCount}`}</b>{' '} | ||
responses collected. | ||
</p> | ||
<p> | ||
Collection Job Status: <b>{collectionJob.status}</b> | ||
</p> | ||
</> | ||
); | ||
} else if (!isSignedIn) { | ||
content = <b>{testCount} tests to view</b>; | ||
} else if (testCount) { | ||
content = ( | ||
<> | ||
{' '} | ||
<b>{`${testResults.reduce( | ||
(acc, { completedAt }) => acc + (completedAt ? 1 : 0), | ||
0 | ||
)} of ${testCount}`}</b>{' '} | ||
tests completed | ||
</> | ||
); | ||
} else { | ||
content = <div>No tests for this AT and Browser combination</div>; | ||
} | ||
|
||
return ( | ||
<div className="test-info-entity tests-completed"> | ||
<div className="info-label"> | ||
<FontAwesomeIcon | ||
icon={testCount ? faCheck : faExclamationCircle} | ||
/> | ||
{content} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
let openAsUserHeading = null; | ||
|
||
if (openAsUser?.isBot) { | ||
openAsUserHeading = ( | ||
<div className="test-info-entity reviewing-as bot"> | ||
Reviewing tests of{' '} | ||
<FontAwesomeIcon icon={faRobot} className="m-0" />{' '} | ||
<b>{`${openAsUser.username}`}.</b> | ||
{!isJobStatusFinal(collectionJob.status) && ( | ||
<> | ||
<br /> | ||
The collection bot is still updating information on this | ||
page. Changes may be lost when updates arrive. | ||
</> | ||
)} | ||
</div> | ||
); | ||
} else if (openAsUser) { | ||
openAsUserHeading = ( | ||
<div className="test-info-entity reviewing-as"> | ||
Reviewing tests of <b>{`${openAsUser.username}`}.</b> | ||
<p>{`All changes will be saved as performed by ${openAsUser.username}.`}</p> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<div className="test-info-wrapper"> | ||
<div | ||
className="test-info-entity apg-example-name" | ||
data-test="apg-example-name" | ||
> | ||
<div className="info-label"> | ||
<b>Test Plan:</b> {testPlanTitle} | ||
</div> | ||
</div> | ||
<div | ||
className="test-info-entity at-browser" | ||
data-test="at-browser" | ||
> | ||
<div className="at-browser-row"> | ||
<div className="info-label"> | ||
<b>AT:</b> {at} | ||
</div> | ||
<div className="info-label"> | ||
<b>Browser:</b> {browser} | ||
</div> | ||
</div> | ||
{showEditAtBrowser && ( | ||
<Button | ||
ref={editAtBrowserDetailsButtonRef} | ||
id="edit-fa-button" | ||
aria-label="Edit version details for AT and Browser" | ||
onClick={handleEditAtBrowserDetailsClick} | ||
> | ||
<FontAwesomeIcon icon={faEdit} /> | ||
</Button> | ||
)} | ||
</div> | ||
{renderTestsCompletedInfoBox()} | ||
</div> | ||
{openAsUserHeading} | ||
</> | ||
); | ||
}; | ||
|
||
TestRunHeading.propTypes = { | ||
testPlanTitle: PropTypes.string.isRequired, | ||
at: PropTypes.string.isRequired, | ||
browser: PropTypes.string.isRequired, | ||
showEditAtBrowser: PropTypes.bool.isRequired, | ||
editAtBrowserDetailsButtonRef: PropTypes.object.isRequired, | ||
isSignedIn: PropTypes.bool.isRequired, | ||
openAsUser: PropTypes.shape({ | ||
isBot: PropTypes.bool.isRequired, | ||
username: PropTypes.string.isRequired | ||
}), | ||
testResults: PropTypes.arrayOf(PropTypes.shape({})), | ||
testCount: PropTypes.number.isRequired, | ||
handleEditAtBrowserDetailsClick: PropTypes.func.isRequired | ||
}; | ||
|
||
export default TestRunHeading; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.