-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Test Run / Navigator polling updates for collection jobs #1125
Changes from all commits
812450e
4aa0f4d
cff2313
8ad9f5a
e5765ed
3fded95
89100a7
df31163
08d9e3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: {} }); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Could be good to return a cleanup function in the event of a component unmount There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what this means exactly. Would this to be to enable the page to stop polling when the components on the page don't need it anymore? Seems like this case wouldn't be necessary, a minor save for what is unlikely to be a permanent situation (most up to date CollectionJob info for the current TestPlanRun is what this provider is providing) The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a cleanup function to ensure that the polling is stopped even if the context is unmounted at an unexpected time. My understanding is that this is best practice for any |
||||||||||||||
}, [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 | ||||||||||||||
}) | ||||||||||||||
}) | ||||||||||||||
}; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this way of sharing the polling across multiple components!