Merge pull request #9865 from overleaf/ab-display-notifications-welcome-page

[web] Display notifications on the react dashboard welcome page

GitOrigin-RevId: 29fb08bbac195c2766dd0e94dbe9e9a0c7065e76
This commit is contained in:
Alexandre Bourdin 2022-10-12 16:17:09 +02:00 committed by Copybot
parent dce00bbefe
commit debe76baa6
4 changed files with 438 additions and 389 deletions

View file

@ -5,7 +5,6 @@ import Notification from '../notification'
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
import useAsyncDismiss from '../hooks/useAsyncDismiss' import useAsyncDismiss from '../hooks/useAsyncDismiss'
import { useProjectListContext } from '../../../context/project-list-context'
import useAsync from '../../../../../shared/hooks/use-async' import useAsync from '../../../../../shared/hooks/use-async'
import { FetchError, postJSON } from '../../../../../infrastructure/fetch-json' import { FetchError, postJSON } from '../../../../../infrastructure/fetch-json'
import { ExposedSettings } from '../../../../../../../types/exposed-settings' import { ExposedSettings } from '../../../../../../../types/exposed-settings'
@ -14,7 +13,6 @@ import { User } from '../../../../../../../types/user'
function Common() { function Common() {
const { t } = useTranslation() const { t } = useTranslation()
const { totalProjectsCount } = useProjectListContext()
const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
const notifications = getMeta('ol-notifications', []) as NotificationType[] const notifications = getMeta('ol-notifications', []) as NotificationType[]
const user = getMeta('ol-user', []) as Pick<User, 'features'> const user = getMeta('ol-user', []) as Pick<User, 'features'>
@ -33,7 +31,7 @@ function Common() {
).catch(console.error) ).catch(console.error)
} }
if (!totalProjectsCount || !notifications.length) { if (!notifications.length) {
return null return null
} }

View file

