Skip to content
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: Track vendor approvals across version updates through result copy process #1267

Open
wants to merge 8 commits into
base: development
Choose a base branch
from
37 changes: 16 additions & 21 deletions client/components/CandidateReview/CandidateTestPlanRun/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const CandidateTestPlanRun = () => {
setFirstTimeViewing(true);
setViewedTests(tests => [...tests, currentTest.id]);
await addViewerToTest(currentTest.id);
refetch();
await refetch();
} else {
setFirstTimeViewing(false);
}
Expand Down Expand Up @@ -151,7 +151,7 @@ const CandidateTestPlanRun = () => {

const submitApproval = async (status = '') => {
if (status === 'APPROVED') {
updateVendorStatus(true);
await updateVendorStatus(true);
setConfirmationModal(
<ApprovedModal
handleAction={async () => {
Expand Down Expand Up @@ -180,8 +180,9 @@ const CandidateTestPlanRun = () => {
if (
!tests[0].viewers?.find(viewer => viewer.username === data.me.username)
) {
addViewerToTest(tests[0].id);
setFirstTimeViewing(true);
addViewerToTest(tests[0].id).then(() => {
setFirstTimeViewing(true);
});
}
const viewedTests = [
tests[0].id,
Expand Down Expand Up @@ -212,23 +213,17 @@ const CandidateTestPlanRun = () => {
useEffect(() => {
if (data) {
updateVendorStatus();
}
}, [reviewStatus]);

useEffect(() => {
if (data) {
updateTestViewed();
if (currentTestIndex !== 0) setIsFirstTest(false);
if (tests?.length === 1) setIsLastTest(true);
if (currentTestIndex + 1 === tests?.length) setIsLastTest(true);
}
}, [currentTestIndex]);

useEffect(() => {
if (data) {
setIsLastTest(tests?.length === 1);
}
}, [data, tests]);
}, [reviewStatus, currentTestIndex]);

useEffect(() => {
if (isLastTest) finishButtonRef.current.focus();
// Prevent a plan with only 1 test from immediately pushing the focus to the
// submit button
if (isLastTest && tests?.length !== 1) finishButtonRef.current.focus();
}, [isLastTest]);

if (error)
Expand Down Expand Up @@ -377,9 +372,9 @@ const CandidateTestPlanRun = () => {
NVDA: 'nvda'
};

if (githubAtLabelMap[at] == 'vo') {
if (githubAtLabelMap[at] === 'vo') {
fileBugUrl = 'https://bugs.webkit.org/buglist.cgi?quicksearch=voiceover';
} else if (githubAtLabelMap[at] == 'nvda') {
} else if (githubAtLabelMap[at] === 'nvda') {
fileBugUrl = 'https://github.com/nvaccess/nvda/issues';
} else {
fileBugUrl =
Expand Down Expand Up @@ -667,14 +662,14 @@ const CandidateTestPlanRun = () => {
issue =>
issue.isCandidateReview &&
issue.feedbackType === 'FEEDBACK' &&
issue.author == data.me.username
issue.author === data.me.username
)}
feedbackGithubUrl={feedbackGithubUrl}
changesRequestedIssues={testPlanReport.issues?.filter(
issue =>
issue.isCandidateReview &&
issue.feedbackType === 'CHANGES_REQUESTED' &&
issue.author == data.me.username
issue.author === data.me.username
)}
changesRequestedGithubUrl={changesRequestedGithubUrl}
handleAction={submitApproval}
Expand Down
59 changes: 48 additions & 11 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,57 @@ window.signMeInAsTester = username => {
return signMeInCommon({ username, roles: [{ name: 'TESTER' }] });
};

window.signMeInAsVendor = username => {
/**
* @param {string} username
* @param {'vispero'|'nvAccess'|'apple'} vendor
* @returns {Promise<void>}
*/
window.signMeInAsVendor = (username, vendor = 'vispero') => {
let company = {
id: '1',
name: 'vispero',
ats: [
{
id: '1',
name: 'JAWS'
}
]
};

switch (vendor.trim()) {
case 'nvAccess':
company = {
id: '6',
name: 'nvAccess',
ats: [
{
id: '2',
name: 'NVDA'
}
]
};
break;
case 'apple':
company = {
id: '4',
name: 'apple',
ats: [
{
id: '3',
name: 'VoiceOver for macOS'
}
]
};
break;
default:
// maintain default vispero selection
break;
}

return signMeInCommon({
username,
roles: [{ name: 'VENDOR' }],
company: {
id: '1',
name: 'vispero',
ats: [
{
id: '1',
name: 'JAWS'
}
]
}
company
});
};

Expand Down
8 changes: 8 additions & 0 deletions docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ Another way to log in as either a tester or admin, useful for quick testing and
```
signMeInAsVendor("joe-the-vendor")
```
By default, you will be signed in as a "vispero (JAWS)" vendor but to sign in as any others, you can provide the vendor's company name as the second parameter. eg:
```
signMeInAsVendor("joe-the-vendor", "apple")
```
The list of applicable constants is currently:
1. vispero (JAWS)
2. nvAccess (NVDA)
3. apple (VoiceOver for macOS)

The part in quotes is the username, feel free to change the username to whatever you prefer.

Expand Down
34 changes: 33 additions & 1 deletion server/models/services/TestPlanVersionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,42 @@ const updateTestPlanVersionById = async ({
});
};

/**
* @param {object} options
* @param {number} id - id of the TestPlanVersion record to be updated
* @param {number} testId
* @param {object[]} viewers
* @param {*} options.transaction - Sequelize transaction
* @returns {Promise<void>}
*/
const updateTestViewersOnTestPlanVersion = async ({
id,
testId,
viewers = [],
transaction
}) => {
await ModelService.rawQuery(
`
update "TestPlanVersion"
set tests = ( select jsonb_agg(case when element ->> 'id' = '${testId}'
then jsonb_set(element, '{viewers}', '${JSON.stringify(
viewers
)}'::jsonb) else element end)
from jsonb_array_elements(tests) as element )
where id = ${id}
and tests @> '[{"id": "${testId}"}]';
`,
{ transaction }
);
};

module.exports = {
// Basic CRUD
getTestPlanVersionById,
getTestPlanVersions,
createTestPlanVersion,
updateTestPlanVersionById
updateTestPlanVersionById,

// Custom functions
updateTestViewersOnTestPlanVersion
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
const { AuthenticationError } = require('apollo-server-errors');
const {
updateTestPlanReportById
} = require('../../models/services/TestPlanReportService');
const populateData = require('../../services/PopulatedData/populateData');
const checkUserRole = require('../helpers/checkUserRole');

const promoteVendorReviewStatusResolver = async (
{ parentContext: { id: testPlanReportId } },
{ vendorReviewStatus },
context
) => {
const { transaction } = context;
const { user, transaction } = context;

const isAdmin = checkUserRole.isAdmin(user?.roles);
const isVendor = checkUserRole.isVendor(user?.roles);
if (!(isAdmin || isVendor)) {
throw new AuthenticationError();
}

let values = { vendorReviewStatus };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,13 @@ const updatePhaseResolver = async (
if (phase === 'CANDIDATE') {
updateParams = {
...updateParams,
metrics: { ...testPlanReport.metrics, ...metrics },
vendorReviewStatus: 'READY'
metrics: { ...testPlanReport.metrics, ...metrics }
};

// In the instance where an older result's status is being copied over
if (!testPlanReport.vendorReviewStatus) {
updateParams = { ...updateParams, vendorReviewStatus: 'READY' };
}
} else if (phase === 'RECOMMENDED') {
updateParams = {
...updateParams,
Expand Down
10 changes: 4 additions & 6 deletions server/resolvers/addViewerResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ const {
getTestPlanVersionById,
updateTestPlanVersionById
} = require('../models/services/TestPlanVersionService');
const checkUserRole = require('./helpers/checkUserRole');

const addViewerResolver = async (_, { testPlanVersionId, testId }, context) => {
const { user, transaction } = context;

if (
!(
user?.roles.find(role => role.name === 'ADMIN') ||
user?.roles.find(role => role.name === 'VENDOR')
)
) {
const isAdmin = checkUserRole.isAdmin(user?.roles);
const isVendor = checkUserRole.isVendor(user?.roles);
if (!(isAdmin || isVendor)) {
throw new AuthenticationError();
}

Expand Down
15 changes: 15 additions & 0 deletions server/resolvers/helpers/checkUserRole.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const ROLES = {
TESTER: 'TESTER',
ADMIN: 'ADMIN',
VENDOR: 'VENDOR'
};

const isTester = (roles = []) => roles.find(role => role.name === ROLES.TESTER);
const isVendor = (roles = []) => roles.find(role => role.name === ROLES.VENDOR);
const isAdmin = (roles = []) => roles.find(role => role.name === ROLES.ADMIN);

module.exports = {
isTester,
isVendor,
isAdmin
};
38 changes: 37 additions & 1 deletion server/resolvers/helpers/processCopiedReports.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {
getTestPlanVersionById
getTestPlanVersionById,
updateTestViewersOnTestPlanVersion
} = require('../../models/services/TestPlanVersionService');
const {
getTestPlanReports,
Expand Down Expand Up @@ -328,6 +329,9 @@ const processCopiedReports = async ({
);

for (const oldTestPlanRun of oldTestPlanReport.testPlanRuns) {
const oldTestPlanRunVendorReviewStatus =
oldTestPlanReport.vendorReviewStatus;

// Track which old test results need to be preserved
const keptTestResultsByTestId = getKeptTestResultsByTestId(
oldTestPlanRun.testResults,
Expand Down Expand Up @@ -358,6 +362,7 @@ const processCopiedReports = async ({
},
transaction
});
let allResultsPreserved = true;
const newTestResults = [];

for (const testResultToSaveTestId of Object.keys(
Expand All @@ -375,6 +380,10 @@ const processCopiedReports = async ({
{ testId: testResultToSaveTestId },
{ context }
);
const { test: oldTest } = await populateData(
{ testResultId: oldTestResult.id },
{ context }
);

// Re-run createTestResultSkeleton to avoid unexpected scenario index matching issues when saving
// future results; override newly generated test results with old results if exists
Expand Down Expand Up @@ -403,6 +412,7 @@ const processCopiedReports = async ({
// Unknown combination of command + settings when compared with last version
const oldScenarioResult = scenarioResultsByScenarioIds[rawScenarioId];
if (!oldScenarioResult) {
allResultsPreserved = false;
newTestResult.completedAt = null;
continue;
}
Expand All @@ -428,17 +438,43 @@ const processCopiedReports = async ({
const oldAssertionResult =
assertionResultsByAssertionIds[rawAssertionId];
if (!oldAssertionResult) {
allResultsPreserved = false;
newTestResult.completedAt = null;
continue;
}

// Update TestPlanVersion.tests to include the viewers from the old
// TestPlanVersion.tests
// TODO: Move viewers to TestPlanReport; more appropriate and
// understandable database structure
if (oldTest.viewers) {
await updateTestViewersOnTestPlanVersion({
id: newTestPlanVersionId,
testId: testResultToSaveTestId,
viewers: oldTest.viewers,
transaction
});
}

eachAssertionResult.passed = oldAssertionResult.passed;
}
}

newTestResults.push(newTestResult);
}

// Since no substantive changes, preserve the TestPlanReport's
// vendorReviewStatus if exists
if (allResultsPreserved && oldTestPlanRunVendorReviewStatus) {
await updateTestPlanReportById({
id: newTestPlanReport.id,
values: {
vendorReviewStatus: oldTestPlanRunVendorReviewStatus
},
transaction
});
}

// Run updated metrics calculations for new TestPlanRun test results to be used in metrics calculations
await updateMetricsAndMarkedFinalAtForTestPlanReport({
newTestPlanReport,
Expand Down
2 changes: 1 addition & 1 deletion server/resources/ats.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"id": 2,
"name": "NVDA",
"key": "nvda",
"vendorId": null,
"vendorId": 6,
"defaultConfigurationInstructionsHTML": "Configure NVDA with default settings. For help, read &lt;a href=&quot;https://github.com/w3c/aria-at/wiki/Configuring-Screen-Readers-for-Testing&quot;&gt;Configuring Screen Readers for Testing&lt;/a&gt;.",
"assertionTokens": {
"screenReader": "NVDA",
Expand Down
Loading
Loading