// Copyright (C) CVAT.ai Corporation // // SPDX-License-Identifier: MIT /// context('Manipulations with masks', { scrollBehavior: false }, () => { const taskName = 'Basic actions with masks'; const serverFiles = ['images/image_1.jpg', 'images/image_2.jpg', 'images/image_3.jpg']; const drawingActions = [{ method: 'brush', coordinates: [[300, 300], [700, 300], [700, 700], [300, 700]], }, { method: 'polygon-plus', coordinates: [[450, 210], [650, 400], [450, 600], [260, 400]], }, { method: 'brush-size', value: 150, }, { method: 'eraser', coordinates: [[500, 500]], }, { method: 'brush-size', value: 10, }, { method: 'polygon-minus', coordinates: [[450, 400], [600, 400], [450, 550], [310, 400]], }]; const editingActions = [{ method: 'polygon-minus', coordinates: [[50, 400], [800, 400], [800, 800], [50, 800]], }]; let taskID = null; let jobID = null; before(() => { cy.visit('/auth/login'); cy.login(); cy.headlessCreateTask({ labels: [{ name: 'mask label', attributes: [], type: 'any' }], name: taskName, project_id: null, source_storage: { location: 'local' }, target_storage: { location: 'local' }, }, { server_files: serverFiles, image_quality: 70, use_zip_chunks: true, use_cache: true, sorting_method: 'lexicographical', }).then((response) => { taskID = response.taskID; [jobID] = response.jobIDs; }).then(() => { cy.visit(`/tasks/${taskID}/jobs/${jobID}`); cy.get('.cvat-canvas-container').should('exist').and('be.visible'); }); }); after(() => { cy.logout(); cy.getAuthKey().then((response) => { const authKey = response.body.key; cy.request({ method: 'DELETE', url: `/api/tasks/${taskID}`, headers: { Authorization: `Token ${authKey}`, }, }); }); }); describe('Tests to make sure that basic features work with masks', () => { beforeEach(() => { cy.removeAnnotations(); cy.goCheckFrameNumber(0); }); it('Drawing a couple of masks. Save job, reopen job, masks must exist', () => { cy.startMaskDrawing(); cy.drawMask(drawingActions); cy.get('.cvat-brush-tools-finish').click(); cy.get('.cvat-brush-tools-continue').click(); cy.get('.cvat-brush-tools-toolbox').should('exist').and('be.visible'); cy.get('#cvat_canvas_shape_1').should('exist').and('be.visible'); // it is expected, that after clicking "continue", brush tools are still opened cy.drawMask(drawingActions); cy.finishMaskDrawing(); cy.get('.cvat-brush-tools-toolbox').should('not.be.visible'); cy.saveJob(); cy.reload(); for (const id of [1, 2]) { cy.get(`#cvat_canvas_shape_${id}`).should('exist').and('be.visible'); } cy.removeAnnotations(); }); it('Propagate mask to another frame', () => { cy.startMaskDrawing(); cy.drawMask(drawingActions); cy.finishMaskDrawing(); cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); cy.get('.cvat-propagate-confirm-up-to-input').find('input') .should('have.attr', 'value', serverFiles.length - 1); cy.contains('button', 'Yes').click(); for (let i = 1; i < serverFiles.length; i++) { cy.goCheckFrameNumber(i); cy.get('.cvat_canvas_shape').should('exist').and('be.visible'); } }); it('Copy mask to another frame', () => { cy.startMaskDrawing(); cy.drawMask(drawingActions); cy.finishMaskDrawing(); cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Make a copy'); cy.goCheckFrameNumber(serverFiles.length - 1); cy.get('.cvat-canvas-container').click(); cy.get('#cvat_canvas_shape_2').should('exist').and('be.visible'); }); it('Check hidden mask still invisible after changing frame/opacity', () => { cy.startMaskDrawing(); cy.drawMask(drawingActions); cy.finishMaskDrawing(); cy.get('#cvat-objects-sidebar-state-item-1').within(() => { cy.get('.cvat-object-item-button-hidden') .should('exist').and('be.visible').click(); cy.get('.cvat-object-item-button-hidden') .should('have.class', 'cvat-object-item-button-hidden-enabled'); }); cy.goCheckFrameNumber(serverFiles.length - 1); cy.goCheckFrameNumber(0); cy.get('.cvat-appearance-opacity-slider').click('right'); cy.get('.cvat-appearance-opacity-slider').click('center'); cy.get('#cvat_canvas_shape_1') .should('exist').and('have.class', 'cvat_canvas_hidden').and('not.be.visible'); }); it('Editing a drawn mask', () => { cy.startMaskDrawing(); cy.drawMask(drawingActions); cy.finishMaskDrawing(); cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); cy.drawMask(editingActions); // Check issue fixed in https://github.com/cvat-ai/cvat/pull/8598 // Frames navigation should not work during editing cy.get('.cvat-player-next-button').click(); cy.checkFrameNum(0); cy.finishMaskDrawing(); }); it('Underlying pixels are removed on enabling "Remove underlying pixels" tool', () => { const mask1 = [{ method: 'brush', coordinates: [[450, 250], [600, 400]], }]; const mask2 = [{ method: 'brush', coordinates: [[450, 250], [525, 325]], }]; cy.startMaskDrawing(); cy.drawMask(mask1); cy.get('.cvat-brush-tools-continue').click(); cy.drawMask(mask2); cy.get('.cvat-brush-tools-underlying-pixels').click(); cy.get('.cvat-brush-tools-underlying-pixels').should('have.class', 'cvat-brush-tools-active-tool'); cy.finishMaskDrawing(); cy.get('#cvat-objects-sidebar-state-item-2').within(() => { cy.get('.cvat-object-item-button-hidden').click(); }); cy.get('.cvat-canvas-container').then(([$canvas]) => { cy.wrap($canvas).trigger('mousemove', { clientX: 450, clientY: 250 }); cy.get('#cvat_canvas_shape_1').should('not.have.class', 'cvat_canvas_shape_activated'); cy.wrap($canvas).trigger('mousemove', { clientX: 550, clientY: 350 }); cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); }); cy.startMaskDrawing(); cy.get('.cvat-brush-tools-underlying-pixels').click(); cy.get('.cvat-brush-tools-underlying-pixels').should('not.have.class', 'cvat-brush-tools-active-tool'); cy.finishMaskDrawing(); }); it('Check brush tools shortcuts', () => { const mask1 = [{ method: 'brush', coordinates: [[450, 250], [600, 400]], }]; cy.startMaskDrawing(); cy.drawMask(mask1); cy.get('.cvat-brush-tools-polygon-minus').click(); cy.get('.cvat-brush-tools-polygon-minus').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('body').type('{shift}{1}'); cy.get('.cvat-brush-tools-brush').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('body').type('{shift}{2}'); cy.get('.cvat-brush-tools-eraser').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('body').type('{shift}{3}'); cy.get('.cvat-brush-tools-polygon-plus').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('body').type('{shift}{4}'); cy.get('.cvat-brush-tools-polygon-minus').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('.cvat-brush-tools-finish').trigger('mouseover'); cy.get('.cvat-brush-tools-finish').trigger('mouseout'); cy.get('body').type('n'); cy.get('.cvat-brush-tools-toolbox').should('not.be.visible'); }); it('Check hide mask feature', () => { function checkHideFeature() { cy.get('.cvat-brush-tools-hide').click(); cy.get('.cvat-brush-tools-hide').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('.cvat_masks_canvas_wrapper').should('not.be.visible'); cy.get('.cvat-brush-tools-hide').click(); cy.get('.cvat_masks_canvas_wrapper').should('be.visible'); } function checkHideShortcut() { cy.get('body').type('h'); cy.get('.cvat-brush-tools-hide').should('have.class', 'cvat-brush-tools-active-tool'); cy.get('.cvat_masks_canvas_wrapper').should('not.be.visible'); } function checkObjectIsHidden() { cy.get('#cvat-objects-sidebar-state-item-1').within(() => { cy.get('.cvat-object-item-button-hidden-enabled').should('exist'); }); } const mask = [{ method: 'brush', coordinates: [[450, 250], [600, 400]], }]; const drawPolygon = [{ method: 'polygon-plus', coordinates: [[450, 210], [650, 400], [450, 600], [260, 400]], }]; cy.startMaskDrawing(); cy.drawMask(mask); checkHideFeature(); checkHideShortcut(); cy.finishMaskDrawing(); cy.get('#cvat_canvas_shape_1').should('be.visible'); cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); checkHideFeature(); cy.drawMask(drawPolygon); checkHideShortcut(); cy.get('.cvat_canvas_shape_drawing') .invoke('attr', 'fill-opacity') .then((opacity) => expect(+opacity).to.be.equal(0)); checkObjectIsHidden(); cy.get('.cvat-brush-tools-brush').click(); cy.get('.cvat-brush-tools-brush').should('have.class', 'cvat-brush-tools-active-tool'); cy.finishMaskDrawing(); checkObjectIsHidden(); }); }); describe('Tests to make sure that empty masks cannot be created', () => { beforeEach(() => { cy.removeAnnotations(); cy.saveJob('PUT'); }); function checkEraseTools(baseTool = '.cvat-brush-tools-brush', disabled = true) { cy.get(baseTool).should('have.class', 'cvat-brush-tools-active-tool'); const condition = disabled ? 'be.disabled' : 'not.be.disabled'; cy.get('.cvat-brush-tools-eraser').should(condition); cy.get('.cvat-brush-tools-polygon-minus').should(condition); } function checkMaskNotEmpty(selector) { cy.get(selector).should('exist').and('be.visible'); cy.get(selector) .should('have.attr', 'height') .then((height) => { expect(+height).to.be.gt(1); }); cy.get(selector) .should('have.attr', 'width') .then((width) => { expect(+width).to.be.gt(1); }); } it('Erase tools are locked when nothing to erase', () => { const erasedMask = [{ method: 'brush', coordinates: [[450, 250], [600, 400], [450, 550], [300, 400]], }, { method: 'polygon-minus', coordinates: [[100, 100], [700, 100], [700, 700], [100, 700]], }]; cy.startMaskDrawing(); checkEraseTools(); cy.drawMask(erasedMask); cy.get('.cvat-brush-tools-brush').click(); checkEraseTools(); cy.finishMaskDrawing(); cy.get('#cvat_canvas_shape_1').should('not.exist'); }); it('Drawing a mask, finish with erasing tool. On new mask drawing tool is reset', () => { const masks = [[{ method: 'brush', coordinates: [[450, 250], [600, 400], [450, 550], [300, 400]], }, { method: 'polygon-minus', coordinates: [[100, 100], [400, 100], [400, 400], [100, 400]], }], [{ method: 'brush', coordinates: [[550, 350], [700, 500], [550, 650], [400, 500]], }, { method: 'eraser', coordinates: [[550, 350]], }]]; for (const [index, mask] of masks.entries()) { cy.startMaskDrawing(); cy.drawMask(mask); cy.finishMaskDrawing(); cy.get(`#cvat_canvas_shape_${index + 1}`).should('exist').and('be.visible'); cy.startMaskDrawing(); checkEraseTools(); cy.finishMaskDrawing(); } }); it('Empty masks are deleted using remove underlying pixels feature', () => { const masks = [[{ method: 'brush', coordinates: [[150, 150], [270, 270]], }], [{ method: 'brush', coordinates: [[350, 350], [370, 370]], }], [{ method: 'polygon-plus', coordinates: [[100, 100], [400, 100], [400, 400], [100, 400]], }]]; cy.startMaskDrawing(); cy.get('.cvat-brush-tools-underlying-pixels').click(); cy.get('.cvat-brush-tools-underlying-pixels').should('have.class', 'cvat-brush-tools-active-tool'); cy.finishMaskDrawing(); for (const [index, mask] of masks.entries()) { cy.startMaskDrawing(); cy.drawMask(mask); cy.finishMaskDrawing(); cy.get(`#cvat_canvas_shape_${index + 1}`).should('exist').and('be.visible'); } // Fist mask is updated, second mask is removed after third mask is drawn cy.contains('Some objects were deleted').should('exist').and('be.visible'); for (const id of [1, 3]) { cy.get(`#cvat_canvas_shape_${id}`).should('exist').and('be.visible'); } cy.get('#cvat_canvas_shape_2').should('not.exist'); cy.saveJob('PATCH', 200, 'removeUnderlyingPixelsUndoRedo'); // Undo creating mask, second mask is restored cy.contains('.cvat-annotation-header-button', 'Undo').click(); for (const id of [1, 2]) { cy.get(`#cvat_canvas_shape_${id}`).should('exist').and('be.visible'); } cy.saveJob('PATCH', 200, 'removeUnderlyingPixelsUndoRedo'); // Redo creating mask, second mask is removed cy.contains('.cvat-annotation-header-button', 'Redo').click(); for (const id of [1, 3]) { cy.get(`#cvat_canvas_shape_${id}`).should('exist').and('be.visible'); } cy.get('#cvat_canvas_shape_2').should('not.exist'); cy.saveJob('PATCH', 200, 'removeUnderlyingPixelsUndoRedo'); cy.get('.cvat-notification-notice-save-annotations-failed').should('not.exist'); }); it('Erasing a mask during editing is not allowed', () => { const mask = [{ method: 'brush', coordinates: [[450, 250], [600, 400], [450, 550], [300, 400]], }]; const eraseAction = [{ method: 'polygon-minus', coordinates: [[100, 100], [700, 100], [700, 700], [100, 700]], }]; cy.startMaskDrawing(); cy.drawMask(mask); cy.finishMaskDrawing(); cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); cy.drawMask(eraseAction); cy.finishMaskDrawing(); checkMaskNotEmpty('#cvat_canvas_shape_1'); }); }); });