import { fireEvent, screen, waitFor, waitForElementToBeRemoved, within, } from '@testing-library/react' import { expect } from 'chai' import fetchMock from 'fetch-mock' import sinon from 'sinon' import ProjectListRoot from '../../../../../frontend/js/features/project-list/components/project-list-root' import { renderWithProjectListContext } from '../helpers/render-with-context' import * as eventTracking from '@/infrastructure/event-tracking' import { projectsData, owner, archivedProjects, makeLongProjectList, archiveableProject, copyableProject, } from '../fixtures/projects-data' import { location } from '@/shared/components/location' import getMeta from '@/utils/meta' const { fullList, currentList, archivedList, trashedList, leavableList, deletableList, } = makeLongProjectList(40) const userId = owner.id describe('', function () { this.timeout('10s') let sendMBSpy: sinon.SinonSpy beforeEach(async function () { global.localStorage.clear() sendMBSpy = sinon.spy(eventTracking, 'sendMB') this.tagId = '999fff999fff' this.tagName = 'First tag name' window.metaAttributesCache.set('ol-tags', [ { _id: this.tagId, name: this.tagName, project_ids: [projectsData[0].id, projectsData[1].id], }, ]) Object.assign(getMeta('ol-ExposedSettings'), { templateLinks: [], }) window.metaAttributesCache.set('ol-userEmails', [ { email: 'test@overleaf.com', default: true }, ]) // we need a blank user here since its used in checking if we should display certain ads window.metaAttributesCache.set('ol-user', {}) window.metaAttributesCache.set('ol-user_id', userId) window.metaAttributesCache.set('ol-footer', { showThinFooter: false, translatedLanguages: { en: 'English' }, subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } }, }) window.metaAttributesCache.set('ol-navbar', { items: [], }) this.locationWrapperSandbox = sinon.createSandbox() this.locationWrapperStub = this.locationWrapperSandbox.stub(location) }) afterEach(function () { sendMBSpy.restore() fetchMock.removeRoutes().clearHistory() this.locationWrapperSandbox.restore() }) describe('welcome page', function () { beforeEach(async function () { renderWithProjectListContext(, { projects: [], }) await fetchMock.callHistory.flush(true) }) it('the welcome page is displayed', async function () { await screen.findByRole('heading', { name: 'Welcome to Overleaf' }) }) it('the email confirmation alert is not displayed', async function () { expect( screen.queryByText( 'Please confirm your primary email address test@overleaf.com by clicking on the link in the confirmation email.' ) ).to.be.null }) }) describe('project table', function () { beforeEach(async function () { const { unmount } = renderWithProjectListContext(, { projects: fullList, }) this.unmount = unmount await fetchMock.callHistory.flush(true) await screen.findByRole('table') }) describe('checkboxes', function () { let allCheckboxes: Array = [] let actionsToolbar: HTMLElement let project1Id: string | null, project2Id: string | null describe('all projects', function () { beforeEach(function () { allCheckboxes = screen.getAllByRole('checkbox') // first one is the select all checkbox fireEvent.click(allCheckboxes[1]) fireEvent.click(allCheckboxes[2]) project1Id = allCheckboxes[1].getAttribute('data-project-id') project2Id = allCheckboxes[2].getAttribute('data-project-id') actionsToolbar = screen.getAllByRole('toolbar')[0] }) it('downloads all selected projects and then unselects them', async function () { const downloadButton = within(actionsToolbar).getByLabelText('Download') fireEvent.click(downloadButton) const assignStub = this.locationWrapperStub.assign await waitFor(() => { expect(assignStub).to.have.been.called }) sinon.assert.calledWithMatch( assignStub, `/project/download/zip?project_ids=${project1Id},${project2Id}` ) const allCheckboxes = screen.getAllByRole('checkbox') const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) expect(allCheckboxesChecked.length).to.equal(0) }) it('opens archive modal for all selected projects and archives all', async function () { const archiveProjectMock = fetchMock.post( `express:/project/:projectId/archive`, { status: 200, }, { delay: 0 } ) const archiveButton = within(actionsToolbar).getByLabelText('Archive') fireEvent.click(archiveButton) const confirmBtn = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmBtn) expect(confirmBtn.disabled).to.be.true await waitFor( () => expect( archiveProjectMock.callHistory.called( `/project/${project1Id}/archive` ) ).to.be.true ) await waitFor( () => expect( archiveProjectMock.callHistory.called( `/project/${project2Id}/archive` ) ).to.be.true ) }) it('opens trash modal for all selected projects and trashes all', async function () { const trashProjectMock = fetchMock.post( `express:/project/:projectId/trash`, { status: 200, }, { delay: 0 } ) const archiveButton = within(actionsToolbar).getByLabelText('Trash') fireEvent.click(archiveButton) const confirmBtn = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmBtn) expect(confirmBtn.disabled).to.be.true await waitFor( () => expect( trashProjectMock.callHistory.called( `/project/${project1Id}/trash` ) ).to.be.true ) await waitFor( () => expect( trashProjectMock.callHistory.called( `/project/${project2Id}/trash` ) ).to.be.true ) }) it('only checks the projects that are viewable when there is a load more button', async function () { // first one is the select all checkbox fireEvent.click(allCheckboxes[0]) allCheckboxes = screen.getAllByRole('checkbox') let checked = allCheckboxes.filter(c => c.checked) expect(checked.length).to.equal(21) // max projects viewable by default is 20, and plus one for check all const loadMoreButton = screen.getByRole('button', { name: 'Show 17 more projects', }) fireEvent.click(loadMoreButton) allCheckboxes = screen.getAllByRole('checkbox') expect(allCheckboxes.length).to.equal(currentList.length + 1) checked = allCheckboxes.filter(c => c.checked) expect(checked.length).to.equal(20) // remains same even after showing more }) it('maintains viewable and selected projects after loading more and then selecting all', async function () { const loadMoreButton = screen.getByRole('button', { name: 'Show 17 more projects', }) fireEvent.click(loadMoreButton) // verify button gone screen.getByText( `Showing ${currentList.length} out of ${currentList.length} projects.` ) // first one is the select all checkbox fireEvent.click(allCheckboxes[0]) // verify button still gone screen.getByText( `Showing ${currentList.length} out of ${currentList.length} projects.` ) // allCheckboxes = screen.getAllByRole('checkbox') // expect(allCheckboxes.length).to.equal(currentList.length + 1) }) }) describe('archived projects', function () { beforeEach(function () { const filterButton = screen.getAllByText('Archived projects')[0] fireEvent.click(filterButton) allCheckboxes = screen.getAllByRole('checkbox') expect(allCheckboxes.length === 2).to.be.true // first one is the select all checkbox fireEvent.click(allCheckboxes[1]) project1Id = allCheckboxes[1].getAttribute('data-project-id') actionsToolbar = screen.getAllByRole('toolbar')[0] }) it('does not show the archive button in toolbar when archive view selected', function () { expect(screen.queryByLabelText('Archive')).to.be.null }) it('restores all projects when selected', async function () { fetchMock.delete(`express:/project/:id/archive`, { status: 200, }) const unarchiveButton = within(actionsToolbar).getByRole('button', { name: 'Restore', }) fireEvent.click(unarchiveButton) await fetchMock.callHistory.flush(true) await screen.findByText('No projects') }) it('only unarchive the selected projects', async function () { // beforeEach selected all, so uncheck the 1st project fireEvent.click(allCheckboxes[1]) const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) expect(allCheckboxesChecked.length).to.equal( archivedProjects.length - 1 ) await fetchMock.callHistory.flush(true) expect(screen.queryByText('No projects')).to.be.null }) }) describe('trashed projects', function () { beforeEach(function () { const filterButton = screen.getAllByText('Trashed projects')[0] fireEvent.click(filterButton) allCheckboxes = screen.getAllByRole('checkbox') // + 1 because of select all expect(allCheckboxes.length).to.equal(trashedList.length + 1) // first one is the select all checkbox fireEvent.click(allCheckboxes[0]) const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) // + 1 because of select all expect(allCheckboxesChecked.length).to.equal(trashedList.length + 1) actionsToolbar = screen.getAllByRole('toolbar')[0] }) it('shows the download, archive, and restore buttons in top toolbar', function () { expect(screen.queryByLabelText('Trash')).to.be.null within(actionsToolbar).queryByLabelText('Download') within(actionsToolbar).queryByLabelText('Archive') within(actionsToolbar).getByText('Restore') // no icon for this button }) it('clears selected projects when filter changed', function () { const filterButton = screen.getAllByText('All projects')[0] fireEvent.click(filterButton) const allCheckboxes = screen.getAllByRole('checkbox') const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) expect(allCheckboxesChecked.length).to.equal(0) }) it('untrashes all the projects', async function () { fetchMock.delete(`express:/project/:id/trash`, { status: 200, }) const untrashButton = within(actionsToolbar).getByText('Restore') fireEvent.click(untrashButton) await fetchMock.callHistory.flush(true) await screen.findByText('No projects') }) it('only untrashes the selected projects', async function () { // beforeEach selected all, so uncheck the 1st project fireEvent.click(allCheckboxes[1]) const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1) await fetchMock.callHistory.flush(true) expect(screen.queryByText('No projects')).to.be.null }) it('removes project from view when archiving', async function () { fetchMock.post( `express:/project/:id/archive`, { status: 200, }, { repeat: trashedList.length } ) const archiveButton = within(actionsToolbar).getByLabelText('Archive') fireEvent.click(archiveButton) const confirmButton = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmButton) expect(confirmButton.disabled).to.be.true await fetchMock.callHistory.flush(true) const calls = fetchMock.callHistory.calls().map(({ url }) => url) trashedList.forEach(project => { expect(calls).to.contain( `https://www.test-overleaf.com/project/${project.id}/archive` ) }) }) it('removes only selected projects from view when leaving', async function () { // rerender content with different projects this.unmount() fetchMock.removeRoutes().clearHistory() renderWithProjectListContext(, { projects: leavableList, }) await fetchMock.callHistory.flush(true) await screen.findByRole('table') expect(leavableList.length).to.be.greaterThan(0) fetchMock.post( `express:/project/:id/leave`, { status: 200, }, { repeat: leavableList.length } ) // + 1 because of select all await waitFor(() => expect( screen.getAllByRole('checkbox').length ).to.equal(leavableList.length + 1) ) // first one is the select all checkbox fireEvent.click(screen.getAllByRole('checkbox')[0]) actionsToolbar = screen.getAllByRole('toolbar')[0] const toolbar = within(actionsToolbar) expect(toolbar.queryByRole('button', { name: /delete/i })).to.be.null expect( toolbar.queryByRole('button', { name: /delete \/ leave/i, }) ).to.be.null const leaveButton = toolbar.getByRole('button', { name: /leave/i, }) fireEvent.click(leaveButton) const confirmButton = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmButton) expect(confirmButton.disabled).to.be.true await fetchMock.callHistory.flush(true) const calls = fetchMock.callHistory.calls().map(({ url }) => url) leavableList.forEach(project => { expect(calls).to.contain( `https://www.test-overleaf.com/project/${project.id}/leave` ) }) }) it('removes only selected projects from view when deleting', async function () { // rerender content with different projects this.unmount() fetchMock.removeRoutes().clearHistory() renderWithProjectListContext(, { projects: deletableList, }) await fetchMock.callHistory.flush(true) await screen.findByRole('table') expect(deletableList.length).to.be.greaterThan(0) fetchMock.delete( `express:/project/:id`, { status: 200, }, { repeat: deletableList.length } ) await waitFor(() => { allCheckboxes = screen.getAllByRole('checkbox') // + 1 because of select all expect(allCheckboxes.length).to.equal(deletableList.length + 1) }) // first one is the select all checkbox fireEvent.click(allCheckboxes[0]) actionsToolbar = screen.getAllByRole('toolbar')[0] const toolbar = within(actionsToolbar) expect(toolbar.queryByRole('button', { name: /leave/i })).to.be.null expect( toolbar.queryByRole('button', { name: /delete \/ leave/i, }) ).to.be.null const deleteButton = toolbar.getByRole('button', { name: /delete/i, }) fireEvent.click(deleteButton) const confirmButton = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmButton) expect(confirmButton.disabled).to.be.true await fetchMock.callHistory.flush(true) const calls = fetchMock.callHistory.calls().map(({ url }) => url) deletableList.forEach(project => { expect(calls).to.contain( `https://www.test-overleaf.com/project/${project.id}` ) }) }) it('removes only selected projects from view when deleting and leaving', async function () { // rerender content with different projects this.unmount() fetchMock.removeRoutes().clearHistory() const deletableAndLeavableList = [...deletableList, ...leavableList] renderWithProjectListContext(, { projects: deletableAndLeavableList, }) await fetchMock.callHistory.flush(true) await screen.findByRole('table') expect(deletableList.length).to.be.greaterThan(0) expect(leavableList.length).to.be.greaterThan(0) fetchMock .delete( `express:/project/:id`, { status: 200, }, { repeat: deletableList.length } ) .post( `express:/project/:id/leave`, { status: 200, }, { repeat: leavableList.length } ) // + 1 because of select all await waitFor(() => expect( screen.getAllByRole('checkbox').length ).to.equal(deletableAndLeavableList.length + 1) ) // first one is the select all checkbox fireEvent.click(screen.getAllByRole('checkbox')[0]) actionsToolbar = screen.getAllByRole('toolbar')[0] const toolbar = within(actionsToolbar) expect(toolbar.queryByRole('button', { name: 'Leave' })).to.be.null expect(toolbar.queryByRole('button', { name: 'Delete' })).to.be.null const deleteLeaveButton = toolbar.getByRole('button', { name: /delete \/ leave/i, }) fireEvent.click(deleteLeaveButton) const confirmButton = screen.getByRole('button', { name: 'Confirm', }) as HTMLButtonElement fireEvent.click(confirmButton) expect(confirmButton.disabled).to.be.true await fetchMock.callHistory.flush(true) const calls = fetchMock.callHistory.calls().map(({ url }) => url) deletableAndLeavableList.forEach(project => { expect(calls).to.contain.oneOf([ `https://www.test-overleaf.com/project/${project.id}`, `https://www.test-overleaf.com/project/${project.id}/leave`, ]) }) }) }) describe('tags', function () { it('does not show archived or trashed project', async function () { this.unmount() fetchMock.removeRoutes().clearHistory() window.metaAttributesCache.set('ol-tags', [ { _id: this.tagId, name: this.tagName, project_ids: [ projectsData[0].id, projectsData[1].id, ...archivedList.map(p => p.id), ...trashedList.map(p => p.id), ], }, ]) const trashProjectMock = fetchMock.post( `express:/project/:projectId/trash`, { status: 200 } ) renderWithProjectListContext(, { projects: fullList, }) await screen.findByRole('table') let visibleProjectsCount = 2 const [tagBtn] = screen.getAllByRole('button', { name: `${this.tagName} (${visibleProjectsCount})`, }) fireEvent.click(tagBtn) const nonArchivedAndTrashedProjects = [ projectsData[0], projectsData[1], ] nonArchivedAndTrashedProjects.forEach(p => { screen.getByText(p.name) }) const archivedAndTrashedProjects = [...archivedList, ...trashedList] archivedAndTrashedProjects.forEach(p => { expect(screen.queryByText(p.name)).to.be.null }) const trashBtns = screen.getAllByRole('button', { name: 'Trash' }) for (const [index, trashBtn] of trashBtns.entries()) { fireEvent.click(trashBtn) fireEvent.click( screen.getByRole('button', { name: 'Confirm', }) ) await waitFor(() => { expect( trashProjectMock.callHistory.called( `/project/${projectsData[index].id}/trash` ) ).to.be.true }) await waitFor(() => { expect( screen.queryAllByText(projectsData[index].name) ).to.have.length(0) }) screen.getAllByRole('button', { name: `${this.tagName} (${--visibleProjectsCount})`, }) } }) }) describe('tags dropdown', function () { beforeEach(async function () { allCheckboxes = screen.getAllByRole('checkbox') // first one is the select all checkbox fireEvent.click(allCheckboxes[1]) fireEvent.click(allCheckboxes[2]) actionsToolbar = screen.getAllByRole('toolbar')[0] this.newTagName = 'Some tag name' this.newTagId = 'abc123def456' }) it('opens the tags dropdown and creates a new tag', async function () { fetchMock.post(`express:/tag`, { status: 200, body: { _id: this.newTagId, name: this.newTagName, project_ids: [], }, }) fetchMock.post(`express:/tag/:id/projects`, { status: 204, }) const tagsDropdown = await within(actionsToolbar).findByLabelText('Tags') fireEvent.click(tagsDropdown) screen.getByText('Add to tag') const newTagButton = screen.getByRole('menuitem', { name: 'Create new tag', }) fireEvent.click(newTagButton) const modal = screen.getAllByRole('dialog')[0] const input = within(modal).getByRole('textbox') fireEvent.change(input, { target: { value: this.newTagName }, }) const createButton = within(modal).getByRole('button', { name: 'Create', }) fireEvent.click(createButton) await fetchMock.callHistory.flush(true) expect( fetchMock.callHistory.called('/tag', { name: this.newTagName }) ).to.be.true expect( fetchMock.callHistory.called(`/tag/${this.newTagId}/projects`, { body: { projectIds: [projectsData[0].id, projectsData[1].id], }, }) ).to.be.true await screen.findByRole( 'button', { name: `${this.newTagName} (2)` }, { timeout: 5000 } ) }) it('opens the tags dropdown and remove a tag from selected projects', async function () { const deleteProjectsFromTagMock = fetchMock.post( `express:/tag/:id/projects/remove`, { status: 204, } ) screen.getByRole('button', { name: `${this.tagName} (2)` }) const tagsDropdown = within(actionsToolbar).getByLabelText('Tags') fireEvent.click(tagsDropdown) within(actionsToolbar).getByText('Add to tag') const tagButton = within(actionsToolbar).getByLabelText( `Add or remove project from tag ${this.tagName}` ) fireEvent.click(tagButton) await fetchMock.callHistory.flush(true) expect( deleteProjectsFromTagMock.callHistory.called( `/tag/${this.tagId}/projects/remove`, { body: { projectIds: [projectsData[0].id, projectsData[1].id], }, } ) ).to.be.true screen.getByRole('button', { name: `${this.tagName} (0)` }) }) it('select another project, opens the tags dropdown and add a tag only to the untagged project', async function () { const addProjectsToTagMock = fetchMock.post( `express:/tag/:id/projects`, { status: 204, } ) fireEvent.click(allCheckboxes[3]) screen.getByRole('button', { name: `${this.tagName} (2)` }) const tagsDropdown = within(actionsToolbar).getByLabelText('Tags') fireEvent.click(tagsDropdown) within(actionsToolbar).getByText('Add to tag') const tagButton = within(actionsToolbar).getByLabelText( `Add or remove project from tag ${this.tagName}` ) fireEvent.click(tagButton) await fetchMock.callHistory.flush(true) expect( addProjectsToTagMock.callHistory.called( `/tag/${this.tagId}/projects`, { body: { projectIds: [projectsData[2].id], }, } ) ).to.be.true screen.getByRole('button', { name: `${this.tagName} (3)` }) }) }) describe('project tools', function () { it('renders download, archive, trash buttons followed by tags and "more" dropdowns when selecting an archived or trashed filter before selecting tag filter', function () { const assertToolbarButtonsExists = () => { within(actionsToolbar).getByLabelText(/download/i) within(actionsToolbar).getByLabelText(/archive/i) within(actionsToolbar).getByLabelText(/trash/i) within(actionsToolbar).getByLabelText(/tags/i) within(actionsToolbar).getByText(/more/i) } // Select archived projects const [archivedProjectsButton] = screen.getAllByText(/archived projects/i) fireEvent.click(archivedProjectsButton) const [tag] = screen.getAllByText(this.tagName) fireEvent.click(tag) allCheckboxes = screen.getAllByRole('checkbox') fireEvent.click(allCheckboxes[1]) actionsToolbar = screen.getAllByRole('toolbar')[0] assertToolbarButtonsExists() // select trashed projects const [trashedProjectsButton] = screen.getAllByText(/trashed projects/i) fireEvent.click(trashedProjectsButton) fireEvent.click(tag) allCheckboxes = screen.getAllByRole('checkbox') fireEvent.click(allCheckboxes[1]) actionsToolbar = screen.getAllByRole('toolbar')[0] assertToolbarButtonsExists() }) describe('"More" dropdown', function () { beforeEach(async function () { const filterButton = screen.getAllByText('All projects')[0] fireEvent.click(filterButton) allCheckboxes = screen.getAllByRole('checkbox') }) it('does not show the dropdown when more than 1 project is selected', async function () { fireEvent.click(allCheckboxes[2]) // select a project const actionsToolbar = screen.getAllByRole('toolbar')[0] await waitFor(() => { within(actionsToolbar).getByText('More') }) fireEvent.click(allCheckboxes[0]) // select all expect(within(actionsToolbar).queryByText('More')).to .be.null }) it('validates the project name', async function () { fireEvent.click(allCheckboxes[1]) // select a project owned by the current user const actionsToolbar = screen.getAllByRole('toolbar')[0] const moreDropdown = await within(actionsToolbar).findByText('More') fireEvent.click(moreDropdown) const editButton = screen.getAllByText('Rename')[0] fireEvent.click(editButton) const modals = await screen.findAllByRole('dialog') const modal = modals[0] expect(sendMBSpy).to.have.been.calledTwice expect(sendMBSpy).to.have.been.calledWith('loads_v2_dash') expect(sendMBSpy).to.have.been.calledWith( 'project-list-page-interaction', { action: 'rename', page: '/', projectId: copyableProject.id, isSmallDevice: true, } ) // same name let confirmButton = within(modal).getByRole('button', { name: 'Rename', }) as HTMLButtonElement expect(confirmButton.disabled).to.be.true // no name const input = screen.getByLabelText( /New name/i ) as HTMLButtonElement fireEvent.change(input, { target: { value: '' }, }) confirmButton = within(modal).getByRole('button', { name: 'Rename', }) as HTMLButtonElement expect(confirmButton.disabled).to.be.true }) it('opens the rename modal, and can rename the project, and view updated', async function () { fireEvent.click(allCheckboxes[1]) // select a project owned by the current user const actionsToolbar = screen.getAllByRole('toolbar')[0] const renameProjectMock = fetchMock.post( `express:/project/:id/rename`, { status: 200, } ) const moreDropdown = await within(actionsToolbar).findByText('More') fireEvent.click(moreDropdown) const renameButton = within(actionsToolbar).getByText('Rename') fireEvent.click(renameButton) const modals = await screen.findAllByRole('dialog') const modal = modals[0] // a valid name const newProjectName = 'A new project name' const input = (await within(modal).findByLabelText( /New name/i )) as HTMLButtonElement const oldName = input.value fireEvent.change(input, { target: { value: newProjectName }, }) const confirmButton = within(modal).getByRole('button', { name: 'Rename', }) as HTMLButtonElement expect(confirmButton.disabled).to.be.false fireEvent.click(confirmButton) await fetchMock.callHistory.flush(true) expect( renameProjectMock.callHistory.called( `/project/${projectsData[0].id}/rename` ) ).to.be.true const table = await screen.findByRole('table') await within(table).findByText(newProjectName) expect(within(table).queryByText(oldName)).to.be.null const allCheckboxesInTable = await within(table).findAllByRole('checkbox') const allCheckboxesChecked = allCheckboxesInTable.filter( c => c.checked ) expect(allCheckboxesChecked.length).to.equal(0) }) it('opens the copy modal, can copy the project, and view updated', async function () { fireEvent.click(allCheckboxes[2]) const actionsToolbar = screen.getAllByRole('toolbar')[0] const tableRows = screen.getAllByRole('row') const linkForProjectToCopy = within(tableRows[1]).getByRole('link') const projectNameToCopy = linkForProjectToCopy.textContent || '' // needed for type checking screen.getByText(projectNameToCopy) // make sure not just empty string const copiedProjectName = `${projectNameToCopy} (Copy)` const cloneProjectMock = fetchMock.post( `express:/project/:id/clone`, { status: 200, body: { name: copiedProjectName, lastUpdated: new Date(), project_id: userId, owner_ref: userId, owner, id: '6328e14abec0df019fce0be5', lastUpdatedBy: owner, accessLevel: 'owner', source: 'owner', trashed: false, archived: false, }, } ) await waitFor(() => { const moreDropdown = within(actionsToolbar).getByText('More') fireEvent.click(moreDropdown) }) const copyButton = within(actionsToolbar).getByText('Make a copy') fireEvent.click(copyButton) // confirm in modal const copyConfirmButton = document.querySelector( 'button[type="submit"]' ) as HTMLElement fireEvent.click(copyConfirmButton) await fetchMock.callHistory.flush(true) expect( cloneProjectMock.callHistory.called( `/project/${projectsData[1].id}/clone` ) ).to.be.true expect(sendMBSpy).to.have.been.calledTwice expect(sendMBSpy).to.have.been.calledWith('loads_v2_dash') expect(sendMBSpy).to.have.been.calledWith( 'project-list-page-interaction', { action: 'clone', page: '/', projectId: archiveableProject.id, isSmallDevice: true, } ) await screen.findByText(copiedProjectName) }) }) }) describe('projects list in actions modal', function () { let modal: HTMLElement let projectsToProcess: any[] function selectedProjectNames() { projectsToProcess = [] // needs to be done ahead of opening modal allCheckboxes = screen.getAllByRole('checkbox') // update list so we know which are checked const tableRows = screen.getAllByRole('row') for (const [index, checkbox] of allCheckboxes.entries()) { if (checkbox.checked) { const linkForProjectToCopy = within(tableRows[index]).getByRole( 'link' ) const projectNameToCopy = linkForProjectToCopy.textContent projectsToProcess.push(projectNameToCopy) } } expect(projectsToProcess.length > 0).to.be.true } function selectedMatchesDisplayed(expectedLength: number) { selectedProjectNames() // any action will work for check since they all use the same modal const archiveButton = within(actionsToolbar).getByLabelText('Archive') fireEvent.click(archiveButton) modal = screen.getAllByRole('dialog')[0] const listitems = within(modal).getAllByRole('listitem') expect(listitems.length).to.equal(projectsToProcess.length) expect(listitems.length).to.equal(expectedLength) for (const projectName of projectsToProcess) { within(modal).getByText(projectName) } } beforeEach(function () { fetchMock.removeRoutes().clearHistory() allCheckboxes = screen.getAllByRole('checkbox') // first one is the select all checkbox, just check 2 at first fireEvent.click(allCheckboxes[1]) fireEvent.click(allCheckboxes[2]) actionsToolbar = screen.getAllByRole('toolbar')[0] }) it('opens the modal with the 2 originally selected projects', function () { selectedMatchesDisplayed(2) }) it('shows correct list after closing modal, changing selection, and reopening modal', async function () { selectedMatchesDisplayed(2) const modal = screen.getAllByRole('dialog', { hidden: false })[0] const cancelButton = within(modal).getByRole('button', { name: 'Cancel', }) fireEvent.click(cancelButton) await waitForElementToBeRemoved( screen.getByRole('dialog', { hidden: false }) ) await screen.findAllByRole('checkbox') fireEvent.click(allCheckboxes[3]) selectedMatchesDisplayed(3) }) it('maintains original list even after some have been processed', async function () { const totalProjectsToProcess = 2 selectedMatchesDisplayed(totalProjectsToProcess) const button = screen.getByRole('button', { name: 'Confirm' }) fireEvent.click(button) project1Id = allCheckboxes[1].getAttribute('data-project-id') fetchMock.post('express:/project/:id/archive', { status: 200, }) fetchMock.post(`/project/${project2Id}/archive`, { status: 500, }) await screen.findByRole('alert') // ensure that error was thrown for the 2nd project const listitems = within(modal).getAllByRole('listitem') expect(listitems.length).to.equal(totalProjectsToProcess) }) }) }) describe('search', function () { it('shows only projects based on the input', async function () { const input = screen.getAllByRole('textbox', { name: /search in all projects/i, })[0] const value = currentList[0].name fireEvent.change(input, { target: { value } }) const results = screen.getAllByRole('row') expect(results.length).to.equal(2) // first is header }) }) describe('copying project', function () { it('correctly updates the view after copying a shared project', async function () { const filterButton = screen.getAllByText('Shared with you')[0] fireEvent.click(filterButton) const tableRows = screen.getAllByRole('row') const linkForProjectToCopy = within(tableRows[1]).getByRole('link') const projectNameToCopy = linkForProjectToCopy.textContent const copiedProjectName = `${projectNameToCopy} Copy` fetchMock.post(`express:/project/:id/clone`, { status: 200, body: { name: copiedProjectName, lastUpdated: new Date(), project_id: userId, owner_ref: userId, owner, id: '6328e14abec0df019fce0be5', lastUpdatedBy: owner, accessLevel: 'owner', source: 'owner', trashed: false, archived: false, }, }) const copyButton = within(tableRows[1]).getAllByRole('button', { name: 'Copy', })[0] fireEvent.click(copyButton) // confirm in modal const copyConfirmButton = document.querySelector( 'button[type="submit"]' ) as HTMLElement fireEvent.click(copyConfirmButton) await fetchMock.callHistory.flush(true) expect(sendMBSpy).to.have.been.calledTwice expect(sendMBSpy).to.have.been.calledWith('loads_v2_dash') expect(sendMBSpy).to.have.been.calledWith( 'project-list-page-interaction', { action: 'clone', page: '/', projectId: archiveableProject.id, isSmallDevice: true, } ) expect(screen.queryByText(copiedProjectName)).to.be.null const yourProjectFilter = screen.getAllByText('Your projects')[0] fireEvent.click(yourProjectFilter) await screen.findByText(copiedProjectName) }) }) }) })