@ -135,6 +135,11 @@ function ProjectListPageContent() {
mdOffset={2} mdOffset={2}
className="project-list-empty-col" className="project-list-empty-col"
> >
<Row>
<Col xs={12}>
<UserNotifications />
</Col>
</Row>
<WelcomeMessage /> <WelcomeMessage />
</Col> </Col>
</Row> </Row>

View file

@ -24,20 +24,17 @@ describe('<ProjectListRoot />', function () {
sendSpy = sinon.spy(eventTracking, 'send') sendSpy = sinon.spy(eventTracking, 'send')
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-tags', []) 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 window.user_id = userId
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { assign: locationStub }, value: { assign: locationStub },
}) })
renderWithProjectListContext(<ProjectListRoot />, {
projects: fullList,
})
await fetchMock.flush(true)
await waitFor(() => {
screen.findByRole('table')
})
}) })
afterEach(function () { afterEach(function () {
@ -49,360 +46,465 @@ describe('<ProjectListRoot />', function () {
}) })
}) })
describe('checkboxes', function () { describe('welcome page', function () {
let allCheckboxes: Array<HTMLInputElement> = [] beforeEach(async function () {
let actionsToolbar: HTMLElement renderWithProjectListContext(<ProjectListRoot />, {
let project1Id: string | null, project2Id: string | null projects: [],
describe('all projects', function () {
beforeEach(function () {
allCheckboxes = screen.getAllByRole<HTMLInputElement>('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]
}) })
await fetchMock.flush(true)
})
it('downloads all selected projects and then unselects them', async function () { it('the welcome page is displayed', async function () {
const downloadButton = within(actionsToolbar).getByLabelText('Download') screen.getByRole('heading', { name: 'Welcome to Overleaf!' })
fireEvent.click(downloadButton) })
await waitFor(() => { it('the email confirmation alert is not displayed', async function () {
expect(locationStub).to.have.been.called expect(
}) screen.queryByText(
'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email'
sinon.assert.calledWithMatch(
locationStub,
`/project/download/zip?project_ids=${project1Id},${project2Id}`
) )
).to.be.null
})
})
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox') describe('project table', function () {
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) beforeEach(async function () {
expect(allCheckboxesChecked.length).to.equal(0) renderWithProjectListContext(<ProjectListRoot />, {
projects: fullList,
}) })
await fetchMock.flush(true)
it('opens archive modal for all selected projects and archives all', async function () { await waitFor(() => {
fetchMock.post( screen.findByRole('table')
`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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('checkbox')
expect(allCheckboxes.length).to.equal(currentList.length + 1)
}) })
}) })
describe('archived projects', function () { describe('checkboxes', function () {
beforeEach(function () { let allCheckboxes: Array<HTMLInputElement> = []
const filterButton = screen.getAllByText('Archived Projects')[0] let actionsToolbar: HTMLElement
fireEvent.click(filterButton) let project1Id: string | null, project2Id: string | null
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox') describe('all projects', function () {
expect(allCheckboxes.length === 2).to.be.true beforeEach(function () {
// first one is the select all checkbox allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
fireEvent.click(allCheckboxes[1]) // first one is the select all checkbox
project1Id = allCheckboxes[1].getAttribute('data-project-id') fireEvent.click(allCheckboxes[1])
fireEvent.click(allCheckboxes[2])
actionsToolbar = screen.getAllByRole('toolbar')[0] project1Id = allCheckboxes[1].getAttribute('data-project-id')
}) project2Id = allCheckboxes[2].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 = it('downloads all selected projects and then unselects them', async function () {
within(actionsToolbar).getByText<HTMLInputElement>('Restore') const downloadButton =
fireEvent.click(unarchiveButton) within(actionsToolbar).getByLabelText('Download')
fireEvent.click(downloadButton)
await fetchMock.flush(true) await waitFor(() => {
expect(fetchMock.done()).to.be.true 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 () { const allCheckboxes =
// beforeEach selected all, so uncheck the 1st project screen.getAllByRole<HTMLInputElement>('checkbox')
fireEvent.click(allCheckboxes[1]) const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(0)
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<HTMLInputElement>('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<HTMLInputElement>('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 = it('opens archive modal for all selected projects and archives all', async function () {
within(actionsToolbar).getByText<HTMLInputElement>('Restore') fetchMock.post(
fireEvent.click(untrashButton) `express:/project/${project1Id}/archive`,
{
status: 200,
},
{ delay: 0 }
)
fetchMock.post(
`express:/project/${project2Id}/archive`,
{
status: 200,
},
{ delay: 0 }
)
await fetchMock.flush(true) const archiveButton = within(actionsToolbar).getByLabelText('Archive')
expect(fetchMock.done()).to.be.true 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 () { await fetchMock.flush(true)
// beforeEach selected all, so uncheck the 1st project expect(fetchMock.done()).to.be.true
fireEvent.click(allCheckboxes[1])
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) const requests = fetchMock.calls()
expect(allCheckboxesChecked.length).to.equal(trashedList.length - 1) const [projectRequest1Url, projectRequest1Headers] = requests[2]
expect(projectRequest1Url).to.equal(`/project/${project1Id}/archive`)
await fetchMock.flush(true) expect(projectRequest1Headers?.method).to.equal('POST')
expect(fetchMock.done()).to.be.true const [projectRequest2Url, projectRequest2Headers] = requests[3]
expect(projectRequest2Url).to.equal(`/project/${project2Id}/archive`)
expect(screen.queryByText('No projects')).to.be.null expect(projectRequest2Headers?.method).to.equal('POST')
})
it('removes project from view when archiving', async function () {
fetchMock.post(`express:/project/:id/archive`, {
status: 200,
}) })
const untrashButton = it('opens trash modal for all selected projects and trashes all', async function () {
within(actionsToolbar).getByLabelText<HTMLInputElement>('Archive') fetchMock.post(
fireEvent.click(untrashButton) `express:/project/${project1Id}/trash`,
{
status: 200,
},
{ delay: 0 }
)
fetchMock.post(
`express:/project/${project2Id}/trash`,
{
status: 200,
},
{ delay: 0 }
)
const confirmButton = screen.getByText<HTMLInputElement>('Confirm') const archiveButton = within(actionsToolbar).getByLabelText('Trash')
fireEvent.click(confirmButton) fireEvent.click(archiveButton)
expect(confirmButton.disabled).to.be.true
await fetchMock.flush(true) const confirmBtn = screen.getByText('Confirm') as HTMLButtonElement
expect(fetchMock.done()).to.be.true 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 () { const requests = fetchMock.calls()
beforeEach(async function () { const [projectRequest1Url, projectRequest1Headers] = requests[2]
const filterButton = screen.getAllByText('All Projects')[0] expect(projectRequest1Url).to.equal(`/project/${project1Id}/trash`)
fireEvent.click(filterButton) expect(projectRequest1Headers?.method).to.equal('POST')
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox') const [projectRequest2Url, projectRequest2Headers] = requests[3]
// first one is the select all checkbox expect(projectRequest2Url).to.equal(`/project/${project2Id}/trash`)
fireEvent.click(allCheckboxes[2]) expect(projectRequest2Headers?.method).to.equal('POST')
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<HTMLElement>('More')
})
fireEvent.click(allCheckboxes[0])
expect(within(actionsToolbar).queryByText<HTMLElement>('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,
}) })
await waitFor(() => { it('only checks the projects that are viewable when there is a load more button', async function () {
const moreDropdown = // first one is the select all checkbox
fireEvent.click(allCheckboxes[0])
allCheckboxes = screen.getAllByRole<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('Archive')
fireEvent.click(untrashButton)
const confirmButton = screen.getByText<HTMLInputElement>('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<HTMLInputElement>('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<HTMLElement>('More') within(actionsToolbar).getByText<HTMLElement>('More')
fireEvent.click(moreDropdown) })
fireEvent.click(allCheckboxes[0])
expect(within(actionsToolbar).queryByText<HTMLElement>('More')).to.be
.null
}) })
const renameButton = screen.getByText<HTMLInputElement>('Rename') it('opens the rename modal, and can rename the project, and view updated', async function () {
fireEvent.click(renameButton) fetchMock.post(`express:/project/:id/rename`, {
status: 200,
})
const modal = screen.getAllByRole('dialog')[0] await waitFor(() => {
const moreDropdown =
within(actionsToolbar).getByText<HTMLElement>('More')
fireEvent.click(moreDropdown)
})
expect(sendSpy).to.be.calledOnce const renameButton = screen.getByText<HTMLInputElement>('Rename')
expect(sendSpy).calledWith('project-list-page-interaction') fireEvent.click(renameButton)
// same name const modal = screen.getAllByRole('dialog')[0]
let confirmButton = within(modal).getByText<HTMLInputElement>('Rename')
expect(confirmButton.disabled).to.be.true
let input = screen.getByLabelText('New Name') as HTMLButtonElement
const oldName = input.value
// no name expect(sendSpy).to.be.calledOnce
let newProjectName = '' expect(sendSpy).calledWith('project-list-page-interaction')
input = screen.getByLabelText('New Name') as HTMLButtonElement
fireEvent.change(input, {
target: { value: newProjectName },
})
confirmButton = within(modal).getByText<HTMLInputElement>('Rename')
expect(confirmButton.disabled).to.be.true
// a valid name // same name
newProjectName = 'A new project name' let confirmButton =
input = screen.getByLabelText('New Name') as HTMLButtonElement within(modal).getByText<HTMLInputElement>('Rename')
fireEvent.change(input, { expect(confirmButton.disabled).to.be.true
target: { value: newProjectName }, 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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(0)
}) })
confirmButton = within(modal).getByText<HTMLInputElement>('Rename') it('opens the copy modal, can copy the project, and view updated', async function () {
expect(confirmButton.disabled).to.be.false const tableRows = screen.getAllByRole('row')
fireEvent.click(confirmButton) 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) await waitFor(() => {
expect(fetchMock.done()).to.be.true const moreDropdown =
within(actionsToolbar).getByText<HTMLElement>('More')
fireEvent.click(moreDropdown)
})
screen.findByText(newProjectName) const copyButton =
expect(screen.queryByText(oldName)).to.be.null within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
fireEvent.click(copyButton)
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox') // confirm in modal
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked) const copyConfirmButton = document.querySelector(
expect(allCheckboxesChecked.length).to.equal(0) '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 tableRows = screen.getAllByRole('row')
const linkForProjectToCopy = within(tableRows[1]).getByRole('link') const linkForProjectToCopy = within(tableRows[1]).getByRole('link')
const projectNameToCopy = linkForProjectToCopy.textContent || '' // needed for type checking const projectNameToCopy = linkForProjectToCopy.textContent
screen.findByText(projectNameToCopy) // make sure not just empty string const copiedProjectName = `${projectNameToCopy} Copy`
const copiedProjectName = `${projectNameToCopy} (Copy)`
fetchMock.post(`express:/project/:id/clone`, { fetchMock.post(`express:/project/:id/clone`, {
status: 200, status: 200,
body: { body: {
@ -419,15 +521,7 @@ describe('<ProjectListRoot />', function () {
archived: false, archived: false,
}, },
}) })
const copyButton = within(tableRows[1]).getAllByLabelText('Copy')[0]
await waitFor(() => {
const moreDropdown =
within(actionsToolbar).getByText<HTMLElement>('More')
fireEvent.click(moreDropdown)
})
const copyButton =
within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
fireEvent.click(copyButton) fireEvent.click(copyButton)
// confirm in modal // confirm in modal
@ -442,71 +536,20 @@ describe('<ProjectListRoot />', function () {
expect(sendSpy).to.be.calledOnce expect(sendSpy).to.be.calledOnce
expect(sendSpy).calledWith('project-list-page-interaction') 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) screen.findByText(copiedProjectName)
}) })
}) })
})
describe('search', function () { describe('notifications', function () {
it('shows only projects based on the input', async function () { it('email confirmation alert is displayed', async function () {
const input = screen.getAllByRole('textbox', { screen.getByText(
name: /search projects/i, 'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email'
})[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]).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)
}) })
}) })
}) })

View file

@ -7,6 +7,8 @@ import { renderWithProjectListContext } from '../../helpers/render-with-context'
describe('<TagsList />', function () { describe('<TagsList />', function () {
beforeEach(async function () { beforeEach(async function () {
global.localStorage.clear()
window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-tags', [ window.metaAttributesCache.set('ol-tags', [
{ {
_id: 'abc123def456', _id: 'abc123def456',
@ -30,6 +32,7 @@ describe('<TagsList />', function () {
renderWithProjectListContext(<TagsList />) renderWithProjectListContext(<TagsList />)
await fetchMock.flush(true)
await waitFor(() => expect(fetchMock.called('/api/project'))) await waitFor(() => expect(fetchMock.called('/api/project')))
}) })
@ -37,7 +40,7 @@ describe('<TagsList />', function () {
fetchMock.reset() fetchMock.reset()
}) })
it('displays the tags list', async function () { it('displays the tags list', function () {
screen.getByRole('heading', { screen.getByRole('heading', {
name: 'Tags/Folders', name: 'Tags/Folders',
}) })