mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
dce00bbefe
commit
debe76baa6
4 changed files with 438 additions and 389 deletions
|
@ -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<User, 'features'>
|
||||
|
@ -33,7 +31,7 @@ function Common() {
|
|||
).catch(console.error)
|
||||
}
|
||||
|
||||
if (!totalProjectsCount || !notifications.length) {
|
||||
if (!notifications.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -135,6 +135,11 @@ function ProjectListPageContent() {
|
|||
mdOffset={2}
|
||||
className="project-list-empty-col"
|
||||
>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<UserNotifications />
|
||||
</Col>
|
||||
</Row>
|
||||
<WelcomeMessage />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -24,20 +24,17 @@ describe('<ProjectListRoot />', 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(<ProjectListRoot />, {
|
||||
projects: fullList,
|
||||
})
|
||||
await fetchMock.flush(true)
|
||||
await waitFor(() => {
|
||||
screen.findByRole('table')
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -49,360 +46,465 @@ describe('<ProjectListRoot />', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('checkboxes', function () {
|
||||
let allCheckboxes: Array<HTMLInputElement> = []
|
||||
let actionsToolbar: HTMLElement
|
||||
let project1Id: string | null, project2Id: string | null
|
||||
|
||||
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]
|
||||
describe('welcome page', function () {
|
||||
beforeEach(async function () {
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
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<HTMLInputElement>('checkbox')
|
||||
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
|
||||
expect(allCheckboxesChecked.length).to.equal(0)
|
||||
describe('project table', function () {
|
||||
beforeEach(async function () {
|
||||
renderWithProjectListContext(<ProjectListRoot />, {
|
||||
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<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)
|
||||
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<HTMLInputElement> = []
|
||||
let actionsToolbar: HTMLElement
|
||||
let project1Id: string | null, project2Id: string | null
|
||||
|
||||
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')
|
||||
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])
|
||||
|
||||
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<HTMLInputElement>('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<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 allCheckboxes =
|
||||
screen.getAllByRole<HTMLInputElement>('checkbox')
|
||||
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
|
||||
expect(allCheckboxesChecked.length).to.equal(0)
|
||||
})
|
||||
|
||||
const untrashButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<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')
|
||||
})
|
||||
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,
|
||||
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<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')
|
||||
fireEvent.click(moreDropdown)
|
||||
})
|
||||
fireEvent.click(allCheckboxes[0])
|
||||
expect(within(actionsToolbar).queryByText<HTMLElement>('More')).to.be
|
||||
.null
|
||||
})
|
||||
|
||||
const renameButton = screen.getByText<HTMLInputElement>('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<HTMLElement>('More')
|
||||
fireEvent.click(moreDropdown)
|
||||
})
|
||||
|
||||
expect(sendSpy).to.be.calledOnce
|
||||
expect(sendSpy).calledWith('project-list-page-interaction')
|
||||
const renameButton = screen.getByText<HTMLInputElement>('Rename')
|
||||
fireEvent.click(renameButton)
|
||||
|
||||
// same name
|
||||
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
|
||||
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<HTMLInputElement>('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<HTMLInputElement>('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<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')
|
||||
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<HTMLElement>('More')
|
||||
fireEvent.click(moreDropdown)
|
||||
})
|
||||
|
||||
screen.findByText(newProjectName)
|
||||
expect(screen.queryByText(oldName)).to.be.null
|
||||
const copyButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('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('<ProjectListRoot />', function () {
|
|||
archived: false,
|
||||
},
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
const moreDropdown =
|
||||
within(actionsToolbar).getByText<HTMLElement>('More')
|
||||
fireEvent.click(moreDropdown)
|
||||
})
|
||||
|
||||
const copyButton =
|
||||
within(actionsToolbar).getByText<HTMLInputElement>('Make a copy')
|
||||
const copyButton = within(tableRows[1]).getAllByLabelText('Copy')[0]
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
// confirm in modal
|
||||
|
@ -442,71 +536,20 @@ describe('<ProjectListRoot />', 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,6 +7,8 @@ import { renderWithProjectListContext } from '../../helpers/render-with-context'
|
|||
|
||||
describe('<TagsList />', function () {
|
||||
beforeEach(async function () {
|
||||
global.localStorage.clear()
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-tags', [
|
||||
{
|
||||
_id: 'abc123def456',
|
||||
|
@ -30,6 +32,7 @@ describe('<TagsList />', function () {
|
|||
|
||||
renderWithProjectListContext(<TagsList />)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
await waitFor(() => expect(fetchMock.called('/api/project')))
|
||||
})
|
||||
|
||||
|
@ -37,7 +40,7 @@ describe('<TagsList />', function () {
|
|||
fetchMock.reset()
|
||||
})
|
||||
|
||||
it('displays the tags list', async function () {
|
||||
it('displays the tags list', function () {
|
||||
screen.getByRole('heading', {
|
||||
name: 'Tags/Folders',
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue