From 241709c710865ccfb57ae3fdf7b2ecd45f89ee11 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:21:06 +0100 Subject: [PATCH] Merge pull request #27828 from overleaf/dp-modal-test-typescript Convert clone project, share project and dictionary modal tests to typescript GitOrigin-RevId: 058eecef6054a40dff7c3697fcd908bb60e39b6b --- ....test.jsx => clone-project-modal.test.tsx} | 35 +++- ....jsx => dictionary-modal-content.spec.tsx} | 2 +- .../review-panel/review-panel.spec.tsx | 3 +- ....test.jsx => share-project-modal.test.tsx} | 193 ++++++++++++------ .../frontend/helpers/editor-providers.tsx | 6 +- 5 files changed, 163 insertions(+), 76 deletions(-) rename services/web/test/frontend/features/clone-project-modal/components/{clone-project-modal.test.jsx => clone-project-modal.test.tsx} (84%) rename services/web/test/frontend/features/dictionary/components/{dictionary-modal-content.spec.jsx => dictionary-modal-content.spec.tsx} (98%) rename services/web/test/frontend/features/share-project-modal/components/{share-project-modal.test.jsx => share-project-modal.test.tsx} (83%) diff --git a/services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.jsx b/services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.tsx similarity index 84% rename from services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.jsx rename to services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.tsx index 0f523349e6..faabe254ea 100644 --- a/services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.jsx +++ b/services/web/test/frontend/features/clone-project-modal/components/clone-project-modal.test.tsx @@ -58,10 +58,14 @@ describe('', function () { contextProps ) - const cancelButton = await screen.findByRole('button', { name: 'Cancel' }) + const cancelButton: HTMLButtonElement = await screen.findByRole('button', { + name: 'Cancel', + }) expect(cancelButton.disabled).to.be.false - const submitButton = await screen.findByRole('button', { name: 'Copy' }) + const submitButton: HTMLButtonElement = await screen.findByRole('button', { + name: 'Copy', + }) expect(submitButton.disabled).to.be.false const input = await screen.getByLabelText('New Name') @@ -81,14 +85,21 @@ describe('', function () { await fetchMock.callHistory.flush(true) expect(fetchMock.callHistory.done()).to.be.true - const { url, options } = fetchMock.callHistory + + const callLog = fetchMock.callHistory .calls('express:/project/:projectId/clone') .at(-1) + + expect(callLog).to.exist + + const { url, options } = callLog! expect(url).to.equal( 'https://www.test-overleaf.com/project/project-1/clone' ) - expect(JSON.parse(options.body)).to.deep.equal({ + expect(options.body).to.exist + + expect(JSON.parse(options.body as string)).to.deep.equal({ projectName: 'A Cloned Project', tags: [], }) @@ -126,10 +137,14 @@ describe('', function () { contextProps ) - const button = await screen.findByRole('button', { name: 'Copy' }) + const button: HTMLButtonElement = await screen.findByRole('button', { + name: 'Copy', + }) expect(button.disabled).to.be.false - const cancelButton = await screen.findByRole('button', { name: 'Cancel' }) + const cancelButton: HTMLButtonElement = await screen.findByRole('button', { + name: 'Cancel', + }) expect(cancelButton.disabled).to.be.false fireEvent.click(button) @@ -163,10 +178,14 @@ describe('', function () { contextProps ) - const button = await screen.findByRole('button', { name: 'Copy' }) + const button: HTMLButtonElement = await screen.findByRole('button', { + name: 'Copy', + }) expect(button.disabled).to.be.false - const cancelButton = await screen.findByRole('button', { name: 'Cancel' }) + const cancelButton: HTMLButtonElement = await screen.findByRole('button', { + name: 'Cancel', + }) expect(cancelButton.disabled).to.be.false fireEvent.click(button) diff --git a/services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.jsx b/services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.tsx similarity index 98% rename from services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.jsx rename to services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.tsx index c8cdd931b3..82aa383bf5 100644 --- a/services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.jsx +++ b/services/web/test/frontend/features/dictionary/components/dictionary-modal-content.spec.tsx @@ -3,7 +3,7 @@ import { EditorProviders } from '../../../helpers/editor-providers' import { learnedWords } from '@/features/source-editor/extensions/spelling/learned-words' describe('', function () { - let originalLearnedWords + let originalLearnedWords: Set beforeEach(function () { cy.then(() => { diff --git a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx index fcad15969b..aadefbe8b3 100644 --- a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx +++ b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx @@ -9,6 +9,7 @@ import { mockScope } from '../source-editor/helpers/mock-scope' import { TestContainer } from '../source-editor/helpers/test-container' import { docId } from '../source-editor/helpers/mock-doc' import { mockProject } from '../source-editor/helpers/mock-project' +import { UserId } from '@ol-types/user' const userData = { avatar_text: 'User', @@ -874,7 +875,7 @@ describe(' for free users', function () { it.skip('opens subscription page after clicking on `try it for free`', function () {}) it('shows `ask project owner to upgrade` message', function () { - mountEditor('other-user-id') + mountEditor('other-user-id' as UserId) cy.findByRole('dialog').within(() => { cy.findByText( 'Please ask the project owner to upgrade to use track changes' diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx similarity index 83% rename from services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx rename to services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx index 548423e90f..a1f6d829c1 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.tsx @@ -1,7 +1,7 @@ import { expect } from 'chai' import sinon from 'sinon' -import { screen, fireEvent, waitFor } from '@testing-library/react' -import fetchMock from 'fetch-mock' +import { screen, Screen, fireEvent, waitFor } from '@testing-library/react' +import fetchMock, { CallLog } from 'fetch-mock' import userEvent from '@testing-library/user-event' import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal' @@ -14,8 +14,17 @@ import { } from '../../../helpers/editor-providers' import { location } from '@/shared/components/location' import { useProjectContext } from '@/shared/context/project-context' +import { + ProjectMember, + ProjectMetadata, +} from '@/shared/context/types/project-metadata' +import { UserId } from '@ol-types/user' +import { PublicAccessLevel } from '@ol-types/public-access-level' -async function changePrivilegeLevel(screen, { current, next }) { +async function changePrivilegeLevel( + screen: Screen, + { current, next }: { current: string; next: string } +) { const select = screen.getByDisplayValue(current) fireEvent.click(select) const option = screen.getByRole('option', { @@ -24,7 +33,7 @@ async function changePrivilegeLevel(screen, { current, next }) { fireEvent.click(option) } -const shareModalProjectDefaults = Object.assign({}, projectDefaults, { +const testProjectOverrides: Partial = { _id: 'test-project', name: 'Test Project', features: { @@ -34,10 +43,20 @@ const shareModalProjectDefaults = Object.assign({}, projectDefaults, { owner: { _id: USER_ID, email: USER_EMAIL, + first_name: 'Test', + last_name: 'Owner', + privileges: 'owner', + signUpDate: new Date('2025-07-07').toISOString(), }, -}) +} -function createContextProps(projectOverrides) { +const shareModalProjectDefaults: ProjectMetadata = Object.assign( + {}, + projectDefaults, + testProjectOverrides +) + +function createContextProps(projectOverrides?: Partial) { const project = Object.assign({}, shareModalProjectDefaults, projectOverrides) return { providers: { ProjectProvider: makeProjectProvider(project) } } } @@ -88,6 +107,7 @@ describe('', function () { const modalProps = { show: true, handleHide: sinon.stub(), + handleOpen: sinon.stub(), } beforeEach(function () { @@ -204,11 +224,13 @@ describe('', function () { it('displays actions for project-owners', async function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, ] @@ -223,11 +245,13 @@ describe('', function () { }) it('hides actions from non-project-owners when link sharing on', async function () { - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, ] @@ -251,11 +275,13 @@ describe('', function () { }) it('hides actions from non-project-owners when link sharing off', async function () { - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, ] @@ -300,29 +326,37 @@ describe('', function () { }) it('displays project members and invites', async function () { - const members = [ + const members: ProjectMember[] = [ { - _id: 'member-author', + _id: 'member-author' as UserId, email: 'member-author@example.com', privileges: 'readAndWrite', + first_name: 'Member', + last_name: 'Author', }, { - _id: 'member-viewer', + _id: 'member-viewer' as UserId, email: 'member-viewer@example.com', privileges: 'readOnly', + first_name: 'Member', + last_name: 'Viewer', }, ] - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, { - _id: 'invited-viewer', + _id: 'invited-viewer' as UserId, email: 'invited-viewer@example.com', privileges: 'readOnly', + first_name: 'Invited', + last_name: 'Viewer', }, ] @@ -361,11 +395,13 @@ describe('', function () { 204 ) - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, ] @@ -381,21 +417,27 @@ describe('', function () { const resendButton = screen.getByRole('button', { name: 'Resend' }) fireEvent.click(resendButton) - await waitFor(() => expect(closeButton.disabled).to.be.true) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.true + ) expect(fetchMock.callHistory.done()).to.be.true - await waitFor(() => expect(closeButton.disabled).to.be.false) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.false + ) }) it('revokes an invite', async function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) fetchMock.deleteOnce('express:/project/:projectId/invite/:inviteId', 204) - const invites = [ + const invites: ProjectMember[] = [ { - _id: 'invited-author', + _id: 'invited-author' as UserId, email: 'invited-author@example.com', privileges: 'readAndWrite', + first_name: 'Invited', + last_name: 'Author', }, ] @@ -410,21 +452,27 @@ describe('', function () { const revokeButton = screen.getByRole('button', { name: 'Revoke' }) fireEvent.click(revokeButton) - await waitFor(() => expect(closeButton.disabled).to.be.true) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.true + ) expect(fetchMock.callHistory.done()).to.be.true - await waitFor(() => expect(closeButton.disabled).to.be.false) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.false + ) }) it('changes member privileges to read + write', async function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) fetchMock.putOnce('express:/project/:projectId/users/:userId', 204) - const members = [ + const members: ProjectMember[] = [ { - _id: 'member-viewer', + _id: 'member-viewer' as UserId, email: 'member-viewer@example.com', privileges: 'readOnly', + first_name: 'Member', + last_name: 'Viewer', }, ] @@ -443,24 +491,32 @@ describe('', function () { await changePrivilegeLevel(screen, { current: 'Viewer', next: 'Editor' }) - await waitFor(() => expect(closeButton.disabled).to.be.true) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.true + ) - const { body } = fetchMock.callHistory.calls().at(-1).options - expect(JSON.parse(body)).to.deep.equal({ privilegeLevel: 'readAndWrite' }) + const body = fetchMock.callHistory.calls().at(-1)?.options?.body + expect(JSON.parse(body as string)).to.deep.equal({ + privilegeLevel: 'readAndWrite', + }) expect(fetchMock.callHistory.done()).to.be.true - await waitFor(() => expect(closeButton.disabled).to.be.false) + await waitFor( + () => expect((closeButton as HTMLButtonElement).disabled).to.be.false + ) }) it('removes a member from the project', async function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) fetchMock.deleteOnce('express:/project/:projectId/users/:userId', 204) - const members = [ + const members: ProjectMember[] = [ { - _id: 'member-viewer', + _id: 'member-viewer' as UserId, email: 'member-viewer@example.com', privileges: 'readOnly', + first_name: 'Member', + last_name: 'Viewer', }, ] @@ -482,7 +538,7 @@ describe('', function () { }) fireEvent.click(removeButton) - const url = fetchMock.callHistory.calls().at(-1).url + const url = fetchMock.callHistory.calls().at(-1)?.url expect(url).to.equal( 'https://www.test-overleaf.com/project/test-project/users/member-viewer' ) @@ -494,11 +550,13 @@ describe('', function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) fetchMock.postOnce('express:/project/:projectId/transfer-ownership', 204) - const members = [ + const members: ProjectMember[] = [ { - _id: 'member-viewer', + _id: 'member-viewer' as UserId, email: 'member-viewer@example.com', privileges: 'readOnly', + first_name: 'Member', + last_name: 'Viewer', }, ] @@ -517,19 +575,22 @@ describe('', function () { screen.getByText((_, node) => { return ( + node !== null && node.textContent === - 'Are you sure you want to make member-viewer@example.com the owner of Test Project?' + 'Are you sure you want to make member-viewer@example.com the owner of Test Project?' ) }) - const confirmButton = screen.getByRole('button', { + const confirmButton: HTMLButtonElement = screen.getByRole('button', { name: 'Change owner', }) fireEvent.click(confirmButton) await waitFor(() => expect(confirmButton.disabled).to.be.true) - const { body } = fetchMock.callHistory.calls().at(-1).options - expect(JSON.parse(body)).to.deep.equal({ user_id: 'member-viewer' }) + const body = fetchMock.callHistory.calls().at(-1)?.options?.body + expect(JSON.parse(body as string)).to.deep.equal({ + user_id: 'member-viewer', + }) expect(fetchMock.callHistory.done()).to.be.true }) @@ -560,7 +621,7 @@ describe('', function () { fetchMock.post( 'express:/project/:projectId/invite', ({ args: [, req] }) => { - const data = JSON.parse(req.body) + const data = JSON.parse((req as { body: string }).body) if (data.email === 'a@b.c') { return { @@ -596,7 +657,7 @@ describe('', function () { const submitButton = screen.getByRole('button', { name: 'Invite' }) await userEvent.click(submitButton) - let calls + let calls: CallLog[] = [] await waitFor( () => { calls = fetchMock.callHistory.calls( @@ -607,16 +668,16 @@ describe('', function () { { timeout: 5000 } // allow time for delay between each request ) - expect(calls[0].args[1].body).to.equal( + expect((calls[0].args[1] as { body: string }).body).to.equal( JSON.stringify({ email: 'test@example.com', privileges: 'readOnly' }) ) - expect(calls[1].args[1].body).to.equal( + expect((calls[1].args[1] as { body: string }).body).to.equal( JSON.stringify({ email: 'foo@example.com', privileges: 'readOnly' }) ) - expect(calls[2].args[1].body).to.equal( + expect((calls[2].args[1] as { body: string }).body).to.equal( JSON.stringify({ email: 'bar@example.com', privileges: 'readOnly' }) ) - expect(calls[3].args[1].body).to.equal( + expect((calls[3].args[1] as { body: string }).body).to.equal( JSON.stringify({ email: 'a@b.c', privileges: 'readOnly' }) ) @@ -652,9 +713,9 @@ describe('', function () { const reviewerOption = screen.getByText('Reviewer').closest('button') const viewerOption = screen.getByText('Viewer').closest('button') - expect(editorOption.classList.contains('disabled')).to.be.true - expect(reviewerOption.classList.contains('disabled')).to.be.true - expect(viewerOption.classList.contains('disabled')).to.be.false + expect(editorOption?.classList.contains('disabled')).to.be.true + expect(reviewerOption?.classList.contains('disabled')).to.be.true + expect(viewerOption?.classList.contains('disabled')).to.be.false screen.getByText( /Upgrade to add more collaborators and access collaboration features like track changes and full project history/ @@ -671,9 +732,11 @@ describe('', function () { }, members: [ { - _id: 'reviewer-id', + _id: 'reviewer-id' as UserId, email: 'reviewer@example.com', privileges: 'review', + first_name: 'Reviewer', + last_name: 'LastName', }, ], }) @@ -688,9 +751,9 @@ describe('', function () { const reviewerOption = screen.getByText('Reviewer').closest('button') const viewerOption = screen.getByText('Viewer').closest('button') - expect(editorOption.classList.contains('disabled')).to.be.true - expect(reviewerOption.classList.contains('disabled')).to.be.true - expect(viewerOption.classList.contains('disabled')).to.be.false + expect(editorOption?.classList.contains('disabled')).to.be.true + expect(reviewerOption?.classList.contains('disabled')).to.be.true + expect(viewerOption?.classList.contains('disabled')).to.be.false screen.getByText( /Upgrade to add more collaborators and access collaboration features like track changes and full project history/ @@ -714,9 +777,11 @@ describe('', function () { const [inputElement] = await screen.findAllByLabelText('Add people') - const submitButton = screen.getByRole('button', { name: 'Invite' }) + const submitButton: HTMLButtonElement = screen.getByRole('button', { + name: 'Invite', + }) - const respondWithError = async function (errorReason) { + const respondWithError = async function (errorReason: string) { fireEvent.focus(inputElement) fireEvent.change(inputElement, { target: { value: 'invited-author-1@example.com' }, @@ -760,11 +825,11 @@ describe('', function () { fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {}) fetchMock.post('express:/project/:projectId/settings/admin', 204) - let setPublicAccessLevel = function () {} + let setPublicAccessLevel = function (_: PublicAccessLevel) {} function WrappedModal() { const { updateProject } = useProjectContext() - setPublicAccessLevel = publicAccessLevel => { + setPublicAccessLevel = (publicAccessLevel: PublicAccessLevel) => { updateProject({ publicAccessLevel }) } return @@ -779,14 +844,14 @@ describe('', function () { await screen.findByText('Link sharing is off') - const enableButton = await screen.findByRole('button', { + const enableButton: HTMLButtonElement = await screen.findByRole('button', { name: 'Turn on link sharing', }) fireEvent.click(enableButton) await waitFor(() => expect(enableButton.disabled).to.be.true) - const { body: tokenBody } = fetchMock.callHistory.calls().at(-1).options - expect(JSON.parse(tokenBody)).to.deep.equal({ + const tokenBody = fetchMock.callHistory.calls().at(-1)?.options.body + expect(JSON.parse(tokenBody as string)).to.deep.equal({ publicAccessLevel: 'tokenBased', }) @@ -796,14 +861,14 @@ describe('', function () { setPublicAccessLevel('tokenBased') await screen.findByText('Link sharing is on') - const disableButton = await screen.findByRole('button', { + const disableButton: HTMLButtonElement = await screen.findByRole('button', { name: 'Turn off link sharing', }) fireEvent.click(disableButton) await waitFor(() => expect(disableButton.disabled).to.be.true) - const { body: privateBody } = fetchMock.callHistory.calls().at(-1).options - expect(JSON.parse(privateBody)).to.deep.equal({ + const privateBody = fetchMock.callHistory.calls().at(-1)?.options.body + expect(JSON.parse(privateBody as string)).to.deep.equal({ publicAccessLevel: 'private', }) diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx index 16df207b88..e3ac0042d6 100644 --- a/services/web/test/frontend/helpers/editor-providers.tsx +++ b/services/web/test/frontend/helpers/editor-providers.tsx @@ -52,7 +52,7 @@ import { ProjectCompiler } from '../../../types/project-settings' // using magic strings export const PROJECT_ID = 'project123' export const PROJECT_NAME = 'project-name' -export const USER_ID = '123abd' +export const USER_ID = '123abd' as UserId export const USER_EMAIL = 'testuser@example.com' const defaultUserSettings = { @@ -91,7 +91,7 @@ export type EditorProvidersProps = { providers?: Record>> } -export const projectDefaults = { +export const projectDefaults: ProjectMetadata = { _id: PROJECT_ID, name: PROJECT_NAME, owner: { @@ -125,6 +125,8 @@ export const projectDefaults = { compiler: 'pdflatex' as ProjectCompiler, members: [], invites: [], + trackChangesState: {} as Record, + spellCheckLanguage: 'en', } /**