From debe76baa64b86188f46f4a56cdc8b5da5a43f82 Mon Sep 17 00:00:00 2001 From: Alexandre Bourdin Date: Wed, 12 Oct 2022 16:17:09 +0200 Subject: [PATCH] Merge pull request #9865 from overleaf/ab-display-notifications-welcome-page [web] Display notifications on the react dashboard welcome page GitOrigin-RevId: 29fb08bbac195c2766dd0e94dbe9e9a0c7065e76 --- .../notifications/groups/common.tsx | 4 +- .../components/project-list-root.tsx | 5 + .../components/project-list-root.test.tsx | 813 +++++++++--------- .../components/sidebar/tags-list.test.tsx | 5 +- 4 files changed, 438 insertions(+), 389 deletions(-) diff --git a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx index d9f78e57df..1a08f7eed5 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx +++ b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx @@ -5,7 +5,6 @@ import Notification from '../notification' import Icon from '../../../../../shared/components/icon' import getMeta from '../../../../../utils/meta' import useAsyncDismiss from '../hooks/useAsyncDismiss' -import { useProjectListContext } from '../../../context/project-list-context' import useAsync from '../../../../../shared/hooks/use-async' import { FetchError, postJSON } from '../../../../../infrastructure/fetch-json' import { ExposedSettings } from '../../../../../../../types/exposed-settings' @@ -14,7 +13,6 @@ import { User } from '../../../../../../../types/user' function Common() { const { t } = useTranslation() - const { totalProjectsCount } = useProjectListContext() const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings const notifications = getMeta('ol-notifications', []) as NotificationType[] const user = getMeta('ol-user', []) as Pick @@ -33,7 +31,7 @@ function Common() { ).catch(console.error) } - if (!totalProjectsCount || !notifications.length) { + if (!notifications.length) { return null } diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 177cfdd2c4..758d31b5c9 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -135,6 +135,11 @@ function ProjectListPageContent() { mdOffset={2} className="project-list-empty-col" > + + + + + diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index 0ee1532531..00aff8b8ac 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -24,20 +24,17 @@ describe('', function () { sendSpy = sinon.spy(eventTracking, 'send') window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-tags', []) - window.metaAttributesCache.set('ol-ExposedSettings', { templateLinks: [] }) + window.metaAttributesCache.set('ol-ExposedSettings', { + templateLinks: [], + }) + window.metaAttributesCache.set('ol-userEmails', [ + { email: 'test@overleaf.com', default: true }, + ]) window.user_id = userId Object.defineProperty(window, 'location', { value: { assign: locationStub }, }) - - renderWithProjectListContext(, { - projects: fullList, - }) - await fetchMock.flush(true) - await waitFor(() => { - screen.findByRole('table') - }) }) afterEach(function () { @@ -49,360 +46,465 @@ describe('', function () { }) }) - 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] + describe('welcome page', function () { + beforeEach(async function () { + renderWithProjectListContext(, { + projects: [], }) + await fetchMock.flush(true) + }) - it('downloads all selected projects and then unselects them', async function () { - const downloadButton = within(actionsToolbar).getByLabelText('Download') - fireEvent.click(downloadButton) + it('the welcome page is displayed', async function () { + screen.getByRole('heading', { name: 'Welcome to Overleaf!' }) + }) - await waitFor(() => { - expect(locationStub).to.have.been.called - }) - - sinon.assert.calledWithMatch( - locationStub, - `/project/download/zip?project_ids=${project1Id},${project2Id}` + it('the email confirmation alert is not displayed', async function () { + expect( + screen.queryByText( + 'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email' ) + ).to.be.null + }) + }) - const allCheckboxes = screen.getAllByRole('checkbox') - const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) - expect(allCheckboxesChecked.length).to.equal(0) + describe('project table', function () { + beforeEach(async function () { + renderWithProjectListContext(, { + projects: fullList, }) - - it('opens archive modal for all selected projects and archives all', async function () { - fetchMock.post( - `express:/project/${project1Id}/archive`, - { - status: 200, - }, - { delay: 0 } - ) - fetchMock.post( - `express:/project/${project2Id}/archive`, - { - status: 200, - }, - { delay: 0 } - ) - - const archiveButton = within(actionsToolbar).getByLabelText('Archive') - fireEvent.click(archiveButton) - - const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement - fireEvent.click(confirmBtn) - expect(confirmBtn.disabled).to.be.true - - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true - - const requests = fetchMock.calls() - const [projectRequest1Url, projectRequest1Headers] = requests[2] - expect(projectRequest1Url).to.equal(`/project/${project1Id}/archive`) - expect(projectRequest1Headers?.method).to.equal('POST') - const [projectRequest2Url, projectRequest2Headers] = requests[3] - expect(projectRequest2Url).to.equal(`/project/${project2Id}/archive`) - expect(projectRequest2Headers?.method).to.equal('POST') - }) - - it('opens trash modal for all selected projects and trashes all', async function () { - fetchMock.post( - `express:/project/${project1Id}/trash`, - { - status: 200, - }, - { delay: 0 } - ) - fetchMock.post( - `express:/project/${project2Id}/trash`, - { - status: 200, - }, - { delay: 0 } - ) - - const archiveButton = within(actionsToolbar).getByLabelText('Trash') - fireEvent.click(archiveButton) - - const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement - fireEvent.click(confirmBtn) - expect(confirmBtn.disabled).to.be.true - - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true - - const requests = fetchMock.calls() - const [projectRequest1Url, projectRequest1Headers] = requests[2] - expect(projectRequest1Url).to.equal(`/project/${project1Id}/trash`) - expect(projectRequest1Headers?.method).to.equal('POST') - const [projectRequest2Url, projectRequest2Headers] = requests[3] - expect(projectRequest2Url).to.equal(`/project/${project2Id}/trash`) - expect(projectRequest2Headers?.method).to.equal('POST') - }) - - 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.getByLabelText('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.getByLabelText('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) + await fetchMock.flush(true) + await waitFor(() => { + screen.findByRole('table') }) }) - describe('archived projects', function () { - beforeEach(function () { - const filterButton = screen.getAllByText('Archived Projects')[0] - fireEvent.click(filterButton) + describe('checkboxes', function () { + let allCheckboxes: Array = [] + let actionsToolbar: HTMLElement + let project1Id: string | null, project2Id: string | null - 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') + 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]) - 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, + project1Id = allCheckboxes[1].getAttribute('data-project-id') + project2Id = allCheckboxes[2].getAttribute('data-project-id') + actionsToolbar = screen.getAllByRole('toolbar')[0] }) - const unarchiveButton = - within(actionsToolbar).getByText('Restore') - fireEvent.click(unarchiveButton) + it('downloads all selected projects and then unselects them', async function () { + const downloadButton = + within(actionsToolbar).getByLabelText('Download') + fireEvent.click(downloadButton) - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true + await waitFor(() => { + expect(locationStub).to.have.been.called + }) - screen.getByText('No projects') - }) + sinon.assert.calledWithMatch( + locationStub, + `/project/download/zip?project_ids=${project1Id},${project2Id}` + ) - 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.flush(true) - expect(fetchMock.done()).to.be.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('only 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 allCheckboxes = + screen.getAllByRole('checkbox') + const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) + expect(allCheckboxesChecked.length).to.equal(0) }) - const untrashButton = - within(actionsToolbar).getByText('Restore') - fireEvent.click(untrashButton) + it('opens archive modal for all selected projects and archives all', async function () { + fetchMock.post( + `express:/project/${project1Id}/archive`, + { + status: 200, + }, + { delay: 0 } + ) + fetchMock.post( + `express:/project/${project2Id}/archive`, + { + status: 200, + }, + { delay: 0 } + ) - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true + const archiveButton = within(actionsToolbar).getByLabelText('Archive') + fireEvent.click(archiveButton) - screen.getByText('No projects') - }) + const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement + fireEvent.click(confirmBtn) + expect(confirmBtn.disabled).to.be.true - it('only untrashes the selected projects', async function () { - // beforeEach selected all, so uncheck the 1st project - fireEvent.click(allCheckboxes[1]) + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true - const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) - expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1) - - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.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, + const requests = fetchMock.calls() + const [projectRequest1Url, projectRequest1Headers] = requests[2] + expect(projectRequest1Url).to.equal(`/project/${project1Id}/archive`) + expect(projectRequest1Headers?.method).to.equal('POST') + const [projectRequest2Url, projectRequest2Headers] = requests[3] + expect(projectRequest2Url).to.equal(`/project/${project2Id}/archive`) + expect(projectRequest2Headers?.method).to.equal('POST') }) - const untrashButton = - within(actionsToolbar).getByLabelText('Archive') - fireEvent.click(untrashButton) + it('opens trash modal for all selected projects and trashes all', async function () { + fetchMock.post( + `express:/project/${project1Id}/trash`, + { + status: 200, + }, + { delay: 0 } + ) + fetchMock.post( + `express:/project/${project2Id}/trash`, + { + status: 200, + }, + { delay: 0 } + ) - const confirmButton = screen.getByText('Confirm') - fireEvent.click(confirmButton) - expect(confirmButton.disabled).to.be.true + const archiveButton = within(actionsToolbar).getByLabelText('Trash') + fireEvent.click(archiveButton) - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true + const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement + fireEvent.click(confirmBtn) + expect(confirmBtn.disabled).to.be.true - screen.getByText('No projects') - }) - }) + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true - describe('project tools "More" dropdown', function () { - beforeEach(async function () { - const filterButton = screen.getAllByText('All Projects')[0] - fireEvent.click(filterButton) - allCheckboxes = screen.getAllByRole('checkbox') - // first one is the select all checkbox - fireEvent.click(allCheckboxes[2]) - actionsToolbar = screen.getAllByRole('toolbar')[0] - }) - - it('does not show the dropdown when more than 1 project is selected', async function () { - await waitFor(() => { - within(actionsToolbar).getByText('More') - }) - fireEvent.click(allCheckboxes[0]) - expect(within(actionsToolbar).queryByText('More')).to.be - .null - }) - - it('opens the rename modal, and can rename the project, and view updated', async function () { - fetchMock.post(`express:/project/:id/rename`, { - status: 200, + const requests = fetchMock.calls() + const [projectRequest1Url, projectRequest1Headers] = requests[2] + expect(projectRequest1Url).to.equal(`/project/${project1Id}/trash`) + expect(projectRequest1Headers?.method).to.equal('POST') + const [projectRequest2Url, projectRequest2Headers] = requests[3] + expect(projectRequest2Url).to.equal(`/project/${project2Id}/trash`) + expect(projectRequest2Headers?.method).to.equal('POST') }) - await waitFor(() => { - const moreDropdown = + 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.getByLabelText('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.getByLabelText('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).getByText('Restore') + fireEvent.click(unarchiveButton) + + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true + + screen.getByText('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.flush(true) + expect(fetchMock.done()).to.be.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('only 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.flush(true) + expect(fetchMock.done()).to.be.true + + screen.getByText('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.flush(true) + expect(fetchMock.done()).to.be.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, + }) + + const untrashButton = + within(actionsToolbar).getByLabelText('Archive') + fireEvent.click(untrashButton) + + const confirmButton = screen.getByText('Confirm') + fireEvent.click(confirmButton) + expect(confirmButton.disabled).to.be.true + + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true + + screen.getByText('No projects') + }) + }) + + describe('project tools "More" dropdown', function () { + beforeEach(async function () { + const filterButton = screen.getAllByText('All Projects')[0] + fireEvent.click(filterButton) + allCheckboxes = screen.getAllByRole('checkbox') + // first one is the select all checkbox + fireEvent.click(allCheckboxes[2]) + actionsToolbar = screen.getAllByRole('toolbar')[0] + }) + + it('does not show the dropdown when more than 1 project is selected', async function () { + await waitFor(() => { within(actionsToolbar).getByText('More') - fireEvent.click(moreDropdown) + }) + fireEvent.click(allCheckboxes[0]) + expect(within(actionsToolbar).queryByText('More')).to.be + .null }) - const renameButton = screen.getByText('Rename') - fireEvent.click(renameButton) + it('opens the rename modal, and can rename the project, and view updated', async function () { + fetchMock.post(`express:/project/:id/rename`, { + status: 200, + }) - const modal = screen.getAllByRole('dialog')[0] + await waitFor(() => { + const moreDropdown = + within(actionsToolbar).getByText('More') + fireEvent.click(moreDropdown) + }) - expect(sendSpy).to.be.calledOnce - expect(sendSpy).calledWith('project-list-page-interaction') + const renameButton = screen.getByText('Rename') + fireEvent.click(renameButton) - // same name - let confirmButton = within(modal).getByText('Rename') - expect(confirmButton.disabled).to.be.true - let input = screen.getByLabelText('New Name') as HTMLButtonElement - const oldName = input.value + const modal = screen.getAllByRole('dialog')[0] - // no name - let newProjectName = '' - input = screen.getByLabelText('New Name') as HTMLButtonElement - fireEvent.change(input, { - target: { value: newProjectName }, - }) - confirmButton = within(modal).getByText('Rename') - expect(confirmButton.disabled).to.be.true + expect(sendSpy).to.be.calledOnce + expect(sendSpy).calledWith('project-list-page-interaction') - // a valid name - newProjectName = 'A new project name' - input = screen.getByLabelText('New Name') as HTMLButtonElement - fireEvent.change(input, { - target: { value: newProjectName }, + // same name + let confirmButton = + within(modal).getByText('Rename') + expect(confirmButton.disabled).to.be.true + let input = screen.getByLabelText('New Name') as HTMLButtonElement + const oldName = input.value + + // no name + let newProjectName = '' + input = screen.getByLabelText('New Name') as HTMLButtonElement + fireEvent.change(input, { + target: { value: newProjectName }, + }) + confirmButton = within(modal).getByText('Rename') + expect(confirmButton.disabled).to.be.true + + // a valid name + newProjectName = 'A new project name' + input = screen.getByLabelText('New Name') as HTMLButtonElement + fireEvent.change(input, { + target: { value: newProjectName }, + }) + + confirmButton = within(modal).getByText('Rename') + expect(confirmButton.disabled).to.be.false + fireEvent.click(confirmButton) + + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true + + screen.findByText(newProjectName) + expect(screen.queryByText(oldName)).to.be.null + + const allCheckboxes = + screen.getAllByRole('checkbox') + const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) + expect(allCheckboxesChecked.length).to.equal(0) }) - confirmButton = within(modal).getByText('Rename') - expect(confirmButton.disabled).to.be.false - fireEvent.click(confirmButton) + it('opens the copy modal, can copy the project, and view updated', async function () { + const tableRows = screen.getAllByRole('row') + const linkForProjectToCopy = within(tableRows[1]).getByRole('link') + const projectNameToCopy = linkForProjectToCopy.textContent || '' // needed for type checking + screen.findByText(projectNameToCopy) // make sure not just empty string + 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, + }, + }) - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true + await waitFor(() => { + const moreDropdown = + within(actionsToolbar).getByText('More') + fireEvent.click(moreDropdown) + }) - screen.findByText(newProjectName) - expect(screen.queryByText(oldName)).to.be.null + const copyButton = + within(actionsToolbar).getByText('Make a copy') + fireEvent.click(copyButton) - const allCheckboxes = screen.getAllByRole('checkbox') - const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) - expect(allCheckboxesChecked.length).to.equal(0) + // confirm in modal + const copyConfirmButton = document.querySelector( + 'button[type="submit"]' + ) as HTMLElement + fireEvent.click(copyConfirmButton) + + await fetchMock.flush(true) + expect(fetchMock.done()).to.be.true + + expect(sendSpy).to.be.calledOnce + expect(sendSpy).calledWith('project-list-page-interaction') + + screen.findByText(copiedProjectName) + }) }) + }) + + describe('search', function () { + it('shows only projects based on the input', async function () { + const input = screen.getAllByRole('textbox', { + name: /search 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) - it('opens the copy modal, can copy the project, and view updated', async function () { const tableRows = screen.getAllByRole('row') + const linkForProjectToCopy = within(tableRows[1]).getByRole('link') - const projectNameToCopy = linkForProjectToCopy.textContent || '' // needed for type checking - screen.findByText(projectNameToCopy) // make sure not just empty string - const copiedProjectName = `${projectNameToCopy} (Copy)` + const projectNameToCopy = linkForProjectToCopy.textContent + const copiedProjectName = `${projectNameToCopy} Copy` fetchMock.post(`express:/project/:id/clone`, { status: 200, body: { @@ -419,15 +521,7 @@ describe('', function () { archived: false, }, }) - - await waitFor(() => { - const moreDropdown = - within(actionsToolbar).getByText('More') - fireEvent.click(moreDropdown) - }) - - const copyButton = - within(actionsToolbar).getByText('Make a copy') + const copyButton = within(tableRows[1]).getAllByLabelText('Copy')[0] fireEvent.click(copyButton) // confirm in modal @@ -442,71 +536,20 @@ describe('', function () { expect(sendSpy).to.be.calledOnce expect(sendSpy).calledWith('project-list-page-interaction') + expect(screen.queryByText(copiedProjectName)).to.be.null + + const yourProjectFilter = screen.getAllByText('Your Projects')[0] + fireEvent.click(yourProjectFilter) screen.findByText(copiedProjectName) }) }) - }) - describe('search', function () { - it('shows only projects based on the input', async function () { - const input = screen.getAllByRole('textbox', { - name: /search 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, - }, + describe('notifications', function () { + it('email confirmation alert is displayed', async function () { + screen.getByText( + 'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email' + ) }) - const copyButton = within(tableRows[1]).getAllByLabelText('Copy')[0] - fireEvent.click(copyButton) - - // confirm in modal - const copyConfirmButton = document.querySelector( - 'button[type="submit"]' - ) as HTMLElement - fireEvent.click(copyConfirmButton) - - await fetchMock.flush(true) - expect(fetchMock.done()).to.be.true - - expect(sendSpy).to.be.calledOnce - expect(sendSpy).calledWith('project-list-page-interaction') - - expect(screen.queryByText(copiedProjectName)).to.be.null - - const yourProjectFilter = screen.getAllByText('Your Projects')[0] - fireEvent.click(yourProjectFilter) - screen.findByText(copiedProjectName) }) }) }) diff --git a/services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx b/services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx index c80e81adcb..e88bc145f5 100644 --- a/services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx +++ b/services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx @@ -7,6 +7,8 @@ import { renderWithProjectListContext } from '../../helpers/render-with-context' describe('', function () { beforeEach(async function () { + global.localStorage.clear() + window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-tags', [ { _id: 'abc123def456', @@ -30,6 +32,7 @@ describe('', function () { renderWithProjectListContext() + await fetchMock.flush(true) await waitFor(() => expect(fetchMock.called('/api/project'))) }) @@ -37,7 +40,7 @@ describe('', function () { fetchMock.reset() }) - it('displays the tags list', async function () { + it('displays the tags list', function () { screen.getByRole('heading', { name: 'Tags/Folders', })