From e563863fc069d8ab8278ff9a910a37366d4a6469 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 13:37:34 -0400 Subject: [PATCH 01/11] Add minimum or exact at version to reports --- .../AddTestToQueueWithConfirmation/index.jsx | 38 +- client/components/Home/Home.jsx | 6 +- client/components/TestQueue/queries.js | 19 +- server/graphql-schema.js | 45 +- ...veTestPlanReportValuesToTestPlanVersion.js | 634 ------------------ ...20240312122457-testPlanReportAtVersions.js | 37 + server/models/TestPlanReport.js | 8 + .../models/services/TestPlanReportService.js | 97 +-- .../TestPlanReportOperations/index.js | 4 +- ...teTestPlanReportTestPlanVersionResolver.js | 341 ---------- .../resolvers/createTestPlanReportResolver.js | 28 + .../findOrCreateTestPlanReportResolver.js | 33 - .../resolvers/helpers/processCopiedReports.js | 20 +- server/resolvers/index.js | 4 +- .../tests/integration/dataManagement.test.js | 45 +- server/tests/integration/graphql.test.js | 9 +- server/tests/integration/testPlanRun.test.js | 13 +- server/tests/integration/testQueue.test.js | 86 +-- .../services/TestPlanReportService.test.js | 45 -- 19 files changed, 190 insertions(+), 1322 deletions(-) delete mode 100644 server/migrations/20230608171911-moveTestPlanReportValuesToTestPlanVersion.js create mode 100644 server/migrations/20240312122457-testPlanReportAtVersions.js delete mode 100644 server/resolvers/TestPlanReportOperations/updateTestPlanReportTestPlanVersionResolver.js create mode 100644 server/resolvers/createTestPlanReportResolver.js delete mode 100644 server/resolvers/findOrCreateTestPlanReportResolver.js diff --git a/client/components/AddTestToQueueWithConfirmation/index.jsx b/client/components/AddTestToQueueWithConfirmation/index.jsx index 637a7d2ee..5acc09284 100644 --- a/client/components/AddTestToQueueWithConfirmation/index.jsx +++ b/client/components/AddTestToQueueWithConfirmation/index.jsx @@ -149,24 +149,29 @@ function AddTestToQueueWithConfirmation({ 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?' + 'browser and test plan version. Work is currently ' + + 'underway to remove this limitation.' } closeLabel="Cancel" staticBackdrop={true} - actions={[ - { - label: 'Proceed', - onClick: async () => { - setErrorMessage(false); - if (hasAutomationSupport) { - setShowConfirmation(true); - } else { - await addTestToQueue(); - } - } - } - ]} + // The proceed button is disabled because it will now create + // duplicate testPlanReports, and support has not yet been + // implemented across the full frontend to account for + // duplicates. + + // actions={[ + // { + // label: 'Proceed', + // onClick: async () => { + // setErrorMessage(false); + // if (hasAutomationSupport) { + // setShowConfirmation(true); + // } else { + // await addTestToQueue(); + // } + // } + // } + // ]} useOnHide handleClose={async () => { setErrorMessage(false); @@ -186,8 +191,7 @@ function AddTestToQueueWithConfirmation({ } }); const testPlanReport = - res?.data?.findOrCreateTestPlanReport?.populatedData - ?.testPlanReport ?? null; + res?.data?.createTestPlanReport?.testPlanReport ?? null; tpr = testPlanReport; }, 'Adding Test Plan to Test Queue'); setShowConfirmation(true); diff --git a/client/components/Home/Home.jsx b/client/components/Home/Home.jsx index 1bc4668c6..08a5b3216 100644 --- a/client/components/Home/Home.jsx +++ b/client/components/Home/Home.jsx @@ -178,9 +178,9 @@ const Home = () => {

Enabling AT interoperability is a large, ongoing endeavor that requires industry-wide collaboration and - support. The W3C ARIA-AT Community Group is focusing on - a stable and mature test suite for five screen readers - by the end of 2023. In the future, we plan to test + support. The W3C ARIA-AT Community Group is currently + focusing on a stable and mature test suite for five + screen readers. In the future, we plan to test additional screen readers and other kinds of assistive technologies with a broader set of web design patterns and test material. diff --git a/client/components/TestQueue/queries.js b/client/components/TestQueue/queries.js index faaf8300d..05276cbbc 100644 --- a/client/components/TestQueue/queries.js +++ b/client/components/TestQueue/queries.js @@ -223,29 +223,24 @@ export const ADD_TEST_QUEUE_MUTATION = gql` $atId: ID! $browserId: ID! ) { - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: $testPlanVersionId atId: $atId browserId: $browserId } ) { - populatedData { - testPlanReport { + testPlanReport { + id + at { id - at { - id - } - browser { - id - } } - testPlanVersion { + browser { id } } - created { - locationOfData + testPlanVersion { + id } } } diff --git a/server/graphql-schema.js b/server/graphql-schema.js index fb818822d..8ec87b39a 100644 --- a/server/graphql-schema.js +++ b/server/graphql-schema.js @@ -1016,6 +1016,8 @@ const graphqlSchema = gql` testPlanVersionId: ID! atId: ID! browserId: ID! + exactAtVersionId: ID + minimumAtVersionId: ID } """ @@ -1230,15 +1232,6 @@ const graphqlSchema = gql` """ 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 """ @@ -1331,25 +1324,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. @@ -1364,17 +1338,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/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..4c1c12455 --- /dev/null +++ b/server/migrations/20240312122457-testPlanReportAtVersions.js @@ -0,0 +1,37 @@ +'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 } + ); + }); + }, + + 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/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/services/TestPlanReportService.js b/server/models/services/TestPlanReportService.js index 8fd9eee90..303402003 100644 --- a/server/models/services/TestPlanReportService.js +++ b/server/models/services/TestPlanReportService.js @@ -215,7 +215,14 @@ const getTestPlanReports = async ({ * @returns {Promise<*>} */ const createTestPlanReport = async ({ - values: { testPlanVersionId, atId, browserId }, + values: { + testPlanVersionId, + atId, + browserId, + exactAtVersionId, + minimumAtVersionId, + vendorReviewStatus + }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, @@ -234,6 +241,9 @@ const createTestPlanReport = async ({ testPlanVersionId, atId, browserId, + exactAtVersionId, + minimumAtVersionId, + vendorReviewStatus, testPlanId: testPlanVersion.testPlanId }, transaction @@ -326,94 +336,11 @@ const removeTestPlanReportById = async ({ }); }; -/** - * Gets one TestPlanReport, or creates it if it doesn't exist, and then optionally updates it. Supports nested / associated values. - * @param {object} options - * @param {*} options.where - These values will be used to find a matching record, or they will be used to create one - * @param {string[]} options.testPlanReportAttributes - TestPlanReport attributes to be returned in the result - * @param {string[]} options.testPlanRunAttributes - TestPlanRun attributes to be returned in the result - * @param {string[]} options.testPlanVersionAttributes - TestPlanVersion attributes to be returned in the result - * @param {string[]} options.testPlanAttributes - TestPlan attributes to be returned in the result - * @param {string[]} options.atAttributes - At attributes to be returned in the result - * @param {string[]} options.browserAttributes - Browser attributes to be returned in the result - * @param {string[]} options.userAttributes - User attributes to be returned in the result - * @param {*} options.transaction - Sequelize transaction - * @returns {Promise<[*, [*]]>} - */ -const getOrCreateTestPlanReport = async ({ - where: { testPlanVersionId, atId, browserId }, - testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, - testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, - testPlanVersionAttributes = TEST_PLAN_VERSION_ATTRIBUTES, - testPlanAttributes = TEST_PLAN_ATTRIBUTES, - atAttributes = AT_ATTRIBUTES, - browserAttributes = BROWSER_ATTRIBUTES, - userAttributes = USER_ATTRIBUTES, - transaction -}) => { - const accumulatedResults = await ModelService.nestedGetOrCreate({ - operations: [ - { - get: getTestPlanReports, - create: createTestPlanReport, - values: { testPlanVersionId, atId, browserId }, - returnAttributes: { - testPlanReportAttributes, - testPlanRunAttributes: [], - testPlanVersionAttributes: [], - testPlanAttributes: [], - atAttributes: [], - browserAttributes: [], - userAttributes: [] - } - } - ], - transaction - }); - - const [[{ id: testPlanReportId }, isNewTestPlanReport]] = - accumulatedResults; - - const testPlanReport = await getTestPlanReportById({ - id: testPlanReportId, - testPlanReportAttributes, - testPlanRunAttributes, - testPlanVersionAttributes, - testPlanAttributes, - atAttributes, - browserAttributes, - userAttributes, - transaction - }); - - // If a TestPlanReport is being intentionally created that was previously marked as final, - // This will allow it to be displayed in the Test Queue again to be worked on - if (!isNewTestPlanReport && testPlanReport.markedFinalAt) { - await updateTestPlanReportById({ - id: testPlanReportId, - values: { markedFinalAt: null }, - testPlanReportAttributes, - testPlanRunAttributes, - testPlanVersionAttributes, - testPlanAttributes, - atAttributes, - browserAttributes, - userAttributes, - transaction - }); - } - - const created = isNewTestPlanReport ? [{ testPlanReportId }] : []; - - return [testPlanReport, created]; -}; - module.exports = { // Basic CRUD getTestPlanReportById, getTestPlanReports, createTestPlanReport, updateTestPlanReportById, - removeTestPlanReportById, - getOrCreateTestPlanReport + removeTestPlanReportById }; 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/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/createTestPlanReportResolver.js b/server/resolvers/createTestPlanReportResolver.js new file mode 100644 index 000000000..7e75b749b --- /dev/null +++ b/server/resolvers/createTestPlanReportResolver.js @@ -0,0 +1,28 @@ +const { AuthenticationError } = require('apollo-server-errors'); +const { + createTestPlanReport +} = require('../models/services/TestPlanReportService'); +const populateData = require('../services/PopulatedData/populateData'); + +const createTestPlanReportResolver = async (_, { input }, context) => { + const { user, transaction } = context; + + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + const testPlanReport = await createTestPlanReport({ + values: input, + 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 303c2d41c..000000000 --- a/server/resolvers/findOrCreateTestPlanReportResolver.js +++ /dev/null @@ -1,33 +0,0 @@ -const { AuthenticationError } = require('apollo-server-errors'); -const { - getOrCreateTestPlanReport -} = require('../models/services/TestPlanReportService'); -const populateData = require('../services/PopulatedData/populateData'); - -const findOrCreateTestPlanReportResolver = async (_, { input }, context) => { - const { user, transaction } = context; - - if (!user?.roles.find(role => role.name === 'ADMIN')) { - throw new AuthenticationError(); - } - - 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/processCopiedReports.js b/server/resolvers/helpers/processCopiedReports.js index 8430b3a65..53d2bc768 100644 --- a/server/resolvers/helpers/processCopiedReports.js +++ b/server/resolvers/helpers/processCopiedReports.js @@ -3,7 +3,7 @@ const { } = require('../../models/services/TestPlanVersionService'); const { getTestPlanReports, - getOrCreateTestPlanReport, + createTestPlanReport, updateTestPlanReportById } = require('../../models/services/TestPlanReportService'); const { @@ -166,16 +166,14 @@ const processCopiedReports = async ({ } if (Object.keys(newTestResultsToSaveByTestId).length) { - const [newTestPlanReport] = await getOrCreateTestPlanReport( - { - where: { - testPlanVersionId: newTestPlanVersionId, - atId: oldTestPlanReport.atId, - browserId: oldTestPlanReport.browserId - }, - transaction - } - ); + const newTestPlanReport = await createTestPlanReport({ + values: { + testPlanVersionId: newTestPlanVersionId, + atId: oldTestPlanReport.atId, + browserId: oldTestPlanReport.browserId + }, + transaction + }); newTestPlanReportIds.push(newTestPlanReport.id); diff --git a/server/resolvers/index.js b/server/resolvers/index.js index 5e097b81d..5005094a3 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'); @@ -73,7 +73,7 @@ const resolvers = { testResult: mutateTestResult, testPlanVersion: mutateTestPlanVersion, collectionJob: mutateCollectionJob, - findOrCreateTestPlanReport, + createTestPlanReport, updateMe, addViewer, updateCollectionJob, diff --git a/server/tests/integration/dataManagement.test.js b/server/tests/integration/dataManagement.test.js index d1b38e349..74b040c8f 100644 --- a/server/tests/integration/dataManagement.test.js +++ b/server/tests/integration/dataManagement.test.js @@ -163,7 +163,7 @@ const updateVersionToPhaseQuery = ( ); }; -const findOrCreateTestPlanReportQuery = ( +const createTestPlanReportQuery = ( testPlanVersionId, atId, browserId, @@ -172,31 +172,26 @@ const findOrCreateTestPlanReportQuery = ( return mutate( gql` mutation { - findOrCreateTestPlanReport(input: { + createTestPlanReport(input: { testPlanVersionId: ${testPlanVersionId} atId: ${atId} 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 + } } } } @@ -515,12 +510,10 @@ describe('data management', () => { }); const { - findOrCreateTestPlanReport: { - populatedData: { - testPlanVersion: newModalDialogVersionInDraft - } + createTestPlanReport: { + testPlanVersion: newModalDialogVersionInDraft } - } = await findOrCreateTestPlanReportQuery( + } = await createTestPlanReportQuery( newModalDialogVersion.id, 3, 1, @@ -625,12 +618,10 @@ describe('data management', () => { }); const { - findOrCreateTestPlanReport: { - populatedData: { - testPlanVersion: newModalDialogVersionInDraft - } + createTestPlanReport: { + testPlanVersion: newModalDialogVersionInDraft } - } = await findOrCreateTestPlanReportQuery( + } = await createTestPlanReportQuery( newModalDialogVersion.id, 3, 3, diff --git a/server/tests/integration/graphql.test.js b/server/tests/integration/graphql.test.js index 9bfeca0d8..d61fa3471 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -150,7 +150,6 @@ describe('graphql', () => { ['PopulatedData', 'browserVersion'], ['TestPlanReport', 'issues'], ['TestPlanReport', 'vendorReviewStatus'], - ['TestPlanReportOperations', 'updateTestPlanReportTestPlanVersion'], ['TestPlanVersion', 'candidatePhaseReachedAt'], ['TestPlanVersion', 'recommendedPhaseReachedAt'], ['TestPlanVersion', 'recommendedPhaseTargetDate'], @@ -589,7 +588,7 @@ describe('graphql', () => { $browserVersionId: ID! ) { __typename - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: 2 atId: 2 @@ -597,12 +596,6 @@ describe('graphql', () => { } ) { __typename - populatedData { - locationOfData - } - created { - locationOfData - } } testPlanReport(id: 1) { __typename diff --git a/server/tests/integration/testPlanRun.test.js b/server/tests/integration/testPlanRun.test.js index a760e4a39..94f4c6000 100644 --- a/server/tests/integration/testPlanRun.test.js +++ b/server/tests/integration/testPlanRun.test.js @@ -41,19 +41,17 @@ const prepopulateTestPlanReport = async ({ transaction }) => { const mutationResult = await mutate( gql` mutation { - findOrCreateTestPlanReport( + createTestPlanReport( input: { testPlanVersionId: ${testPlanVersionId} atId: 1 browserId: 1 } ) { - populatedData { - testPlanReport { + testPlanReport { + id + runnableTests { id - runnableTests { - id - } } } } @@ -61,8 +59,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..b7b6b1da5 100644 --- a/server/tests/integration/testQueue.test.js +++ b/server/tests/integration/testQueue.test.js @@ -279,56 +279,39 @@ describe('test queue', () => { const testPlanVersionId = '1'; const atId = '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} + 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 +322,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 - }) - ]) - ); - }); - }); }); From f233b0fb8de1be74c7545366f4ba8708aac02922 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 13:57:36 -0400 Subject: [PATCH 02/11] Quick tweak --- server/resolvers/createTestPlanReportResolver.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/resolvers/createTestPlanReportResolver.js b/server/resolvers/createTestPlanReportResolver.js index 7e75b749b..f0be12bfa 100644 --- a/server/resolvers/createTestPlanReportResolver.js +++ b/server/resolvers/createTestPlanReportResolver.js @@ -19,10 +19,7 @@ const createTestPlanReportResolver = async (_, { input }, context) => { const locationOfData = { testPlanReportId: testPlanReport.id }; const preloaded = { testPlanReport }; - return populateData(locationOfData, { - preloaded, - context - }); + return populateData(locationOfData, { preloaded, context }); }; module.exports = createTestPlanReportResolver; From 5d7ef54c633d7663e9662d01fad896f8f1983e04 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 17:22:31 -0400 Subject: [PATCH 03/11] Revert home copy change --- client/components/Home/Home.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/Home/Home.jsx b/client/components/Home/Home.jsx index 08a5b3216..1bc4668c6 100644 --- a/client/components/Home/Home.jsx +++ b/client/components/Home/Home.jsx @@ -178,9 +178,9 @@ const Home = () => {

Enabling AT interoperability is a large, ongoing endeavor that requires industry-wide collaboration and - support. The W3C ARIA-AT Community Group is currently - focusing on a stable and mature test suite for five - screen readers. In the future, we plan to test + support. The W3C ARIA-AT Community Group is focusing on + a stable and mature test suite for five screen readers + by the end of 2023. In the future, we plan to test additional screen readers and other kinds of assistive technologies with a broader set of web design patterns and test material. From 408b3f1026b0ce775878ba093da562e40cfdaed3 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 17:40:52 -0400 Subject: [PATCH 04/11] Remove unused field from createTestPlanReport --- server/models/services/TestPlanReportService.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/models/services/TestPlanReportService.js b/server/models/services/TestPlanReportService.js index 303402003..e9fc7c354 100644 --- a/server/models/services/TestPlanReportService.js +++ b/server/models/services/TestPlanReportService.js @@ -220,8 +220,7 @@ const createTestPlanReport = async ({ atId, browserId, exactAtVersionId, - minimumAtVersionId, - vendorReviewStatus + minimumAtVersionId }, testPlanReportAttributes = TEST_PLAN_REPORT_ATTRIBUTES, testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES, From 1d140c09570992b21a9e77055a61beba40d56718 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 17:41:35 -0400 Subject: [PATCH 05/11] Fix undefined var --- server/models/services/TestPlanReportService.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/models/services/TestPlanReportService.js b/server/models/services/TestPlanReportService.js index e9fc7c354..e38cee595 100644 --- a/server/models/services/TestPlanReportService.js +++ b/server/models/services/TestPlanReportService.js @@ -242,7 +242,6 @@ const createTestPlanReport = async ({ browserId, exactAtVersionId, minimumAtVersionId, - vendorReviewStatus, testPlanId: testPlanVersion.testPlanId }, transaction From 9695e4f460fe5b1e514c7e793c4eac0dc46ca9e8 Mon Sep 17 00:00:00 2001 From: alflennik Date: Wed, 3 Apr 2024 17:56:50 -0400 Subject: [PATCH 06/11] Prevent API from creating duplicate reports --- .../resolvers/createTestPlanReportResolver.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/server/resolvers/createTestPlanReportResolver.js b/server/resolvers/createTestPlanReportResolver.js index f0be12bfa..4ecde0e74 100644 --- a/server/resolvers/createTestPlanReportResolver.js +++ b/server/resolvers/createTestPlanReportResolver.js @@ -1,12 +1,33 @@ const { AuthenticationError } = require('apollo-server-errors'); const { - createTestPlanReport + createTestPlanReport, + getTestPlanReports } = require('../models/services/TestPlanReportService'); const populateData = require('../services/PopulatedData/populateData'); const createTestPlanReportResolver = async (_, { input }, context) => { const { user, transaction } = context; + // TODO: remove code related to conflictingReports which is meant to be + // temporary until the frontend fully supports duplicate AT / Browsers + // for TestPlanReports. + // START TEMPORARY CODE + const conflictingReports = await getTestPlanReports({ + where: { + testPlanVersionId: input.testPlanVersionId, + atId: input.atId, + browserId: input.browserId + }, + transaction + }); + if (conflictingReports.length) { + throw new Error( + 'Temporarily, creating duplicate reports for a AT / browser ' + + 'combination is not allowed' + ); + } + // END TEMPORARY CODE + if (!user?.roles.find(role => role.name === 'ADMIN')) { throw new AuthenticationError(); } From f60af799fbf34a60d7129699d51b347de3672d96 Mon Sep 17 00:00:00 2001 From: alflennik Date: Mon, 22 Apr 2024 13:55:57 -0400 Subject: [PATCH 07/11] Add atVersion frontend --- .../AddTestToQueueWithConfirmation/index.jsx | 131 ++++++------------ client/components/ManageTestQueue/index.jsx | 115 +++++++++++++-- .../components/TestPlanVersionsPage/index.jsx | 68 --------- client/components/TestQueue/queries.js | 4 + client/components/common/RadioBox/index.jsx | 88 ++++++++++++ client/tests/AtVersions.test.js | 4 +- server/graphql-schema.js | 2 + .../resolvers/createTestPlanReportResolver.js | 31 +++++ .../tests/integration/dataManagement.test.js | 54 ++++---- server/tests/integration/graphql.test.js | 1 + server/tests/integration/testPlanRun.test.js | 1 + server/tests/integration/testQueue.test.js | 2 + 12 files changed, 311 insertions(+), 190 deletions(-) create mode 100644 client/components/common/RadioBox/index.jsx create mode 100644 server/resolvers/createTestPlanReportResolver.js diff --git a/client/components/AddTestToQueueWithConfirmation/index.jsx b/client/components/AddTestToQueueWithConfirmation/index.jsx index 4add46dfc..bd7accaeb 100644 --- a/client/components/AddTestToQueueWithConfirmation/index.jsx +++ b/client/components/AddTestToQueueWithConfirmation/index.jsx @@ -19,6 +19,8 @@ function AddTestToQueueWithConfirmation({ testPlanVersion, browser, at, + exactAtVersion, + minimumAtVersion, disabled = false, buttonText = 'Add to Test Queue', triggerUpdate = () => {} @@ -44,23 +46,11 @@ 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 - ) { + // Check if any results data available from a previous result + if (existingTestPlanReportsData?.oldTestPlanVersions?.length) { latestOldVersion = existingTestPlanReportsData?.oldTestPlanVersions?.reduce((a, b) => new Date(a.updatedAt) > new Date(b.updatedAt) ? a : b @@ -184,76 +174,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({ + copyResultsFromTestPlanReportId: + latestOldVersion.id + }); + } + } + } + ]} useOnHide handleClose={async () => { setShowPreserveReportDataMessage(false); @@ -269,6 +230,8 @@ function AddTestToQueueWithConfirmation({ variables: { testPlanVersionId: testPlanVersion.id, atId: at.id, + minimumAtVersionId: minimumAtVersion?.id, + exactAtVersionId: exactAtVersion?.id, browserId: browser.id, copyResultsFromTestPlanReportId } @@ -300,14 +263,10 @@ function AddTestToQueueWithConfirmation({ disabled={disabled} variant="secondary" onClick={async () => { - if (conflictingReportExists || oldReportToCopyResultsFrom) { + if (oldReportToCopyResultsFrom) { setShowPreserveReportDataMessage(true); } else { - if (hasAutomationSupport) { - setShowConfirmation(true); - } else { - await addTestToQueue(); - } + await addTestToQueue(); } }} className="w-auto" @@ -325,6 +284,8 @@ AddTestToQueueWithConfirmation.propTypes = { testPlanVersion: PropTypes.object, browser: PropTypes.object, at: PropTypes.object, + exactAtVersion: PropTypes.object, + minimumAtVersion: PropTypes.object, buttonRef: PropTypes.object, onFocus: PropTypes.func, onBlur: PropTypes.func, diff --git a/client/components/ManageTestQueue/index.jsx b/client/components/ManageTestQueue/index.jsx index cd5aee9c2..77b4717ba 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -18,6 +18,7 @@ import { convertStringToDate } from '../../utils/formatter'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; import DisclosureComponent from '../common/DisclosureComponent'; import AddTestToQueueWithConfirmation from '../AddTestToQueueWithConfirmation'; +import RadioBox from '../common/RadioBox'; const DisclosureContainer = styled.div` // Following directives are related to the ManageTestQueue component @@ -28,11 +29,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 +76,38 @@ const DisclosureContainer = styled.div` .disclosure-row-test-plans { display: grid; - grid-auto-flow: column; - grid-template-columns: 1fr 1fr 1fr 1fr; - grid-gap: 1rem; + grid-template-columns: 1fr; + row-gap: 0.5rem; + + & > :nth-of-type(2) { + display: none; + } + & > :nth-of-type(5) { + grid-column: span 2; + } + + @media (min-width: 768px) { + grid-template-columns: 2fr 2fr 1fr; + column-gap: 2rem; + + & > :nth-of-type(2) { + display: block; + } + } + } + + .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 { @@ -139,6 +166,10 @@ const ManageTestQueue = ({ const [selectedAtId, setSelectedAtId] = useState(''); const [selectedBrowserId, setSelectedBrowserId] = useState(''); + const [selectAtVersionExactOrMinimum, setSelectedAtVersionExactOrMinimum] = + useState('Exact Version'); + const [selectedReportAtVersionId, setSelectedReportAtVersionId] = + useState(null); const [addAtVersion] = useMutation(ADD_AT_VERSION_MUTATION); const [editAtVersion] = useMutation(EDIT_AT_VERSION_MUTATION); @@ -307,6 +338,7 @@ const ManageTestQueue = ({ const onAtChange = e => { const { value } = e.target; setSelectedAtId(value); + setSelectedReportAtVersionId(null); }; const onBrowserChange = e => { @@ -314,6 +346,11 @@ const ManageTestQueue = ({ setSelectedBrowserId(value); }; + const onReportAtVersionIdChange = e => { + const { value } = e.target; + setSelectedReportAtVersionId(value); + }; + const onTestPlanVersionChange = e => { const { value } = e.target; setSelectedTestPlanVersionId(value); @@ -466,6 +503,10 @@ const ManageTestQueue = ({ setShowThemedModal(true); }; + const exactOrMinimumAtVersion = ats + .find(item => item.id === selectedAtId) + ?.atVersions.find(item => item.id === selectedReportAtVersionId); + return ( - Select an Assistive Technology and manage its + Select an assistive technology and manage its versions in the ARIA-AT App

@@ -551,8 +592,8 @@ const ManageTestQueue = ({ key={`manage-test-queue-add-test-plans-section`} > - Select a Test Plan and version and an Assistive - Technology and Browser to add it to the Test Queue + Select a test plan, assistive technology and browser + to add a new test plan report to the test queue.
@@ -616,6 +657,7 @@ const ManageTestQueue = ({ )} +
{/* blank grid cell */}
Assistive Technology @@ -637,6 +679,47 @@ const ManageTestQueue = ({ ))} + + + Assistive Technology Version + +
+ + setSelectedAtVersionExactOrMinimum( + exactOrMinimum + ) + } + /> + + + {ats + .find(at => at.id === selectedAtId) + ?.atVersions.map(item => ( + + ))} + +
+
Browser @@ -644,6 +727,7 @@ const ManageTestQueue = ({