Merge pull request #23940 from overleaf/td-react-18

Upgrade to React 18

GitOrigin-RevId: 9b81936e6eea2bccd97fe5c2c5841f0b946371b8
This commit is contained in:
Tim Down
2025-05-02 08:05:29 +00:00
committed by Copybot
parent 4464320757
commit 7abafb01ea
213 changed files with 2469 additions and 2105 deletions
@@ -1,7 +1,7 @@
import { EditorProviders } from '../../helpers/editor-providers'
import PdfJsViewer from '../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
import { mockScope } from './scope'
import { getContainerEl } from 'cypress/react'
import { getContainerEl } from 'cypress/react18'
import { unmountComponentAtNode } from 'react-dom'
import { PdfPreviewProvider } from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-provider'
@@ -18,7 +18,7 @@ describe('<PdfLogsEntries/>', function () {
entity: { _id: '123', name: '123 Doc' },
}
const FileTreePathProvider: FC = ({ children }) => (
const FileTreePathProvider: FC<React.PropsWithChildren> = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
@@ -34,7 +34,7 @@ describe('<PdfLogsEntries/>', function () {
</FileTreePathContext.Provider>
)
const EditorManagerProvider: FC = ({ children }) => {
const EditorManagerProvider: FC<React.PropsWithChildren> = ({ children }) => {
const value = {
openDocWithId: cy.spy().as('openDocWithId'),
// @ts-ignore
@@ -23,6 +23,7 @@ describe('<ChatPane />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-user', user)
window.metaAttributesCache.set('ol-chatEnabled', true)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
afterEach(function () {
@@ -1,7 +1,7 @@
// Disable prop type checks for test harnesses
/* eslint-disable react/prop-types */
import { renderHook, act } from '@testing-library/react-hooks/dom'
import { renderHook, act, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
@@ -30,6 +30,7 @@ describe('ChatContext', function () {
window.metaAttributesCache.set('ol-user', user)
window.metaAttributesCache.set('ol-chatEnabled', true)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
this.stub = sinon.stub(chatClientIdGenerator, 'generate').returns(uuidValue)
})
@@ -43,8 +44,9 @@ describe('ChatContext', function () {
describe('socket connection', function () {
beforeEach(function () {
// Mock GET messages to return no messages
// FIXME?
// fetchMock.get('express:/project/:projectId/messages', [])
fetchMock.get('express:/project/:projectId/messages', [], {
name: 'fetchMessages',
})
// Mock POST new message to return 200
fetchMock.post('express:/project/:projectId/messages', 200)
@@ -72,13 +74,15 @@ describe('ChatContext', function () {
it('adds received messages to the list', async function () {
// Mock socket: we only need to emit events, not mock actual connections
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
// Wait until initial messages have loaded
result.current.loadInitialMessages()
await waitForNextUpdate()
await waitFor(
() => expect(result.current.initialMessagesLoaded).to.be.true
)
// No messages shown at first
expect(result.current.messages).to.deep.equal([])
@@ -95,30 +99,34 @@ describe('ChatContext', function () {
},
})
const message = result.current.messages[0]
expect(message.id).to.equal('msg_1')
expect(message.contents).to.deep.equal(['new message'])
await waitFor(() => {
const message = result.current.messages[0]
expect(message.id).to.equal('msg_1')
expect(message.contents).to.deep.equal(['new message'])
})
})
it('deduplicate messages from preloading', async function () {
// Mock socket: we only need to emit events, not mock actual connections
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
fetchMock.get('express:/project/:projectId/messages', [
{
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user: {
id: 'another_fake_user',
first_name: 'another_fake_user_first_name',
email: 'another_fake@example.com',
fetchMock.modifyRoute('fetchMessages', {
response: () => [
{
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user: {
id: 'another_fake_user',
first_name: 'another_fake_user_first_name',
email: 'another_fake@example.com',
},
},
},
])
],
})
// Mock message being received from another user
socket.emitToClient('new-chat-message', {
@@ -133,11 +141,13 @@ describe('ChatContext', function () {
})
// Check if received the message ID
expect(result.current.messages).to.have.length(1)
await waitFor(() => expect(result.current.messages).to.have.length(1))
// Wait until initial messages have loaded
result.current.loadInitialMessages()
await waitForNextUpdate()
await waitFor(
() => expect(result.current.initialMessagesLoaded).to.be.true
)
// Check if there are no message duplication
expect(result.current.messages).to.have.length(1)
@@ -150,29 +160,30 @@ describe('ChatContext', function () {
it('deduplicate messages from websocket', async function () {
// Mock socket: we only need to emit events, not mock actual connections
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
fetchMock.get('express:/project/:projectId/messages', [
{
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user: {
id: 'another_fake_user',
first_name: 'another_fake_user_first_name',
email: 'another_fake@example.com',
fetchMock.modifyRoute('fetchMessages', {
response: [
{
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user: {
id: 'another_fake_user',
first_name: 'another_fake_user_first_name',
email: 'another_fake@example.com',
},
},
},
])
],
})
// Wait until initial messages have loaded
result.current.loadInitialMessages()
await waitForNextUpdate()
// Check if received the message ID
expect(result.current.messages).to.have.length(1)
await waitFor(() => expect(result.current.messages).to.have.length(1))
// Mock message being received from another user
socket.emitToClient('new-chat-message', {
@@ -196,13 +207,15 @@ describe('ChatContext', function () {
it("doesn't add received messages from the current user if a message was just sent", async function () {
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
// Wait until initial messages have loaded
result.current.loadInitialMessages()
await waitForNextUpdate()
await waitFor(
() => expect(result.current.initialMessagesLoaded).to.be.true
)
// Send a message from the current user
const sentMsg = 'sent message'
@@ -219,7 +232,7 @@ describe('ChatContext', function () {
})
})
expect(result.current.messages).to.have.length(1)
await waitFor(() => expect(result.current.messages).to.have.length(1))
const [message] = result.current.messages
@@ -228,17 +241,21 @@ describe('ChatContext', function () {
it('adds the new message from the current user if another message was received after sending', async function () {
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
// Wait until initial messages have loaded
result.current.loadInitialMessages()
await waitForNextUpdate()
await waitFor(
() => expect(result.current.initialMessagesLoaded).to.be.true
)
// Send a message from the current user
const sentMsg = 'sent message from current user'
result.current.sendMessage(sentMsg)
act(() => {
result.current.sendMessage(sentMsg)
})
const [sentMessageFromCurrentUser] = result.current.messages
expect(sentMessageFromCurrentUser.contents).to.deep.equal([sentMsg])
@@ -295,24 +312,29 @@ describe('ChatContext', function () {
})
it('adds messages to the list', async function () {
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadInitialMessages()
await waitForNextUpdate()
expect(result.current.messages[0].contents).to.deep.equal(['a message'])
await waitFor(() =>
expect(result.current.messages[0].contents).to.deep.equal(['a message'])
)
})
it("won't load messages a second time", async function () {
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadInitialMessages()
await waitForNextUpdate()
expect(result.current.initialMessagesLoaded).to.equal(true)
await waitFor(() =>
expect(result.current.initialMessagesLoaded).to.equal(true)
)
// Calling a second time won't do anything
result.current.loadInitialMessages()
await waitFor(
() => expect(result.current.initialMessagesLoaded).to.be.true
)
expect(
fetchMock.callHistory.calls('express:/project/:projectId/messages')
).to.have.lengthOf(1)
@@ -321,12 +343,10 @@ describe('ChatContext', function () {
it('provides an error on failure', async function () {
fetchMock.removeRoutes().clearHistory()
fetchMock.get('express:/project/:projectId/messages', 500)
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadInitialMessages()
await waitForNextUpdate()
expect(result.current.error).to.exist
await waitFor(() => expect(result.current.error).to.exist)
expect(result.current.status).to.equal('error')
})
})
@@ -343,14 +363,14 @@ describe('ChatContext', function () {
},
])
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadMoreMessages()
await waitForNextUpdate()
expect(result.current.messages[0].contents).to.deep.equal([
'first message',
])
await waitFor(() =>
expect(result.current.messages[0].contents).to.deep.equal([
'first message',
])
)
// The before query param is not set
expect(getLastFetchMockQueryParam('before')).to.be.null
@@ -373,20 +393,23 @@ describe('ChatContext', function () {
},
])
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadMoreMessages()
await waitForNextUpdate()
await waitFor(() =>
expect(result.current.messages[0].contents).to.have.length(50)
)
// Call a second time
result.current.loadMoreMessages()
await waitForNextUpdate()
// The second request is added to the list
// Since both messages from the same user, they are collapsed into the
// same "message"
expect(result.current.messages[0].contents).to.include(
'message from second page'
await waitFor(() =>
expect(result.current.messages[0].contents).to.include(
'message from second page'
)
)
// The before query param for the second request matches the timestamp
@@ -396,23 +419,25 @@ describe('ChatContext', function () {
})
it("won't load more messages if there are no more messages", async function () {
// Mock a GET request for 49 messages. This is less the the full page size
// (50 messages), meaning that there are no further messages to be loaded
// Mock a GET request for 49 messages. This is less than the full page
// size (50 messages), meaning that there are no further messages to be
// loaded
fetchMock.getOnce(
'express:/project/:projectId/messages',
createMessages(49, user)
)
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadMoreMessages()
await waitForNextUpdate()
expect(result.current.messages[0].contents).to.have.length(49)
await waitFor(() =>
expect(result.current.messages[0].contents).to.have.length(49)
)
result.current.loadMoreMessages()
expect(result.current.atEnd).to.be.true
await waitFor(() => expect(result.current.atEnd).to.be.true)
expect(
fetchMock.callHistory.calls('express:/project/:projectId/messages')
).to.have.lengthOf(1)
@@ -429,7 +454,7 @@ describe('ChatContext', function () {
)
const socket = new SocketIOMock()
const { result, waitForNextUpdate } = renderChatContextHook({
const { result } = renderChatContextHook({
socket,
})
@@ -458,28 +483,27 @@ describe('ChatContext', function () {
timestamp: Date.now(),
},
])
await waitForNextUpdate()
// Although the loaded message was resolved last, it appears first (since
// requested messages must have come first)
const messageContents = result.current.messages.map(
({ contents }) => contents[0]
)
expect(messageContents).to.deep.equal([
'loaded message',
'socket message',
])
await waitFor(() => {
// Although the loaded message was resolved last, it appears first (since
// requested messages must have come first)
const messageContents = result.current.messages.map(
({ contents }) => contents[0]
)
expect(messageContents).to.deep.equal([
'loaded message',
'socket message',
])
})
})
it('provides an error on failures', async function () {
fetchMock.removeRoutes().clearHistory()
fetchMock.get('express:/project/:projectId/messages', 500)
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.loadMoreMessages()
await waitForNextUpdate()
expect(result.current.error).to.exist
await waitFor(() => expect(result.current.error).to.exist)
expect(result.current.status).to.equal('error')
})
})
@@ -493,14 +517,16 @@ describe('ChatContext', function () {
.postOnce('express:/project/:projectId/messages', 200)
})
it('optimistically adds the message to the list', function () {
it('optimistically adds the message to the list', async function () {
const { result } = renderChatContextHook({})
result.current.sendMessage('sent message')
expect(result.current.messages[0].contents).to.deep.equal([
'sent message',
])
await waitFor(() =>
expect(result.current.messages[0].contents).to.deep.equal([
'sent message',
])
)
})
it('POSTs the message to the backend', function () {
@@ -534,12 +560,11 @@ describe('ChatContext', function () {
fetchMock
.get('express:/project/:projectId/messages', [])
.postOnce('express:/project/:projectId/messages', 500)
const { result, waitForNextUpdate } = renderChatContextHook({})
const { result } = renderChatContextHook({})
result.current.sendMessage('sent message')
await waitForNextUpdate()
await waitFor(() => expect(result.current.error).to.exist)
expect(result.current.error).to.exist
expect(result.current.status).to.equal('error')
})
})
@@ -555,11 +580,13 @@ describe('ChatContext', function () {
const { result } = renderChatContextHook({ socket })
// Receive a new message from the socket
socket.emitToClient('new-chat-message', {
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user,
act(() => {
socket.emitToClient('new-chat-message', {
id: 'msg_1',
content: 'new message',
timestamp: Date.now(),
user,
})
})
expect(result.current.unreadMessageCount).to.equal(1)
@@ -8,6 +8,7 @@ import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<EditorCloneProjectModalWrapper />', function () {
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
after(function () {
@@ -4,22 +4,17 @@ import sinon from 'sinon'
import { expect } from 'chai'
import ActionsCopyProject from '../../../../../frontend/js/features/editor-left-menu/components/actions-copy-project'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
describe('<ActionsCopyProject />', function () {
let assignStub
beforeEach(function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
})
@@ -53,6 +48,7 @@ describe('<ActionsCopyProject />', function () {
expect(button.textContent).to.equal('Copying…')
})
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.calledOnceWith('/project/new-project')
})
@@ -1,7 +1,7 @@
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import { screen, waitFor } from '@testing-library/react'
import LayoutDropdownButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import * as eventTracking from '@/infrastructure/event-tracking'
@@ -31,13 +31,15 @@ describe('<LayoutDropdownButton />', function () {
screen.getByRole('button', { name: 'Layout' }).click()
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
@@ -64,7 +66,7 @@ describe('<LayoutDropdownButton />', function () {
).to.equal('false')
})
it('should not select any option in history view', function () {
it('should not select any option in history view', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
ui: { ...defaultUi, view: 'history' },
@@ -72,13 +74,15 @@ describe('<LayoutDropdownButton />', function () {
screen.getByRole('button', { name: 'Layout' }).click()
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
@@ -105,7 +109,7 @@ describe('<LayoutDropdownButton />', function () {
).to.equal('false')
})
it('should treat file and editor views the same way', function () {
it('should treat file and editor views the same way', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
ui: {
@@ -116,13 +120,15 @@ describe('<LayoutDropdownButton />', function () {
screen.getByRole('button', { name: 'Layout' }).click()
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
@@ -149,9 +155,9 @@ describe('<LayoutDropdownButton />', function () {
).to.equal('false')
})
describe('on detach', function () {
describe('on detach', async function () {
let originalBroadcastChannel
beforeEach(function () {
beforeEach(async function () {
window.BroadcastChannel = originalBroadcastChannel || true // ensure that window.BroadcastChannel is truthy
renderWithEditorContext(<LayoutDropdownButton />, {
@@ -160,19 +166,21 @@ describe('<LayoutDropdownButton />', function () {
screen.getByRole('button', { name: 'Layout' }).click()
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.click()
)
})
afterEach(function () {
window.BroadcastChannel = originalBroadcastChannel
})
it('should show processing', function () {
screen.getByText('Layout processing')
it('should show processing', async function () {
await screen.findByText('Layout processing')
})
it('should record event', function () {
@@ -180,8 +188,8 @@ describe('<LayoutDropdownButton />', function () {
})
})
describe('on layout change / reattach', function () {
beforeEach(function () {
describe('on layout change / reattach', async function () {
beforeEach(async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<LayoutDropdownButton />, {
ui: { ...defaultUi, view: 'editor' },
@@ -189,11 +197,13 @@ describe('<LayoutDropdownButton />', function () {
screen.getByRole('button', { name: 'Layout' }).click()
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.click()
)
})
it('should not show processing', function () {
@@ -28,6 +28,7 @@ describe('<ToolbarHeader />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-chatEnabled', true)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
describe('cobranding logo', function () {
@@ -1,7 +1,7 @@
import FileTreeFolder from '../../../../../frontend/js/features/file-tree/components/file-tree-folder'
import { EditorProviders } from '../../../helpers/editor-providers'
import { FileTreeProvider } from '../helpers/file-tree-provider'
import { getContainerEl } from 'cypress/react'
import { getContainerEl } from 'cypress/react18'
import ReactDom from 'react-dom'
describe('<FileTreeFolder/>', function () {
@@ -1,9 +1,11 @@
import { ComponentProps, FC, useRef, useState } from 'react'
import FileTreeContext from '@/features/file-tree/components/file-tree-context'
export const FileTreeProvider: FC<{
refProviders?: Record<string, boolean>
}> = ({ children, refProviders = {} }) => {
export const FileTreeProvider: FC<
React.PropsWithChildren<{
refProviders?: Record<string, boolean>
}>
> = ({ children, refProviders = {} }) => {
const [fileTreeContainer, setFileTreeContainer] =
useState<HTMLDivElement | null>(null)
@@ -20,6 +20,7 @@ describe('<FileViewText/>', function () {
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
it('renders a text view', async function () {
@@ -33,6 +33,7 @@ describe('<FileView/>', function () {
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
describe('for a text file', function () {
@@ -95,7 +95,7 @@ const createInitialValue = () =>
setProjectSearchIsOpen: cy.stub(),
}) satisfies LayoutContextValue
const LayoutProvider: FC = ({ children }) => {
const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
const [value] = useState(createInitialValue)
return (
@@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ReactElement } from 'react'
import { FC, PropsWithChildren, ReactElement } from 'react'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import UnlinkUserModal from '@/features/group-management/components/members-table/unlink-user-modal'
@@ -7,11 +7,9 @@ import { GroupMembersProvider } from '@/features/group-management/context/group-
import { expect } from 'chai'
export function renderWithContext(component: ReactElement, props = {}) {
const GroupMembersProviderWrapper = ({
children,
}: {
children: ReactElement
}) => <GroupMembersProvider {...props}>{children}</GroupMembersProvider>
const GroupMembersProviderWrapper: FC<PropsWithChildren> = ({ children }) => (
<GroupMembersProvider {...props}>{children}</GroupMembersProvider>
)
return render(component, { wrapper: GroupMembersProviderWrapper })
}
@@ -59,7 +59,7 @@ const highlights: Highlight[] = [
},
]
const Container: FC = ({ children }) => (
const Container: FC<React.PropsWithChildren> = ({ children }) => (
<div style={{ width: 600, height: 400 }}>{children}</div>
)
@@ -3,23 +3,16 @@ import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import ModalContentNewProjectForm from '../../../../../../frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
describe('<ModalContentNewProjectForm />', function () {
let assignStub: sinon.SinonStub
beforeEach(function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
})
@@ -51,10 +44,11 @@ describe('<ModalContentNewProjectForm />', function () {
expect(newProjectMock.callHistory.called()).to.be.true
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
sinon.assert.calledOnce(assignStub)
sinon.assert.calledWith(assignStub, `/project/${projectId}`)
})
sinon.assert.calledWith(assignStub, `/project/${projectId}`)
})
it('shows error when project name contains "/"', async function () {
@@ -41,7 +41,7 @@ import { Project } from '../../../../../types/project/dashboard/api'
import GroupsAndEnterpriseBanner from '../../../../../frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner'
import GroupSsoSetupSuccess from '../../../../../frontend/js/features/project-list/components/notifications/groups/group-sso-setup-success'
import localStorage from '@/infrastructure/local-storage'
import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import {
commonsSubscription,
freeSubscription,
@@ -50,7 +50,9 @@ import {
} from '../fixtures/user-subscriptions'
import getMeta from '@/utils/meta'
const renderWithinProjectListProvider = (Component: React.ComponentType) => {
const renderWithinProjectListProvider = (
Component: React.ComponentType<React.PropsWithChildren>
) => {
render(<Component />, {
wrapper: ({ children }) => (
<ProjectListProvider>
@@ -655,12 +657,10 @@ describe('<UserNotifications />', function () {
window.metaAttributesCache.set('ol-user', {
signUpDate: new Date('2024-01-01').toISOString(),
})
this.clock = sinon.useFakeTimers(new Date('2025-07-01').getTime())
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
this.clock.restore()
})
function testUnconfirmedNotification(
@@ -675,7 +675,9 @@ describe('<UserNotifications />', function () {
fetchMock.post('/user/emails/resend_confirmation', 200)
const email = userEmails[0].email
const notificationBody = screen.getByTestId('pro-notification-body')
const notificationBody = await screen.findByTestId(
'pro-notification-body'
)
if (isPrimary) {
expect(notificationBody.textContent).to.contain(
@@ -691,7 +693,7 @@ describe('<UserNotifications />', function () {
fireEvent.click(resendButton)
await waitForElementToBeRemoved(() =>
screen.getByRole('button', { name: /resend/i })
screen.queryByRole('button', { name: /resend/i })
)
expect(fetchMock.callHistory.called()).to.be.true
@@ -717,7 +719,7 @@ describe('<UserNotifications />', function () {
fetchMock.post('/user/emails/resend_confirmation', 200)
const email = untrustedUserData.email
const notificationBody = screen.getByTestId(
const notificationBody = await screen.findByTestId(
'not-trusted-notification-body'
)
expect(notificationBody.textContent).to.contain(
@@ -742,7 +744,9 @@ describe('<UserNotifications />', function () {
await fetchMock.callHistory.flush(true)
fetchMock.post('/user/emails/resend_confirmation', 500)
const resendButtons = screen.getAllByRole('button', { name: /resend/i })
const resendButtons = await screen.findAllByRole('button', {
name: /resend/i,
})
const resendButton = resendButtons[0]
fireEvent.click(resendButton)
const notificationBody = screen.getByTestId('pro-notification-body')
@@ -767,7 +771,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(ConfirmEmail)
await fetchMock.callHistory.flush(true)
const alert = screen.getByRole('alert')
const alert = await screen.findByRole('alert')
const email = unconfirmedCommonsUserData.email
const notificationBody = within(alert).getByTestId('notification-body')
expect(notificationBody.textContent).to.contain(
@@ -788,7 +792,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(ConfirmEmail)
await fetchMock.callHistory.flush(true)
const alert = screen.getByRole('alert')
const alert = await screen.findByRole('alert')
const email = unconfirmedCommonsUserData.email
const notificationBody = within(alert).getByTestId(
'pro-notification-body'
@@ -808,22 +812,15 @@ describe('<UserNotifications />', function () {
})
describe('<Affiliation/>', function () {
let assignStub: sinon.SinonStub
beforeEach(function () {
Object.assign(getMeta('ol-ExposedSettings'), exposedSettings)
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
fetchMock.removeRoutes().clearHistory()
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
})
@@ -879,9 +876,9 @@ describe('<UserNotifications />', function () {
fireEvent.click(
screen.getByRole('button', { name: /confirm affiliation/i })
)
sinon.assert.calledOnce(assignStub)
sinon.assert.calledOnce(this.locationWrapperStub.assign)
sinon.assert.calledWithMatch(
assignStub,
this.locationWrapperStub.assign,
`${exposedSettings.samlInitPath}?university_id=${professionalUserData.affiliation.institution.id}&reconfirm=/project`
)
})
@@ -944,8 +941,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
.null
await screen.findByRole('button', { name: 'Contact Sales' })
})
it('shows the banner for users that have dismissed the banner more than 30 days ago', async function () {
@@ -960,8 +956,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
expect(screen.queryByRole('button', { name: 'Contact Sales' })).to.not.be
.null
await screen.findByRole('button', { name: 'Contact Sales' })
})
it('does not show the banner for users that have dismissed the banner within the last 30 days', async function () {
@@ -1014,7 +1009,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
screen.getByText(
await screen.findByText(
'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
)
const link = screen.getByRole('button', { name: 'Contact Sales' })
@@ -1031,7 +1026,7 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
screen.getByText(
await screen.findByText(
'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
)
const link = screen.getByRole('button', { name: 'Contact Sales' })
@@ -19,7 +19,7 @@ import {
archiveableProject,
copyableProject,
} from '../fixtures/projects-data'
import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import getMeta from '@/utils/meta'
const {
@@ -37,7 +37,6 @@ describe('<ProjectListRoot />', function () {
this.timeout('10s')
let sendMBSpy: sinon.SinonSpy
let assignStub: sinon.SinonStub
beforeEach(async function () {
global.localStorage.clear()
@@ -68,19 +67,14 @@ describe('<ProjectListRoot />', function () {
window.metaAttributesCache.set('ol-navbar', {
items: [],
})
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
sendMBSpy.restore()
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
describe('welcome page', function () {
@@ -136,6 +130,7 @@ describe('<ProjectListRoot />', function () {
within(actionsToolbar).getByLabelText('Download')
fireEvent.click(downloadButton)
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.called
})
@@ -430,12 +425,15 @@ describe('<ProjectListRoot />', function () {
{ repeat: leavableList.length }
)
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// + 1 because of select all
expect(allCheckboxes.length).to.equal(leavableList.length + 1)
await waitFor(() =>
expect(
screen.getAllByRole<HTMLInputElement>('checkbox').length
).to.equal(leavableList.length + 1)
)
// first one is the select all checkbox
fireEvent.click(allCheckboxes[0])
fireEvent.click(screen.getAllByRole('checkbox')[0])
actionsToolbar = screen.getAllByRole('toolbar')[0]
@@ -491,9 +489,11 @@ describe('<ProjectListRoot />', function () {
{ repeat: deletableList.length }
)
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// + 1 because of select all
expect(allCheckboxes.length).to.equal(deletableList.length + 1)
await waitFor(() => {
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// + 1 because of select all
expect(allCheckboxes.length).to.equal(deletableList.length + 1)
})
// first one is the select all checkbox
fireEvent.click(allCheckboxes[0])
@@ -563,14 +563,15 @@ describe('<ProjectListRoot />', function () {
{ repeat: leavableList.length }
)
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// + 1 because of select all
expect(allCheckboxes.length).to.equal(
deletableAndLeavableList.length + 1
await waitFor(() =>
expect(
screen.getAllByRole<HTMLInputElement>('checkbox').length
).to.equal(deletableAndLeavableList.length + 1)
)
// first one is the select all checkbox
fireEvent.click(allCheckboxes[0])
fireEvent.click(screen.getAllByRole<HTMLInputElement>('checkbox')[0])
actionsToolbar = screen.getAllByRole('toolbar')[0]
@@ -663,9 +664,11 @@ describe('<ProjectListRoot />', function () {
)
).to.be.true
})
expect(
screen.queryAllByText(projectsData[index].name)
).to.have.length(0)
await waitFor(() => {
expect(
screen.queryAllByText(projectsData[index].name)
).to.have.length(0)
})
screen.getAllByRole('button', {
name: `${this.tagName} (${--visibleProjectsCount})`,
@@ -699,13 +702,15 @@ describe('<ProjectListRoot />', function () {
status: 204,
})
await waitFor(() => {
const tagsDropdown = within(actionsToolbar).getByLabelText('Tags')
fireEvent.click(tagsDropdown)
})
const tagsDropdown =
await within(actionsToolbar).findByLabelText('Tags')
fireEvent.click(tagsDropdown)
screen.getByText('Add to tag')
const newTagButton = screen.getByText('Create new tag')
const newTagButton = screen.getByRole('menuitem', {
name: 'Create new tag',
})
fireEvent.click(newTagButton)
const modal = screen.getAllByRole('dialog')[0]
@@ -731,7 +736,11 @@ describe('<ProjectListRoot />', function () {
})
).to.be.true
screen.getByRole('button', { name: `${this.newTagName} (2)` })
await screen.findByRole(
'button',
{ name: `${this.newTagName} (2)` },
{ timeout: 5000 }
)
})
it('opens the tags dropdown and remove a tag from selected projects', async function () {
@@ -949,7 +958,7 @@ describe('<ProjectListRoot />', function () {
).to.be.true
const table = await screen.findByRole('table')
within(table).getByText(newProjectName)
await within(table).findByText(newProjectName)
expect(within(table).queryByText(oldName)).to.be.null
const allCheckboxesInTable =
@@ -1025,7 +1034,7 @@ describe('<ProjectListRoot />', function () {
}
)
screen.getByText(copiedProjectName)
await screen.findByText(copiedProjectName)
})
})
})
@@ -1073,6 +1082,8 @@ describe('<ProjectListRoot />', function () {
}
beforeEach(function () {
fetchMock.removeRoutes().clearHistory()
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
// first one is the select all checkbox, just check 2 at first
fireEvent.click(allCheckboxes[1])
@@ -30,7 +30,7 @@ describe('Add affiliation widget', function () {
await fetchMock.callHistory.flush(true)
await waitFor(() => expect(fetchMock.callHistory.called('/api/project')))
screen.getByText(/are you affiliated with an institution/i)
await screen.findByText(/are you affiliated with an institution/i)
const addAffiliationLink = screen.getByRole('button', {
name: /add affiliation/i,
})
@@ -63,7 +63,7 @@ describe('<TagsList />', function () {
const tag1Button = screen.getByText('Tag 1')
assert.isFalse(tag1Button.closest('li')?.classList.contains('active'))
await fireEvent.click(tag1Button)
fireEvent.click(tag1Button)
assert.isTrue(tag1Button.closest('li')?.classList.contains('active'))
})
@@ -85,7 +85,7 @@ describe('<TagsList />', function () {
name: 'New Tag',
})
await fireEvent.click(newTagButton)
fireEvent.click(newTagButton)
})
it('modal is open', async function () {
@@ -97,7 +97,7 @@ describe('<TagsList />', function () {
const modal = screen.getAllByRole('dialog', { hidden: false })[0]
const cancelButton = within(modal).getByRole('button', { name: 'Cancel' })
await fireEvent.click(cancelButton)
fireEvent.click(cancelButton)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
})
@@ -144,13 +144,15 @@ describe('<TagsList />', function () {
const createButton = within(modal).getByRole('button', { name: 'Create' })
expect(createButton.hasAttribute('disabled')).to.be.false
await fireEvent.click(createButton)
fireEvent.click(createButton)
await waitFor(
() => expect(fetchMock.callHistory.called(`/tag`)).to.be.true
)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
await waitFor(
() => expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
)
screen.getByRole('button', {
name: 'New Tag (0)',
@@ -164,9 +166,9 @@ describe('<TagsList />', function () {
const dropdownToggle = within(
tag1Button.closest('li') as HTMLElement
).getByTestId('tag-dropdown-toggle')
await fireEvent.click(dropdownToggle)
fireEvent.click(dropdownToggle)
const editMenuItem = await screen.findByRole('menuitem', { name: 'Edit' })
await fireEvent.click(editMenuItem)
fireEvent.click(editMenuItem)
})
it('modal is open', async function () {
@@ -178,7 +180,7 @@ describe('<TagsList />', function () {
const modal = screen.getAllByRole('dialog', { hidden: false })[0]
const cancelButton = within(modal).getByRole('button', { name: 'Cancel' })
await fireEvent.click(cancelButton)
fireEvent.click(cancelButton)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
})
@@ -237,13 +239,15 @@ describe('<TagsList />', function () {
const saveButton = within(modal).getByRole('button', { name: 'Save' })
expect(saveButton.hasAttribute('disabled')).to.be.false
await fireEvent.click(saveButton)
fireEvent.click(saveButton)
await waitFor(() =>
expect(fetchMock.callHistory.called(`/tag/abc123def456/rename`))
)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
await waitFor(
() => expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
)
screen.getByRole('button', {
name: 'New Tag Name (1)',
@@ -257,11 +261,11 @@ describe('<TagsList />', function () {
const dropdownToggle = within(
tag1Button.closest('li') as HTMLElement
).getByTestId('tag-dropdown-toggle')
await fireEvent.click(dropdownToggle)
fireEvent.click(dropdownToggle)
const deleteMenuItem = await screen.findByRole('menuitem', {
name: 'Delete',
})
await fireEvent.click(deleteMenuItem)
fireEvent.click(deleteMenuItem)
})
it('modal is open', async function () {
@@ -273,20 +277,22 @@ describe('<TagsList />', function () {
const modal = screen.getAllByRole('dialog', { hidden: false })[0]
const cancelButton = within(modal).getByRole('button', { name: 'Cancel' })
await fireEvent.click(cancelButton)
fireEvent.click(cancelButton)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
})
it('clicking Delete sends a request', async function () {
const modal = screen.getAllByRole('dialog', { hidden: false })[0]
const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
await fireEvent.click(deleteButton)
fireEvent.click(deleteButton)
await waitFor(() =>
expect(fetchMock.callHistory.called(`/tag/bcd234efg567`))
)
expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
await waitFor(
() => expect(screen.queryByRole('dialog', { hidden: false })).to.be.null
)
expect(
screen.queryByRole('button', {
name: 'Another Tag (2)',
@@ -299,13 +305,13 @@ describe('<TagsList />', function () {
const modal = screen.getAllByRole('dialog', { hidden: false })[0]
const deleteButton = within(modal).getByRole('button', { name: 'Delete' })
await fireEvent.click(deleteButton)
fireEvent.click(deleteButton)
await waitFor(() =>
expect(fetchMock.callHistory.called(`/tag/bcd234efg567`))
)
within(modal).getByText('Sorry, something went wrong')
await within(modal).findByText('Sorry, something went wrong')
})
})
})
@@ -24,7 +24,7 @@ describe('<SystemMessages />', function () {
await fetchMock.callHistory.flush(true)
screen.getByText(data.content)
await screen.findByText(data.content)
expect(screen.queryByRole('button', { name: /close/i })).to.be.null
})
@@ -38,7 +38,7 @@ describe('<SystemMessages />', function () {
await fetchMock.callHistory.flush(true)
screen.getByText(data.content)
await screen.findByText(data.content)
const closeBtn = screen.getByRole('button', { name: /close/i })
fireEvent.click(closeBtn)
@@ -2,32 +2,25 @@ import { expect } from 'chai'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import sinon from 'sinon'
import { projectsData } from '../../../../fixtures/projects-data'
import * as useLocationModule from '../../../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import { CompileAndDownloadProjectPDFButtonTooltip } from '../../../../../../../../frontend/js/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button'
import fetchMock from 'fetch-mock'
import * as eventTracking from '@/infrastructure/event-tracking'
describe('<CompileAndDownloadProjectPDFButton />', function () {
let assignStub: sinon.SinonStub
let locationStub: sinon.SinonStub
let sendMBSpy: sinon.SinonSpy
beforeEach(function () {
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
assignStub = sinon.stub()
locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
render(
<CompileAndDownloadProjectPDFButtonTooltip project={projectsData[0]} />
)
})
afterEach(function () {
locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
sendMBSpy.restore()
})
@@ -57,6 +50,7 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
screen.getByRole('button', { name: 'Compiling…' })
})
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.called
})
@@ -85,9 +79,9 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
}) as HTMLButtonElement
fireEvent.click(btn)
await waitFor(() => {
screen.getByText(`${projectsData[0].name}: PDF unavailable for download`)
})
expect(assignStub).to.have.not.been.called
await screen.findByText(
`${projectsData[0].name}: PDF unavailable for download`
)
expect(this.locationWrapperStub.assign).to.have.not.been.called
})
})
@@ -3,24 +3,17 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import sinon from 'sinon'
import { DownloadProjectButtonTooltip } from '../../../../../../../../frontend/js/features/project-list/components/table/cells/action-buttons/download-project-button'
import { projectsData } from '../../../../fixtures/projects-data'
import * as useLocationModule from '../../../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
describe('<DownloadProjectButton />', function () {
let assignStub: sinon.SinonStub
beforeEach(function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
render(<DownloadProjectButtonTooltip project={projectsData[0]} />)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
it('renders tooltip for button', async function () {
@@ -35,6 +28,7 @@ describe('<DownloadProjectButton />', function () {
}) as HTMLButtonElement
fireEvent.click(btn)
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.called
})
@@ -57,7 +57,7 @@ describe('<InlineTags />', function () {
const removeButton = screen.getByRole('button', {
name: 'Remove tag My Test Tag',
})
await fireEvent.click(removeButton)
fireEvent.click(removeButton)
await waitFor(() =>
expect(
fetchMock.callHistory.called(
@@ -1,4 +1,4 @@
import { screen, within, fireEvent } from '@testing-library/react'
import { screen, within, fireEvent, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import ProjectListTable from '../../../../../../frontend/js/features/project-list/components/table/project-list-table'
import { currentProjects } from '../../fixtures/projects-data'
@@ -67,9 +67,13 @@ describe('<ProjectListTable />', function () {
renderWithProjectListContext(<ProjectListTable />)
await fetchMock.callHistory.flush(true)
await waitFor(() =>
expect(screen.getAllByRole('row').length).to.equal(
currentProjects.length + 1
)
)
const rows = screen.getAllByRole('row')
rows.shift() // remove first row since it's the header
expect(rows.length).to.equal(currentProjects.length)
// Project name cell
currentProjects.forEach(project => {
@@ -139,42 +143,60 @@ describe('<ProjectListTable />', function () {
it('selects all projects when header checkbox checked', async function () {
renderWithProjectListContext(<ProjectListTable />)
await fetchMock.callHistory.flush(true)
const checkbox = screen.getByLabelText('Select all projects')
const checkbox = await screen.findByLabelText('Select all projects')
fireEvent.click(checkbox)
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
// + 1 because of select all checkbox
expect(allCheckboxesChecked.length).to.equal(currentProjects.length + 1)
await waitFor(() => {
const allCheckboxes = screen.queryAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
// + 1 because of select all checkbox
expect(allCheckboxesChecked.length).to.equal(currentProjects.length + 1)
})
})
it('unselects all projects when select all checkbox uchecked', async function () {
renderWithProjectListContext(<ProjectListTable />)
await fetchMock.callHistory.flush(true)
const checkbox = screen.getByLabelText('Select all projects')
const checkbox = await screen.findByLabelText('Select all projects')
fireEvent.click(checkbox)
fireEvent.click(checkbox)
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(0)
await waitFor(() => {
const allCheckboxes = screen.queryAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(0)
})
})
it('unselects select all projects checkbox when one project is unchecked', async function () {
renderWithProjectListContext(<ProjectListTable />)
await fetchMock.callHistory.flush(true)
const checkbox = screen.getByLabelText('Select all projects')
const checkbox = await screen.findByLabelText('Select all projects')
fireEvent.click(checkbox)
let allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
expect(allCheckboxes[1].getAttribute('data-project-id')).to.exist // make sure we are unchecking a project checkbox
fireEvent.click(allCheckboxes[1])
allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(currentProjects.length - 1)
await waitFor(() => {
expect(
screen
.getAllByRole<HTMLInputElement>('checkbox')[1]
.getAttribute('data-project-id')
).to.exist // make sure we are unchecking a project checkbox
})
fireEvent.click(screen.getAllByRole<HTMLInputElement>('checkbox')[1])
await waitFor(() => {
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
expect(allCheckboxesChecked.length).to.equal(currentProjects.length - 1)
})
})
it('only checks the checked project', async function () {
renderWithProjectListContext(<ProjectListTable />)
await fetchMock.callHistory.flush(true)
const checkbox = screen.getByLabelText(`Select ${currentProjects[0].name}`)
const checkbox = await screen.findByLabelText(
`Select ${currentProjects[0].name}`
)
fireEvent.click(checkbox)
const allCheckboxes = screen.getAllByRole<HTMLInputElement>('checkbox')
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
@@ -1,4 +1,4 @@
import { render, screen, within } from '@testing-library/react'
import { render, screen, waitFor, within } from '@testing-library/react'
import { expect } from 'chai'
import moment from 'moment/moment'
import fetchMock from 'fetch-mock'
@@ -68,24 +68,30 @@ describe('<ProjectTools />', function () {
fetchMock.removeRoutes().clearHistory()
})
it('does not show the Rename option for a project owned by a different user', function () {
it('does not show the Rename option for a project owned by a different user', async function () {
render(<ProjectListRootInner />)
screen.getByLabelText('Select Starfleet Report (readAndWrite)').click()
screen.getByRole('button', { name: 'More' }).click()
expect(
within(
screen.getByTestId('project-tools-more-dropdown-menu')
).queryByRole('menuitem', { name: 'Rename' })
).to.be.null
await waitFor(
() =>
expect(
within(
screen.getByTestId('project-tools-more-dropdown-menu')
).queryByRole('menuitem', { name: 'Rename' })
).to.be.null
)
})
it('displays the Rename option for a project owned by the current user', function () {
it('displays the Rename option for a project owned by the current user', async function () {
render(<ProjectListRootInner />)
screen.getByLabelText('Select Starfleet Report (owner)').click()
screen.getByRole('button', { name: 'More' }).click()
within(screen.getByTestId('project-tools-more-dropdown-menu'))
.getByRole('menuitem', { name: 'Rename' })
.click()
within(screen.getByRole('dialog')).getByText('Rename Project')
const menu = await screen.findByTestId('project-tools-more-dropdown-menu')
const menuItem = within(menu).getByRole('menuitem', {
name: 'Rename',
})
menuItem.click()
const dialog = await screen.findByRole('dialog')
within(dialog).getByText('Rename Project')
})
})
@@ -92,6 +92,6 @@ describe('<RenameProjectModal />', function () {
await waitFor(() => expect(postRenameMock.callHistory.called()).to.be.true)
screen.getByText('Something went wrong. Please try again.')
await screen.findByText('Something went wrong. Please try again.')
})
})
@@ -120,9 +120,16 @@ describe('<AddEmailInput/>', function () {
describe('when there is a domain match', function () {
beforeEach(async function () {
fetchMock.get('express:/institutions/domains', testInstitutionData)
fireEvent.change(screen.getByTestId('affiliations-email'), {
target: { value: 'user@d' },
})
const input = await screen.findByTestId('affiliations-email')
fireEvent.change(input, { target: { value: 'user@d' } })
// Wait for the request to complete and the domain cache to pouplate
await waitFor(
() =>
expect(
fetchMock.callHistory.called('express:/institutions/domains')
).to.be.true
)
// Wait for component to process the change and update the shadow input
await waitFor(() => {
const shadowInput = screen.getByTestId(
@@ -61,7 +61,9 @@ function resetFetchMock() {
}
async function confirmCodeForEmail(email: string) {
screen.getByText(`Enter the 6-digit confirmation code sent to ${email}.`)
await screen.findByText(
`Enter the 6-digit confirmation code sent to ${email}.`
)
const inputCode = screen.getByLabelText(/6-digit confirmation code/i)
fireEvent.change(inputCode, { target: { value: '123456' } })
const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
@@ -139,16 +139,18 @@ describe('user role and institution', function () {
departments: [fakeDepartment],
})
fireEvent.click(
screen.getByRole('button', { name: /add role and department/i })
)
const addRoleButton = await screen.findByRole('button', {
name: /add role and department/i,
})
fireEvent.click(addRoleButton)
await fetchMock.callHistory.flush(true)
fetchMock.removeRoutes().clearHistory()
fireEvent.click(screen.getByRole('combobox', { name: /department/i }))
screen.getByText(fakeDepartment)
await screen.findByText(fakeDepartment)
})
it('adds new role and department', async function () {
@@ -13,7 +13,7 @@ import ReconfirmationInfo from '../../../../../../frontend/js/features/settings/
import { ssoUserData } from '../../fixtures/test-user-email-data'
import { UserEmailData } from '../../../../../../types/user-email'
import { UserEmailsProvider } from '../../../../../../frontend/js/features/settings/context/user-email-context'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import getMeta from '@/utils/meta'
function renderReconfirmationInfo(data: UserEmailData) {
@@ -25,25 +25,18 @@ function renderReconfirmationInfo(data: UserEmailData) {
}
describe('<ReconfirmationInfo/>', function () {
let assignStub: sinon.SinonStub
beforeEach(function () {
Object.assign(getMeta('ol-ExposedSettings'), {
samlInitPath: '/saml',
})
fetchMock.get('/user/emails?ensureAffiliation=true', [])
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
describe('reconfirmed via SAML', function () {
@@ -115,9 +108,9 @@ describe('<ReconfirmationInfo/>', function () {
await waitFor(() => {
expect(confirmButton.disabled).to.be.true
})
sinon.assert.calledOnce(assignStub)
sinon.assert.calledOnce(this.locationWrapperStub.assign)
sinon.assert.calledWithMatch(
assignStub,
this.locationWrapperStub.assign,
'/saml/init?university_id=2&reconfirm=/user/settings'
)
})
@@ -4,7 +4,7 @@ import { fireEvent, screen, render, waitFor } from '@testing-library/react'
import fetchMock, { type FetchMock } from 'fetch-mock'
import LeaveModalForm from '../../../../../../frontend/js/features/settings/components/leave/modal-form'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import getMeta from '@/utils/meta'
describe('<LeaveModalForm />', function () {
@@ -51,25 +51,19 @@ describe('<LeaveModalForm />', function () {
let setInFlight: sinon.SinonStub
let setIsFormValid: sinon.SinonStub
let deleteMock: FetchMock
let assignStub: sinon.SinonStub
beforeEach(function () {
setInFlight = sinon.stub()
setIsFormValid = sinon.stub()
deleteMock = fetchMock.post('/user/delete', 200)
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: sinon.stub(),
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
it('with valid form', async function () {
@@ -86,6 +80,7 @@ describe('<LeaveModalForm />', function () {
sinon.assert.calledOnce(setInFlight)
sinon.assert.calledWithMatch(setInFlight, true)
expect(deleteMock.callHistory.called()).to.be.true
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
sinon.assert.calledTwice(setInFlight)
sinon.assert.calledWithMatch(setInFlight, false)
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { renderHook } from '@testing-library/react-hooks'
import { renderHook, waitFor } from '@testing-library/react'
import {
SSOProvider,
useSSOContext,
@@ -60,10 +60,11 @@ describe('SSOContext', function () {
})
it('should unlink an existing subscription', async function () {
const { result, waitForNextUpdate } = renderSSOContext()
const { result } = renderSSOContext()
result.current.unlink('google')
await waitForNextUpdate()
expect(result.current.subscriptions.google.linked).to.be.false
await waitFor(
() => expect(result.current.subscriptions.google.linked).to.be.false
)
})
it('when the provider is not linked, should do nothing', function () {
@@ -1,8 +1,6 @@
import { expect } from 'chai'
import { cloneDeep } from 'lodash'
import { renderHook } from '@testing-library/react-hooks'
import { waitFor } from '@testing-library/react'
import { renderHook, waitFor } from '@testing-library/react'
import {
EmailContextType,
UserEmailsProvider,
@@ -53,12 +51,14 @@ describe('UserEmailContext', function () {
const { result } = renderUserEmailsContext()
await fetchMock.callHistory.flush(true)
expect(fetchMock.callHistory.calls()).to.have.lengthOf(1)
expect(result.current.state.data.byId).to.deep.equal({
'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
'baz@overleaf.com': unconfirmedUserData,
'foo@overleaf.com': professionalUserData,
'qux@overleaf.com': unconfirmedCommonsUserData,
})
await waitFor(() =>
expect(result.current.state.data.byId).to.deep.equal({
'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
'baz@overleaf.com': unconfirmedUserData,
'foo@overleaf.com': professionalUserData,
'qux@overleaf.com': unconfirmedCommonsUserData,
})
)
expect(result.current.state.data.linkedInstitutionIds).to.have.lengthOf(0)
expect(result.current.isInitializing).to.equal(false)
@@ -70,10 +70,8 @@ describe('UserEmailContext', function () {
const { result } = renderUserEmailsContext()
await fetchMock.callHistory.flush()
await waitFor(() => {
expect(result.current.isInitializing).to.equal(false)
expect(result.current.isInitializingError).to.equal(true)
})
await waitFor(() => expect(result.current.isInitializing).to.equal(false))
expect(result.current.isInitializingError).to.equal(true)
})
describe('state.isLoading', function () {
@@ -82,12 +80,16 @@ describe('UserEmailContext', function () {
expect(result.current.state.isLoading).to.equal(true)
})
it('should be updated with `setLoading`', function () {
it('should be updated with `setLoading`', async function () {
const { result } = renderUserEmailsContext()
result.current.setLoading(true)
expect(result.current.state.isLoading).to.equal(true)
await waitFor(() =>
expect(result.current.state.isLoading).to.equal(true)
)
result.current.setLoading(false)
expect(result.current.state.isLoading).to.equal(false)
await waitFor(() =>
expect(result.current.state.isLoading).to.equal(false)
)
})
})
})
@@ -106,15 +108,19 @@ describe('UserEmailContext', function () {
fetchMock.removeRoutes().clearHistory()
})
it('should set `isLoading === true`', function () {
fetchMock.get(/\/user\/emails/, [
{
email: 'new@email.com',
default: true,
},
])
it('should set `isLoading === true`', async function () {
fetchMock.get(
/\/user\/emails/,
[
{
email: 'new@email.com',
default: true,
},
],
{ delay: 100 }
)
result.current.getEmails()
expect(result.current.state.isLoading).to.be.true
await waitFor(() => expect(result.current.state.isLoading).to.be.true)
})
it('requests a new set of emails', async function () {
@@ -125,9 +131,11 @@ describe('UserEmailContext', function () {
fetchMock.get(/\/user\/emails/, [emailData])
result.current.getEmails()
await fetchMock.callHistory.flush(true)
expect(result.current.state.data.byId).to.deep.equal({
'new@email.com': emailData,
})
await waitFor(() =>
expect(result.current.state.data.byId).to.deep.equal({
'new@email.com': emailData,
})
)
})
it('should populate `linkedInstitutionIds`', async function () {
@@ -138,42 +146,55 @@ describe('UserEmailContext', function () {
])
const { result } = renderUserEmailsContext()
await fetchMock.callHistory.flush(true)
expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
'saml_provider_1',
'saml_provider_2',
])
await waitFor(() =>
expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
'saml_provider_1',
'saml_provider_2',
])
)
})
})
describe('makePrimary()', function () {
it('sets an email as `default`', function () {
it('sets an email as `default`', async function () {
expect(result.current.state.data.byId['bar@overleaf.com'].default).to.be
.false
result.current.makePrimary('bar@overleaf.com')
expect(result.current.state.data.byId['bar@overleaf.com'].default).to.be
.true
await waitFor(
() =>
expect(result.current.state.data.byId['bar@overleaf.com'].default)
.to.be.true
)
})
it('sets `default=false` for the current primary email ', function () {
it('sets `default=false` for the current primary email ', async function () {
expect(result.current.state.data.byId['foo@overleaf.com'].default).to.be
.true
result.current.makePrimary('bar@overleaf.com')
expect(result.current.state.data.byId['foo@overleaf.com'].default).to.be
.false
await waitFor(
() =>
expect(result.current.state.data.byId['foo@overleaf.com'].default)
.to.be.false
)
})
it('produces no effect when passing a non-existing email', function () {
it('produces no effect when passing a non-existing email', async function () {
const emails = cloneDeep(result.current.state.data.byId)
result.current.makePrimary('non-existing@email.com')
expect(result.current.state.data.byId).to.deep.equal(emails)
await waitFor(() =>
expect(result.current.state.data.byId).to.deep.equal(emails)
)
})
})
describe('deleteEmail()', function () {
it('removes data from the deleted email', function () {
it('removes data from the deleted email', async function () {
result.current.deleteEmail('bar@overleaf.com')
expect(result.current.state.data.byId['bar@overleaf.com']).to.be
.undefined
await waitFor(
() =>
expect(result.current.state.data.byId['bar@overleaf.com']).to.be
.undefined
)
})
it('produces no effect when passing a non-existing email', function () {
@@ -184,14 +205,20 @@ describe('UserEmailContext', function () {
})
describe('setEmailAffiliationBeingEdited()', function () {
it('sets an email as currently being edited', function () {
it('sets an email as currently being edited', async function () {
result.current.setEmailAffiliationBeingEdited('bar@overleaf.com')
expect(result.current.state.data.emailAffiliationBeingEdited).to.equal(
'bar@overleaf.com'
await waitFor(() =>
expect(
result.current.state.data.emailAffiliationBeingEdited
).to.equal('bar@overleaf.com')
)
result.current.setEmailAffiliationBeingEdited(null)
expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
await waitFor(
() =>
expect(result.current.state.data.emailAffiliationBeingEdited).to.be
.null
)
})
it('produces no effect when passing a non-existing email', function () {
@@ -202,15 +229,17 @@ describe('UserEmailContext', function () {
})
describe('updateAffiliation()', function () {
it('updates affiliation data for an email', function () {
it('updates affiliation data for an email', async function () {
result.current.updateAffiliation(
'foo@overleaf.com',
'new role',
'new department'
)
expect(
result.current.state.data.byId['foo@overleaf.com'].affiliation!.role
).to.equal('new role')
await waitFor(() =>
expect(
result.current.state.data.byId['foo@overleaf.com'].affiliation!.role
).to.equal('new role')
)
expect(
result.current.state.data.byId['foo@overleaf.com'].affiliation!
.department
@@ -317,9 +346,11 @@ describe('UserEmailContext', function () {
result.current.deleteEmail(affiliatedEmail1.email)
result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
expect(
localStorage.getItem('showInstitutionalLeaversSurveyUntil')
).to.be.greaterThan(Date.now())
await waitFor(() =>
expect(
localStorage.getItem('showInstitutionalLeaversSurveyUntil')
).to.be.greaterThan(Date.now())
)
})
it("when the leaver has no institution license, it shouldn't reset the survey expiration date", async function () {
@@ -14,15 +14,15 @@ import {
USER_EMAIL,
USER_ID,
} from '../../../helpers/editor-providers'
import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
async function changePrivilegeLevel(screen, { current, next }) {
const select = screen.getByDisplayValue(current)
await fireEvent.click(select)
fireEvent.click(select)
const option = screen.getByRole('option', {
name: next,
})
await fireEvent.click(option)
fireEvent.click(option)
}
describe('<ShareProjectModal/>', function () {
@@ -89,19 +89,17 @@ describe('<ShareProjectModal/>', function () {
}
beforeEach(function () {
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
fetchMock.get('/user/contacts', { contacts })
window.metaAttributesCache.set('ol-user', { allowedFreeTrial: true })
window.metaAttributesCache.set('ol-showUpgradePrompt', true)
window.metaAttributesCache.set('ol-isReviewerRoleEnabled', true)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
cleanUpContext()
})
@@ -199,6 +197,8 @@ describe('<ShareProjectModal/>', function () {
})
it('displays actions for project-owners', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
const invites = [
{
_id: 'invited-author',
@@ -304,6 +304,7 @@ describe('<ShareProjectModal/>', function () {
it('only shows read-only token link to restricted token members', async function () {
window.metaAttributesCache.set('ol-isRestrictedTokenMember', true)
fetchMock.get(`/project/${project._id}/tokens`, {})
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
isRestrictedTokenMember: true,
@@ -349,6 +350,8 @@ describe('<ShareProjectModal/>', function () {
},
]
fetchMock.get(`/project/${project._id}/tokens`, {})
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
@@ -382,6 +385,7 @@ describe('<ShareProjectModal/>', function () {
})
it('resends an invite', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.postOnce(
'express:/project/:projectId/invite/:inviteId/resend',
204
@@ -415,10 +419,11 @@ describe('<ShareProjectModal/>', function () {
await waitFor(() => expect(closeButton.disabled).to.be.true)
expect(fetchMock.callHistory.done()).to.be.true
expect(closeButton.disabled).to.be.false
await waitFor(() => expect(closeButton.disabled).to.be.false)
})
it('revokes an invite', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.deleteOnce('express:/project/:projectId/invite/:inviteId', 204)
const invites = [
@@ -448,10 +453,11 @@ describe('<ShareProjectModal/>', function () {
await waitFor(() => expect(closeButton.disabled).to.be.true)
expect(fetchMock.callHistory.done()).to.be.true
expect(closeButton.disabled).to.be.false
await waitFor(() => expect(closeButton.disabled).to.be.false)
})
it('changes member privileges to read + write', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.putOnce('express:/project/:projectId/users/:userId', 204)
const members = [
@@ -488,10 +494,11 @@ describe('<ShareProjectModal/>', function () {
expect(JSON.parse(body)).to.deep.equal({ privilegeLevel: 'readAndWrite' })
expect(fetchMock.callHistory.done()).to.be.true
expect(closeButton.disabled).to.be.false
await waitFor(() => expect(closeButton.disabled).to.be.false)
})
it('removes a member from the project', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.deleteOnce('express:/project/:projectId/users/:userId', 204)
const members = [
@@ -534,6 +541,7 @@ describe('<ShareProjectModal/>', function () {
})
it('changes member privileges to owner with confirmation', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.postOnce('express:/project/:projectId/transfer-ownership', 204)
const members = [
@@ -582,6 +590,8 @@ describe('<ShareProjectModal/>', function () {
})
it('sends invites to input email addresses', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
@@ -674,6 +684,7 @@ describe('<ShareProjectModal/>', function () {
})
it('displays a message when the collaborator limit is reached', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.post(
'/event/paywall-prompt',
{},
@@ -748,6 +759,8 @@ describe('<ShareProjectModal/>', function () {
})
it('handles server error responses', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
@@ -807,6 +820,7 @@ describe('<ShareProjectModal/>', function () {
})
it('handles switching between access levels', async function () {
fetchMock.get(`/project/${project._id}/tokens`, {})
fetchMock.post('express:/project/:projectId/settings/admin', 204)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
@@ -307,7 +307,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
packageNames: new Set(['foo']),
}
const MetadataProvider: FC = ({ children }) => {
const MetadataProvider: FC<React.PropsWithChildren> = ({ children }) => {
return (
<MetadataContext.Provider value={metadata}>
{children}
@@ -376,7 +376,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
const scope = mockScope()
const ReferencesProvider: FC = ({ children }) => {
const ReferencesProvider: FC<React.PropsWithChildren> = ({ children }) => {
return (
<ReferencesContext.Provider
value={{
@@ -728,7 +728,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
packageNames: new Set<string>('amsmath'),
}
const MetadataProvider: FC = ({ children }) => {
const MetadataProvider: FC<React.PropsWithChildren> = ({ children }) => {
return (
<MetadataContext.Provider value={metadata}>
{children}
@@ -43,7 +43,9 @@ describe('<FigureModal />', function () {
const scope = mockScope(content)
scope.editor.showVisual = true
const FileTreePathProvider: FC = ({ children }) => (
const FileTreePathProvider: FC<React.PropsWithChildren> = ({
children,
}) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
@@ -545,7 +547,7 @@ describe('<FigureModal />', function () {
cy.findByText('Done').click()
cy.get('.cm-content').should(
'have.text',
'\\begin{figure}\\centering\\end{figure}'
'\\begin{figure}\\centering\\includegraphics[width=0.75\\linewidth]{frog.jpg}\\end{figure}'
)
})
@@ -567,7 +569,7 @@ describe('<FigureModal />', function () {
cy.findByText('Done').click()
cy.get('.cm-content').should(
'have.text',
'\\begin{figure}\\centering🏷fig:my-label\\end{figure}'
'\\begin{figure}\\centering\\includegraphics[width=0.75\\linewidth]{frog.jpg}🏷fig:my-label\\end{figure}'
)
})
@@ -178,7 +178,9 @@ cell 3 & cell 4 \\\\
})
it('Adds and removes borders when theme is changed', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{c|c}
cell 1 & cell 2 \\\\
cell 3 & cell 4 \\\\
@@ -216,7 +218,9 @@ cell 3 & cell 4 \\\\
})
it('Changes the column alignment with dropdown buttons', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{cc}
cell 1 & cell 2 \\\\
cell 3 & cell 4 \\\\
@@ -269,7 +273,9 @@ cell 3 & cell 4 \\\\
})
it('Removes rows and columns', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{|c|c|c|}
\\hline
cell 1 & cell 2 & cell 3 \\\\ \\hline
@@ -304,7 +310,9 @@ cell 3 & cell 4 \\\\
})
it('Removes rows correctly when removing from the left', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{|c|c|c|}\\hline
cell 1&cell 2&cell 3 \\\\\\hline
\\end{tabular}
@@ -324,7 +332,9 @@ cell 3 & cell 4 \\\\
})
it('Merges and unmerged cells', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{ccc}
cell 1 & cell 2 & cell 3 \\\\
cell 4 & cell 5 & cell 6 \\\\
@@ -333,6 +343,7 @@ cell 3 & cell 4 \\\\
cy.get('.table-generator-cell').first().click()
cy.get('.table-generator-cell').first().type('{shift}{rightarrow}')
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
cy.get('@toolbar').scrollIntoView()
cy.get('@toolbar').findByLabelText('Merge cells').click()
checkTable([
[{ text: 'cell 1 cell 2', colspan: 2 }, 'cell 3'],
@@ -346,7 +357,9 @@ cell 3 & cell 4 \\\\
})
it('Adds rows and columns', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{c}
cell 1
\\end{tabular}
@@ -398,7 +411,9 @@ cell 3 & cell 4 \\\\
})
it('Removes the table on toolbar button click', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{c}
cell 1
\\end{tabular}`)
@@ -409,7 +424,9 @@ cell 3 & cell 4 \\\\
})
it('Moves the caption when using dropdown', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{table}
\\caption{Table caption}
\\label{tab:table}
@@ -456,7 +473,9 @@ cell 3 & cell 4 \\\\
})
it('Renders a table with custom column spacing', function () {
// Add a blank line above the table to allow room for the table toolbar
mountEditor(`
\\begin{tabular}{@{}c@{}l!{}}
cell 1 & cell 2 \\\\
cell 3 & cell 4 \\\\
@@ -498,8 +517,10 @@ cell 3 & cell 4 \\\\
describe('Fixed width columns', function () {
it('Can add fixed width columns', function () {
// Add a blank line above the table to allow room for the table toolbar
// Check that no column indicators exist
mountEditor(`
\\begin{tabular}{cc}
cell 1 & cell 2\\\\
cell 3 & cell 4 \\\\
@@ -574,9 +595,11 @@ cell 3 & cell 4 \\\\
}
)
it(`It can justify fixed width cells`, function () {
it(`Can justify fixed width cells`, function () {
// Add a blank line above the table to allow room for the table toolbar
// Check that no column indicators exist
mountEditor(`
\\begin{tabular}{>{\\raggedright\\arraybackslash}p{2cm}c}
cell 1 & cell 2\\\\
cell 3 & cell 4 \\\\
@@ -6,7 +6,7 @@ import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-pat
import { TestContainer } from '../helpers/test-container'
import { PermissionsContext } from '@/features/ide-react/context/permissions-context'
const FileTreePathProvider: FC = ({ children }) => (
const FileTreePathProvider: FC<React.PropsWithChildren> = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
@@ -22,7 +22,7 @@ const FileTreePathProvider: FC = ({ children }) => (
</FileTreePathContext.Provider>
)
const PermissionsProvider: FC = ({ children }) => (
const PermissionsProvider: FC<React.PropsWithChildren> = ({ children }) => (
<PermissionsContext.Provider
value={{
read: true,
@@ -21,7 +21,9 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
const scope = mockScope(content)
scope.editor.showVisual = true
const FileTreePathProvider: FC = ({ children }) => (
const FileTreePathProvider: FC<React.PropsWithChildren> = ({
children,
}) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
@@ -216,7 +216,7 @@ describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
onlineUsersCount: 3,
}
const OnlineUsersProvider: FC = ({ children }) => {
const OnlineUsersProvider: FC<React.PropsWithChildren> = ({ children }) => {
return (
<OnlineUsersContext.Provider value={value}>
{children}
@@ -15,7 +15,7 @@ import {
groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange,
} from '../../fixtures/subscriptions'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import { UserId } from '../../../../../../types/user'
import { SplitTestProvider } from '@/shared/context/split-test-context'
@@ -73,16 +73,9 @@ describe('<GroupSubscriptionMemberships />', function () {
})
describe('opens leave group modal when button is clicked', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
render(
<SplitTestProvider>
@@ -111,7 +104,7 @@ describe('<GroupSubscriptionMemberships />', function () {
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
it('close the modal', function () {
@@ -130,6 +123,7 @@ describe('<GroupSubscriptionMemberships />', function () {
fireEvent.click(this.leaveNowButton)
expect(leaveGroupApiMock.callHistory.called()).to.be.true
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -97,7 +97,7 @@ describe('<ManagedInstitutions />', function () {
)
const unsubscribeLink = screen.getByText('Unsubscribe')
await fireEvent.click(unsubscribeLink)
fireEvent.click(unsubscribeLink)
await waitFor(
() => expect(fetchMock.callHistory.called(unsubscribeUrl)).to.be.true
)
@@ -126,7 +126,7 @@ describe('<ManagedInstitutions />', function () {
const subscribeLink = await screen.findByText('Subscribe')
await fireEvent.click(subscribeLink)
fireEvent.click(subscribeLink)
await waitFor(
() => expect(fetchMock.callHistory.called(subscribeUrl)).to.be.true
)
@@ -9,7 +9,7 @@ import {
trialSubscription,
} from '../../fixtures/subscriptions'
import { renderActiveSubscription } from '../../helpers/render-active-subscription'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import { MetaTag } from '@/utils/meta'
const pauseSubscriptionSplitTestMeta: MetaTag[] = [
@@ -45,22 +45,17 @@ function clickSubmitButton() {
describe('<PauseSubscriptionModal />', function () {
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon
.stub()
.returns('https://www.dev-overleaf.com/user/subscription'),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
this.locationWrapperStub.toString.returns(
'https://www.dev-overleaf.com/user/subscription'
)
this.replaceStateStub = sinon.stub(window.history, 'replaceState')
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.locationWrapperSandbox.restore()
this.replaceStateStub.restore()
})
@@ -89,7 +84,6 @@ describe('<PauseSubscriptionModal />', function () {
clickCancelButton()
await screen.findByText('Pause instead, to pick up where you left off')
})
let reloadStub: sinon.SinonStub
it('renders options for pause duration', async function () {
renderSubscriptionWithPauseSupport()
@@ -135,6 +129,7 @@ describe('<PauseSubscriptionModal />', function () {
renderSubscriptionWithPauseSupport()
clickCancelButton()
clickSubmitButton()
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -4,6 +4,7 @@ import {
fireEvent,
waitForElementToBeRemoved,
within,
waitFor,
} from '@testing-library/react'
import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
import {
@@ -19,7 +20,7 @@ import {
import { reactivateSubscriptionUrl } from '../../../../../../frontend/js/features/subscription/data/subscription-url'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
describe('<PersonalSubscription />', function () {
afterEach(function () {
@@ -48,21 +49,13 @@ describe('<PersonalSubscription />', function () {
})
describe('subscription states ', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
it('renders the active dash', function () {
@@ -107,15 +100,17 @@ describe('<PersonalSubscription />', function () {
fireEvent.click(reactivateBtn)
expect(reactivateBtn.disabled).to.be.true
await fetchMock.callHistory.flush(true)
expect(reactivateBtn.disabled).to.be.false
expect(reloadStub).not.to.have.been.called
await waitFor(() => expect(reactivateBtn.disabled).to.be.false)
expect(this.locationWrapperStub.reload).not.to.have.been.called
fetchMock.removeRoutes().clearHistory()
// 2nd click - success
fetchMock.postOnce(reactivateSubscriptionUrl, 200)
fireEvent.click(reactivateBtn)
await fetchMock.callHistory.flush(true)
expect(reloadStub).to.have.been.calledOnce
await waitFor(() => {
expect(this.locationWrapperStub.reload).to.have.been.calledOnce
})
expect(reactivateBtn.disabled).to.be.true
fetchMock.removeRoutes().clearHistory()
})
@@ -21,7 +21,7 @@ import {
extendTrialUrl,
subscriptionUpdateUrl,
} from '@/features/subscription/data/subscription-url'
import * as useLocationModule from '../../../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
import { MetaTag } from '@/utils/meta'
describe('<ActiveSubscription />', function () {
@@ -77,9 +77,13 @@ describe('<ActiveSubscription />', function () {
screen.getByText('You are currently subscribed to the', { exact: false })
await screen.findByRole('heading', { name: 'Change plan' })
expect(
screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0
).to.be.true
await waitFor(
() =>
expect(
screen.getAllByRole('button', { name: 'Change to this plan' })
.length > 0
).to.be.true
)
})
it('notes when user is changing plan at end of current plan term', function () {
@@ -193,21 +197,13 @@ describe('<ActiveSubscription />', function () {
})
describe('cancel plan', function () {
const assignStub = sinon.stub()
const reloadStub = sinon.stub()
beforeEach(function () {
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
this.locationStub.restore()
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
})
@@ -277,6 +273,7 @@ describe('<ActiveSubscription />', function () {
name: 'Cancel my subscription',
})
fireEvent.click(button)
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.called
})
@@ -406,6 +403,7 @@ describe('<ActiveSubscription />', function () {
})
fireEvent.click(extendTrialButton)
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -499,7 +497,10 @@ describe('<ActiveSubscription />', function () {
const endPointResponse = {
status: 200,
}
fetchMock.post(subscriptionUpdateUrl, endPointResponse)
fetchMock.post(
`${subscriptionUpdateUrl}?downgradeToPaidPersonal`,
endPointResponse
)
renderActiveSubscription(monthlyActiveCollaborator)
showConfirmCancelUI()
const downgradeButton = await screen.findByRole('button', {
@@ -507,6 +508,7 @@ describe('<ActiveSubscription />', function () {
})
fireEvent.click(downgradeButton)
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -20,26 +20,18 @@ import {
subscriptionUpdateUrl,
} from '../../../../../../../../../frontend/js/features/subscription/data/subscription-url'
import { renderActiveSubscription } from '../../../../../helpers/render-active-subscription'
import * as useLocationModule from '../../../../../../../../../frontend/js/shared/hooks/use-location'
import { location } from '@/shared/components/location'
describe('<ChangePlanModal />', function () {
let reloadStub: sinon.SinonStub
beforeEach(function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
replace: sinon.stub(),
reload: reloadStub,
setHash: sinon.stub(),
toString: sinon.stub(),
})
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
})
afterEach(function () {
cleanUpContext()
fetchMock.removeRoutes().clearHistory()
this.locationStub.restore()
this.locationWrapperSandbox.restore()
})
it('renders the individual plans table and group plans UI', async function () {
@@ -210,6 +202,7 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -299,6 +292,7 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -530,7 +524,8 @@ describe('<ChangePlanModal />', function () {
screen.getByRole('button', { name: 'Processing…' })
// // page is reloaded on success
// page is reloaded on success
const reloadStub = this.locationWrapperStub.reload
await waitFor(() => {
expect(reloadStub).to.have.been.called
})
@@ -1,4 +1,4 @@
import { renderHook, act } from '@testing-library/react-hooks'
import { renderHook, act } from '@testing-library/react'
import { expect } from 'chai'
import sinon from 'sinon'
import useAsync from '../../../../frontend/js/shared/hooks/use-async'
@@ -1,5 +1,5 @@
import sinon from 'sinon'
import { renderHook } from '@testing-library/react-hooks'
import { renderHook } from '@testing-library/react'
import useCallbackHandlers from '../../../../frontend/js/shared/hooks/use-callback-handlers'
describe('useCallbackHandlers', function () {