diff --git a/cvat-ui/src/components/common/go-back-button.tsx b/cvat-ui/src/components/common/go-back-button.tsx index 5b6f2a60e2b..8dd05471538 100644 --- a/cvat-ui/src/components/common/go-back-button.tsx +++ b/cvat-ui/src/components/common/go-back-button.tsx @@ -12,7 +12,7 @@ function GoBackButton(): JSX.Element { const goBack = useGoBack(); return ( <> - Back diff --git a/tests/cypress/e2e/features/consensus.js b/tests/cypress/e2e/features/consensus.js index 8d9814e40fe..4049f5fe6b8 100644 --- a/tests/cypress/e2e/features/consensus.js +++ b/tests/cypress/e2e/features/consensus.js @@ -4,42 +4,60 @@ /// +import { translatePoints } from '../../support/utils'; + context('Basic manipulations with consensus job replicas', () => { - describe('Consensus task creation', () => { - const maxReplicas = 10; - const taskName = 'Test consensus'; - const labelName = 'test'; - const serverFiles = ['archive.zip']; - const replicas = 3; + const labelName = 'Consensus'; + const taskName = 'Test consensus'; + const serverFiles = ['archive.zip']; + const replicas = 4; + const taskSpec = { + name: taskName, + labels: [{ + name: labelName, + attributes: [], + type: 'any', + }], + }; + const dataSpec = { + server_files: serverFiles, + image_quality: 70, + }; + const extras = { consensus_replicas: replicas }; + + before(() => { + cy.visit('auth/login'); + cy.login(); + }); + describe('Consensus job creation', () => { + const maxReplicas = 10; + let consensusTaskID = null; before(() => { - cy.visit('auth/login'); - cy.login(); + cy.headlessCreateTask(taskSpec, dataSpec, extras).then(({ taskID }) => { + consensusTaskID = taskID; + }); cy.get('.cvat-create-task-dropdown').click(); cy.get('.cvat-create-task-button').should('be.visible').click(); }); - it('Check allowed number of replicas', () => { - // Fill the fields to create the task - cy.get('#name').type(taskName); - cy.addNewLabel({ name: labelName }); - cy.selectFilesFromShare(serverFiles); - cy.contains('[role="tab"]', 'My computer').click(); - cy.contains('Advanced configuration').click(); // 'Consensus Replicas' field cannot equal to 1 cy.get('#consensusReplicas').type(`{backspace}${1}`); - cy.get('.ant-form-item-explain-error') + cy.get('.ant-form-item-has-error') .should('be.visible') - .invoke('text').should('eq', 'Value can not be equal to 1'); + .should('include.text', 'Value can not be equal to 1'); cy.contains('button', 'Submit & Continue').click(); cy.get('.ant-notification-notice-error').should('exist').and('be.visible'); cy.closeNotification('.ant-notification-notice-error'); // 'Consensus Replicas' field cannot be > 10 + cy.get('#consensusReplicas').clear(); + cy.get('.ant-form-item-has-error').should('not.exist'); cy.get('#consensusReplicas').type(`{backspace}${maxReplicas + 1}`); - cy.get('.ant-form-item-explain-error').should('be.visible') - .invoke('text').should('eq', `Value must be less than ${maxReplicas}`); + cy.get('.ant-form-item-has-error') + .should('be.visible') + .should('include.text', `Value must be less than ${maxReplicas}`); cy.contains('button', 'Submit & Continue').click(); cy.get('.ant-notification-notice-error').should('exist').and('be.visible'); cy.closeNotification('.ant-notification-notice-error'); @@ -47,9 +65,8 @@ context('Basic manipulations with consensus job replicas', () => { }); it('Check new consensus task has correct tags and drop-down with replicas', () => { - // Create task with consensus - cy.get('#consensusReplicas').type(replicas); - cy.contains('button', 'Submit & Open').click(); + cy.goToTaskList(); + cy.openTask(taskName); cy.get('.cvat-task-details-wrapper').should('be.visible'); cy.get('.ant-notification-notice-error').should('not.exist'); // Check tags @@ -64,20 +81,159 @@ context('Basic manipulations with consensus job replicas', () => { expect($el.text()).to.equal(`${replicas} Replicas`); cy.wrap($el).click(); }); + }); + after(() => { + cy.headlessDeleteTask(consensusTaskID); + }); + }); + + describe('Cosensus jobs merging', () => { + let consensusTaskID = null; + const baseShape = { + objectType: 'shape', + labelName, + frame: 0, + type: 'rectangle', + points: [250, 64, 491, 228], + occluded: false, + }; + const jobIDs = []; + + before(() => { + cy.headlessCreateTask(taskSpec, dataSpec, extras).then(({ taskID }) => { + consensusTaskID = taskID; + }); + cy.goToTaskList(); + cy.openTask(taskName); + cy.get('.cvat-consensus-job-collapse').click(); + }); - // Check asc order of jobs + it("Check new merge buttons exist and are visible. Trying to merge 'new' jobs should trigger errors", () => { + // Check asc order of jobs in drop-down function parseJobId(jobItem) { const jobItemText = jobItem.innerText; const [start, stop] = [0, jobItemText.indexOf('\n')]; return +(jobItemText.substring(start, stop).split('#')[1]); } - cy.get('.cvat-job-item').then((jobItems) => { - const sourceJobId = parseJobId(jobItems[0]); - for (let i = 1; i <= replicas; i++) { - const jobId = parseJobId(jobItems[i]); - expect(jobId).equals(sourceJobId + i); - } + cy.get('.cvat-job-item').each(([$el], i) => { + const jobID = parseJobId($el); + jobIDs.push(jobID); + expect(jobID).equals(jobIDs[0] + i); }); + + // Merge one consensus job + cy.then(() => { + cy.mergeConsensusJob(jobIDs[0], 400); + }); + cy.get('.cvat-notification-notice-consensus-merge-task-failed') + .should('be.visible') + .invoke('text').should('include', 'Could not merge the job'); + cy.closeNotification('.cvat-notification-notice-consensus-merge-task-failed'); + + // Merge all consensus jobs in task + cy.mergeConsensusTask(400); + cy.get('.cvat-notification-notice-consensus-merge-task-failed') + .should('be.visible') + .invoke('text') + .should('include', 'Could not merge the task'); + cy.closeNotification('.cvat-notification-notice-consensus-merge-task-failed'); + }); + + it('Check consensus management page', () => { + const defaultQuorum = 50; + const defaultIoU = 40; + cy.contains('button', 'Actions').click(); + cy.contains('Consensus management').should('be.visible').click(); + cy.get('.cvat-consensus-management-inner').should('be.visible'); + // Save settings, confirm request is sent + cy.intercept('PATCH', 'api/consensus/settings/**').as('settingsMeta'); + cy.contains('button', 'Save').click(); + cy.wait('@settingsMeta'); + cy.get('.ant-notification-notice-message') + .should('be.visible') + .invoke('text') + .should('eq', 'Settings have been updated'); + cy.closeNotification('.ant-notification-notice-closable'); + + // Forms and invalid saving + function checkFieldValue(selector, value) { + return cy.get(selector).then(([$el]) => { + cy.wrap($el).invoke('val').should('eq', `${value}`); + return cy.wrap($el); + }); + } + function attemptInvalidSaving(errorsCount) { + cy.get('.ant-form-item-explain-error').should('be.visible') + .should('have.length', errorsCount) + .each(($el) => { + cy.wrap($el).should('have.text', 'This field is required'); + }); + cy.contains('button', 'Save').click(); + cy.closeNotification('.cvat-notification-save-consensus-settings-failed'); + } + checkFieldValue('#quorum', defaultQuorum).clear(); + attemptInvalidSaving(1); + checkFieldValue('#iouThreshold', defaultIoU).clear(); + attemptInvalidSaving(2); + cy.get('.ant-notification-notice').should('not.exist'); + + // Go back to task page + cy.get('.cvat-back-btn').should('be.visible').click(); + }); + + it('Create annotations and check that job replicas merge correctly', () => { + // Create annotations for job replicas + const delta = 50; + const [consensusJobID, ...replicaJobIDs] = jobIDs; + for (let i = 0, shape = baseShape; i < replicas; i++) { + cy.headlessCreateObjects([shape], jobIDs[i]); // only 'in progress' jobs can be merged + cy.headlessUpdateJob(replicaJobIDs[i], { state: 'in progress' }); + const points = translatePoints(shape.points, delta, 'x'); + shape = { ...shape, points }; + } + // Merging of consensus job should go without errors in network and UI + cy.mergeConsensusJob(consensusJobID); + cy.get('.cvat-notification-notice-consensus-merge-job-failed').should('not.exist'); + cy.get('.ant-notification-notice-message') + .should('be.visible') + .invoke('text') + .should('eq', `Consensus job #${consensusJobID} has been merged`); + cy.closeNotification('.ant-notification-notice-closable'); + + // Shapes in consensus job and a job replica in the middle should be equal + const middle = Math.floor(jobIDs.length / 2); + const consensusRect = {}; + cy.openJob(0, false).then(() => { + cy.get('.cvat_canvas_shape').trigger('mousemove'); + cy.get('.cvat_canvas_shape').then(($el) => { + consensusRect.x = $el.attr('x'); + consensusRect.y = $el.attr('y'); + consensusRect.width = $el.attr('width'); + consensusRect.height = $el.attr('height'); + }); + cy.get('#cvat_canvas_text_content').should('be.visible') + .invoke('text') + .should('include', `${labelName}`) + .and('include', 'consensus'); + }); + cy.go('back'); // go to previous page + // After returning to task page, consensus job should be 'completed' + cy.get('.cvat-job-item').first() + .find('.cvat-job-item-state').first() + .invoke('text') + .should('eq', 'completed'); + cy.contains('.cvat-job-item', `Job #${jobIDs[middle]}`).scrollIntoView(); + cy.openJob(middle, false).then(() => { + cy.get('.cvat_canvas_shape').then(($el) => { + expect($el.attr('x')).to.equal(consensusRect.x); + expect($el.attr('y')).to.equal(consensusRect.y); + expect($el.attr('width')).to.equal(consensusRect.width); + expect($el.attr('height')).to.equal(consensusRect.height); + }); + }); + }); + after(() => { + cy.headlessDeleteTask(consensusTaskID); }); }); }); diff --git a/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js index 51a599672aa..352792431db 100644 --- a/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js +++ b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js @@ -4,6 +4,8 @@ /// +import { translatePoints } from '../../support/utils'; + const taskName = '5frames'; const labelName = 'label'; const attrName = 'attr1'; @@ -27,26 +29,6 @@ const rect = [ 30 + 23, ]; -function translatePoints(points, delta, axis) { - if (axis === 'x') { - return [ - points[0] + delta, - points[1], - points[2] + delta, - points[3], - ]; - } - if (axis === 'y') { - return [ - points[0], - points[1] + delta, - points[2], - points[3] + delta, - ]; - } - return points; -} - context('Create any track, check if track works correctly after deleting some frames', () => { function readShapeCoords() { return cy.get('.cvat_canvas_shape').then(($shape) => ({ diff --git a/tests/cypress/plugins/createZipArchive/addPlugin.js b/tests/cypress/plugins/createZipArchive/addPlugin.js index 882b6c5c037..a2f468e5bdb 100644 --- a/tests/cypress/plugins/createZipArchive/addPlugin.js +++ b/tests/cypress/plugins/createZipArchive/addPlugin.js @@ -2,17 +2,21 @@ // // SPDX-License-Identifier: MIT -// eslint-disable-next-line no-use-before-define +/* eslint-disable + import/no-extraneous-dependencies, + security/detect-non-literal-fs-filename, + no-use-before-define +*/ + exports.createZipArchive = createZipArchive; const archiver = require('archiver'); -// eslint-disable-next-line import/no-extraneous-dependencies const fs = require('fs-extra'); function createZipArchive(args) { - const { directoryToArchive } = args; + const { directoryToArchive, archivePath } = args; const { level } = args; - const output = fs.createWriteStream(args.arhivePath); + const output = fs.createWriteStream(archivePath); const archive = archiver('zip', { gzip: true, zlib: { level }, @@ -27,5 +31,5 @@ function createZipArchive(args) { archive.directory(`${directoryToArchive}/`, false); archive.finalize(); - return fs.pathExists(archive); + return fs.pathExists(archivePath); } diff --git a/tests/cypress/plugins/createZipArchive/createZipArchiveCommand.js b/tests/cypress/plugins/createZipArchive/createZipArchiveCommand.js index 5b27347906d..906ab689fbf 100644 --- a/tests/cypress/plugins/createZipArchive/createZipArchiveCommand.js +++ b/tests/cypress/plugins/createZipArchive/createZipArchiveCommand.js @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: MIT -Cypress.Commands.add('createZipArchive', (directoryToArchive, arhivePath, level = 9) => cy.task('createZipArchive', { +Cypress.Commands.add('createZipArchive', (directoryToArchive, archivePath, level = 9) => cy.task('createZipArchive', { directoryToArchive, - arhivePath, + archivePath, level, })); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 5973d10c709..d0f36c47641 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -450,6 +450,14 @@ Cypress.Commands.add('headlessCreateJob', (jobSpec) => { }); }); +Cypress.Commands.add('headlessUpdateJob', (jobID, updateJobParameters) => { + cy.window().then(async ($win) => { + const job = (await $win.cvat.jobs.get({ jobID }))[0]; + const result = await job.save(updateJobParameters); + return cy.wrap(result); + }); +}); + Cypress.Commands.add('openTask', (taskName, projectSubsetFieldValue) => { cy.contains('strong', new RegExp(`^${taskName}$`)) .parents('.cvat-tasks-list-item') @@ -1833,3 +1841,31 @@ Cypress.Commands.add('applyActionToSliders', (wrapper, slidersClassNames, action }); cy.get('.ant-tooltip').invoke('hide'); }); + +Cypress.Commands.add('mergeConsensusTask', (status = 202) => { + cy.intercept('POST', '/api/consensus/merges**').as('mergeTask'); + + cy.get('.cvat-task-details-wrapper').should('be.visible'); + cy.contains('button', 'Actions').click(); + cy.contains('Merge consensus jobs').should('be.visible').click(); + cy.get('.cvat-modal-confirm-consensus-merge-task') + .contains('button', 'Merge') + .click(); + + cy.wait('@mergeTask').its('response.statusCode').should('eq', status); +}); + +Cypress.Commands.add('mergeConsensusJob', (jobID, status = 202) => { + cy.intercept('POST', '/api/consensus/merges**').as('mergeJob'); + cy.get('.cvat-job-item') + .filter(':has(.cvat-tag-consensus)') + .filter(`:contains("Job #${jobID}")`) + .find('.anticon-more').first().click(); + + cy.get('.ant-dropdown-menu').contains('li', 'Merge consensus job').click(); + cy.get('.cvat-modal-confirm-consensus-merge-job') + .contains('button', 'Merge') + .click(); + + cy.wait('@mergeJob').its('response.statusCode').should('eq', status); +}); diff --git a/tests/cypress/support/utils.js b/tests/cypress/support/utils.js index 441fae14497..12cd0f61ac2 100644 --- a/tests/cypress/support/utils.js +++ b/tests/cypress/support/utils.js @@ -21,3 +21,23 @@ export function decomposeMatrix(matrix) { const skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90).toFixed(1); return skewX; } + +export function translatePoints(points, delta, axis) { + if (axis === 'x') { + return [ + points[0] + delta, + points[1], + points[2] + delta, + points[3], + ]; + } + if (axis === 'y') { + return [ + points[0], + points[1] + delta, + points[2], + points[3] + delta, + ]; + } + return points; +}