Skip to content

Commit

Permalink
feat: Add primary run dialog and earliest AT version algorithm (#1001)
Browse files Browse the repository at this point in the history
* Add minimum or exact at version to reports

* Quick tweak

* Revert home copy change

* Remove unused field from createTestPlanReport

* Fix undefined var

* Prevent API from creating duplicate reports

* Support primary test plan to be selected

* Fix test

* Add dialog when marking report as final for an admin to select from probably primary test run options

* prioritised -> prioritized typo (british -> american english)

* Avoid displaying primary test plan run confirmation when just 1 run option

* Add atVersion frontend

* Make sure automation dialog always shows when valid

* Make sure existing reports have a minimum at version

* Formatting

* feat: Add resolver for tracking first required AT Version (#1051) Address #792

* Add resolver for finding firstRequiredAtVersion for a RECOMMENDED TestPlanVersion, given an atId

* Update tests

* Fix graphql call when including "firstRequiredAtVersion" under "testPlanVersions"

* Update description of firstRequiredAtVersion

* Rename resolver

---------

Co-authored-by: alflennik <[email protected]>
  • Loading branch information
howard-e and alflennik authored May 2, 2024
1 parent 58fb718 commit 6dff002
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 26 deletions.
9 changes: 6 additions & 3 deletions client/components/TestQueue/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,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
}
Expand Down
12 changes: 9 additions & 3 deletions client/components/TestQueueRow/TestQueueRow.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
110 changes: 102 additions & 8 deletions client/components/TestQueueRow/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,14 +46,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
Expand All @@ -61,6 +64,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;
Expand Down Expand Up @@ -91,6 +95,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 => !isBot(tester));

const getTestPlanRunIdByUserId = userId => {
return draftTestPlanRuns.find(({ tester }) => tester.id === userId).id;
};
Expand Down Expand Up @@ -353,7 +370,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
Expand All @@ -370,12 +403,71 @@ const TestQueueRow = ({
}
};

const updateReportStatus = async () => {
const handlePrimaryTestRunChange = e => {
const value = e.target.value;
setPrimaryTestPlanRunId(value);
};

const renderPrimaryRunSelectionDialog = testers => {
return (
<BasicModal
show={showPrimaryTestPlanRunModal}
title="Select Primary Test Plan Run"
content={
<>
When a tester&apos;s run is marked as primary, it means
that their output for collected results will be
prioritized and shown on report pages.
<br />
<br />
A tester&apos;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.
<br />
<br />
<Form.Select
className="primary-test-run-select"
defaultValue={primaryTestPlanRunId}
onChange={handlePrimaryTestRunChange}
htmlSize={testers.length}
>
{testers.map(tester => (
<option
key={`${testPlanReport.id}-${tester.id}`}
value={tester.testPlanRunId}
>
{tester.username}
</option>
))}
</Form.Select>
</>
}
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();
Expand Down Expand Up @@ -600,6 +692,8 @@ const TestQueueRow = ({
showCloseAction={false}
/>
)}
{showPrimaryTestPlanRunModal &&
renderPrimaryRunSelectionDialog(primaryTestPlanRunOptions)}
</LoadingStatus>
);
};
Expand Down
28 changes: 27 additions & 1 deletion server/graphql-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,29 @@ const graphqlSchema = gql`
False value indicates to return the reports which have no markedFinalAt date.
"""
testPlanReports(isFinal: Boolean): [TestPlanReport]!
"""
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
}
"""
Expand Down Expand Up @@ -1235,8 +1258,11 @@ 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
});
});
}
};
5 changes: 5 additions & 0 deletions server/models/TestPlanRun.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
isPrimary: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: false
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions server/models/services/TestPlanRunService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
30 changes: 29 additions & 1 deletion server/resolvers/TestPlanReportOperations/markAsFinalResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() },
Expand Down
Loading

0 comments on commit 6dff002

Please sign in to comment.