diff --git a/.drone.yml b/.drone.yml index aad7f66e4..5b705d80f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -95,7 +95,7 @@ steps: CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB YARN_CACHE_FOLDER: .cache/yarn/ CYPRESS_CACHE_FOLDER: .cache/cypress/ - CYPRESS_DEFAULT_COMMAND_TIMEOUT: 10000 + CYPRESS_DEFAULT_COMMAND_TIMEOUT: 20000 commands: - sed -i 's/localhost/api/g' public/index.html - yarn serve & npx wait-on http://localhost:8080 diff --git a/cypress.json b/cypress.json index 9497a81ea..b7319d941 100644 --- a/cypress.json +++ b/cypress.json @@ -4,5 +4,8 @@ "API_URL": "http://localhost:3456/api/v1", "TEST_SECRET": "testingS3cr3et" }, - "video": false + "video": false, + "retries": { + "runMode": 2 + } } diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js index 4befee509..3c8c0d6fc 100644 --- a/cypress/integration/list/list.spec.js +++ b/cypress/integration/list/list.spec.js @@ -10,10 +10,12 @@ import {BucketFactory} from '../../factories/bucket' import '../../support/authenticateUser' describe('Lists', () => { + let lists + beforeEach(() => { UserFactory.create(1) NamespaceFactory.create(1) - const lists = ListFactory.create(1, { + lists = ListFactory.create(1, { title: 'First List' }) TaskFactory.truncate() @@ -54,6 +56,64 @@ describe('Lists', () => { .should('contain', '/lists/1/kanban') }) + it('Should rename the list in all places', () => { + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + }) + const newListName = 'New list name' + + cy.visit('/lists/1') + cy.get('.list-title h1') + .should('contain', 'First List') + + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger') + .click() + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content') + .contains('Edit') + .click() + cy.get('#listtext') + .type(`{selectall}${newListName}`) + cy.get('footer.modal-card-foot .button') + .contains('Save') + .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.get('.list-title h1') + .should('contain', newListName) + .should('not.contain', lists[0].title) + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child') + .should('contain', newListName) + .should('not.contain', lists[0].title) + cy.visit('/') + cy.get('.card-content .tasks') + .should('contain', newListName) + .should('not.contain', lists[0].title) + }) + + it('Should remove a list', () => { + cy.visit(`/lists/${lists[0].id}`) + + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger') + .click() + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content') + .contains('Delete') + .click() + cy.url() + .should('contain', '/settings/delete') + cy.get('.modal-mask .modal-container .modal-content .actions a.button') + .contains('Do it') + .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list') + .should('not.contain', lists[0].title) + cy.location('pathname') + .should('equal', '/') + }) + describe('List View', () => { it('Should be an empty list', () => { cy.visit('/lists/1') @@ -202,11 +262,15 @@ describe('Lists', () => { it('Shows tasks from the current and next month', () => { const now = new Date() + const nextMonth = now + nextMonth.setDate(1) + nextMonth.setMonth(now.getMonth() + 1) + cy.visit('/lists/1/gantt') cy.get('.gantt-chart-container .gantt-chart .months') .should('contain', format(now, 'MMMM')) - .should('contain', format(now.setMonth(now.getMonth() + 1), 'MMMM')) + .should('contain', format(nextMonth, 'MMMM')) }) it('Shows tasks with dates', () => { @@ -401,7 +465,7 @@ describe('Lists', () => { }) cy.visit('/lists/1/kanban') - cy.getAttached('.kanban .bucket .tasks .task') + cy.getSettled('.kanban .bucket .tasks .task') .contains(tasks[0].title) .should('be.visible') .click() @@ -423,7 +487,7 @@ describe('Lists', () => { const task = tasks[0] cy.visit('/lists/1/kanban') - cy.getAttached('.kanban .bucket .tasks .task') + cy.getSettled('.kanban .bucket .tasks .task') .contains(task.title) .should('be.visible') .click() diff --git a/cypress/integration/list/namespaces.spec.js b/cypress/integration/list/namespaces.spec.js index 9dba7b431..dbdba9156 100644 --- a/cypress/integration/list/namespaces.spec.js +++ b/cypress/integration/list/namespaces.spec.js @@ -3,6 +3,7 @@ import {UserFactory} from '../../factories/user' import '../../support/authenticateUser' import {ListFactory} from '../../factories/list' import {NamespaceFactory} from '../../factories/namespace' +import {TaskFactory} from '../../factories/task' describe('Namepaces', () => { let namespaces @@ -20,20 +21,82 @@ describe('Namepaces', () => { }) it('Should create a new Namespace', () => { + const newNamespaceTitle = 'New Namespace' + cy.visit('/namespaces') cy.get('a.button') .contains('Create namespace') .click() + cy.url() .should('contain', '/namespaces/new') cy.get('.card-header-title') .should('contain', 'Create a new namespace') cy.get('input.input') - .type('New Namespace') + .type(newNamespaceTitle) cy.get('.button') .contains('Create') .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.get('.namespace-container') + .should('contain', newNamespaceTitle) cy.url() .should('contain', '/namespaces') }) + + it('Should rename the namespace all places', () => { + const newNamespaces = NamespaceFactory.create(5) + const newNamespaceName = 'New namespace name' + + cy.visit('/namespaces') + + cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`) + .click() + cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content') + .contains('Edit') + .click() + cy.url() + .should('contain', '/settings/edit') + cy.get('#namespacetext') + .invoke('val') + .should('equal', newNamespaces[0].title) // wait until the namespace data is loaded + cy.get('#namespacetext') + .type(`{selectall}${newNamespaceName}`) + cy.get('footer.modal-card-foot .button') + .contains('Save') + .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.get('.namespace-container .menu.namespaces-lists') + .should('contain', newNamespaceName) + .should('not.contain', newNamespaces[0].title) + cy.get('.content.namespaces-list') + .should('contain', newNamespaceName) + .should('not.contain', newNamespaces[0].title) + }) + + it('Should remove a namespace when deleting it', () => { + const newNamespaces = NamespaceFactory.create(5) + + cy.visit('/') + + cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`) + .click() + cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content') + .contains('Delete') + .click() + cy.url() + .should('contain', '/settings/delete') + cy.get('.modal-mask .modal-container .modal-content .actions a.button') + .contains('Do it') + .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.get('.namespace-container .menu.namespaces-lists') + .should('not.contain', newNamespaces[0].title) + }) }) diff --git a/cypress/integration/task/task.spec.js b/cypress/integration/task/task.spec.js index 34967d10e..e07936375 100644 --- a/cypress/integration/task/task.spec.js +++ b/cypress/integration/task/task.spec.js @@ -11,6 +11,7 @@ import '../../support/authenticateUser' import {TaskAssigneeFactory} from '../../factories/task_assignee' import {LabelFactory} from '../../factories/labels' import {LabelTaskFactory} from '../../factories/label_task' +import {BucketFactory} from '../../factories/bucket' describe('Task', () => { let namespaces @@ -112,9 +113,10 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .heading .is-done') - .should('exist') + .should('be.visible') .should('contain', 'Done') cy.get('.task-view .action-buttons p.created') + .should('be.visible') .should('contain', 'Done') }) @@ -182,9 +184,11 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .comments .media.comment .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll') + .should('be.visible') .type('{selectall}New Comment') cy.get('.task-view .comments .media.comment .button:not([disabled])') .contains('Comment') + .should('be.visible') .click() cy.get('.task-view .comments .media.comment .editor') @@ -195,6 +199,9 @@ describe('Task', () => { it('Can move a task to another list', () => { const lists = ListFactory.create(2) + BucketFactory.create(2, { + list_id: '{increment}' + }) const tasks = TaskFactory.create(1, { id: 1, list_id: lists[0].id, @@ -228,6 +235,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .action-buttons .button') + .should('be.visible') .contains('Delete task') .click() cy.get('.modal-mask .modal-container .modal-content .header') @@ -310,6 +318,7 @@ describe('Task', () => { cy.get('.task-view .action-buttons .button') .contains('Add labels') + .should('be.visible') .click() cy.get('.task-view .details.labels-list .multiselect input') .type(newLabelText) @@ -365,6 +374,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .details.labels-list .multiselect .input-wrapper') + .should('be.visible') .should('contain', labels[0].title) cy.get('.task-view .details.labels-list .multiselect .input-wrapper') .children() diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ccdc6f366..4bc18b684 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,17 +1,33 @@ /** - * getAttached(selector) - * getAttached(selectorFn) + * Recursively gets an element, returning only after it's determined to be attached to the DOM for good. * - * Waits until the selector finds an attached element, then yields it (wrapped). - * selectorFn, if provided, is passed $(document). Don't use cy methods inside selectorFn. - * - * Source: https://github.com/cypress-io/cypress/issues/5743#issuecomment-650421731 + * Source: https://github.com/cypress-io/cypress/issues/7306#issuecomment-850621378 */ -Cypress.Commands.add('getAttached', selector => { - const getElement = typeof selector === 'function' ? selector : $d => $d.find(selector); - let $el = null; - return cy.document().should($d => { - $el = getElement(Cypress.$($d)); - expect(Cypress.dom.isDetached($el)).to.be.false; - }).then(() => cy.wrap($el)); -}); +Cypress.Commands.add('getSettled', (selector, opts = {}) => { + const retries = opts.retries || 3 + const delay = opts.delay || 100 + + const isAttached = (resolve, count = 0) => { + const el = Cypress.$(selector) + + // is element attached to the DOM? + count = Cypress.dom.isAttached(el) ? count + 1 : 0 + + // hit our base case, return the element + if (count >= retries) { + return resolve(el) + } + + // retry after a bit of a delay + setTimeout(() => isAttached(resolve, count), delay) + } + + // wrap, so we can chain cypress commands off the result + return cy.wrap(null).then(() => { + return new Cypress.Promise((resolve) => { + return isAttached(resolve, 0) + }).then((el) => { + return cy.wrap(el) + }) + }) +}) diff --git a/package.json b/package.json index 75a82b633..3274a0ef8 100644 --- a/package.json +++ b/package.json @@ -17,16 +17,16 @@ "bulma": "0.9.2", "camel-case": "4.1.2", "copy-to-clipboard": "3.3.1", - "date-fns": "2.21.3", - "dompurify": "2.2.8", - "highlight.js": "10.7.2", + "date-fns": "2.22.1", + "dompurify": "2.2.9", + "highlight.js": "11.0.0", "lodash": "4.17.21", - "marked": "2.0.5", + "marked": "2.0.7", "register-service-worker": "1.7.2", "sass": "1.34.0", "snake-case": "3.0.4", "verte": "0.0.12", - "vue": "2.6.12", + "vue": "2.6.13", "vue-advanced-cropper": "1.6.0", "vue-drag-resize": "1.5.4", "vue-easymde": "1.4.0", @@ -49,14 +49,14 @@ "cypress": "7.4.0", "cypress-file-upload": "5.0.7", "eslint": "7.27.0", - "eslint-plugin-vue": "7.9.0", + "eslint-plugin-vue": "7.10.0", "faker": "5.5.3", - "jest": "27.0.1", + "jest": "27.0.3", "sass-loader": "10.2.0", "vue-flatpickr-component": "8.1.6", "vue-notification": "1.3.20", "vue-router": "3.5.1", - "vue-template-compiler": "2.6.12", + "vue-template-compiler": "2.6.13", "wait-on": "5.3.0" }, "eslintConfig": { diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index 2918915b4..cee4fadbd 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -20,6 +20,8 @@ > + + @@ -43,10 +45,11 @@ import {mapState} from 'vuex' import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types' import Navigation from '@/components/home/navigation' +import QuickActions from '@/components/quick-actions/quick-actions' export default { name: 'contentAuth', - components: {Navigation}, + components: {QuickActions, Navigation}, watch: { '$route': 'doStuffAfterRoute', }, @@ -83,7 +86,7 @@ export default { this.$route.name === 'user.settings' || this.$route.name === 'namespaces.index' ) { - this.$store.commit(CURRENT_LIST, {}) + this.$store.commit(CURRENT_LIST, null) } }, renewTokenOnFocus() { diff --git a/src/components/home/topNavigation.vue b/src/components/home/topNavigation.vue index fed0f0a4e..5a475f154 100644 --- a/src/components/home/topNavigation.vue +++ b/src/components/home/topNavigation.vue @@ -37,6 +37,14 @@