[web] Convert EditorProviders and some test files to Typescript (#26512)

* Rename files to tsx

* Update types

* Remove props that aren't typed

* Add `LayoutContextOwnStates`

* Use `LayoutContextOwnStates`

* Ignore ts errors about `SocketIOMock`

* Address comments: remove `satisfies`, update `BroadcastChannel` fixture

* Add types to `makeEditorOpenDocProvider`. Update `openDocId`->`currentDocumentId`

* misc.

* Type sockets as `SocketIOMock & Socket`

* Fix remaining typing errors

* Fix type of `ideReactContextValue`

GitOrigin-RevId: 2734ac707517d56c452b0bf06ea3438f947a64be
This commit is contained in:
Antoine Clausse
2025-07-04 12:50:45 +02:00
committed by Copybot
parent 779e346cb1
commit 12b3dcdfdc
17 changed files with 186 additions and 107 deletions

View File

@@ -39,6 +39,28 @@ const enableROMirrorOnClient =
new URLSearchParams(window.location.search).get('ro-mirror-on-client') ===
'enabled'
export type ToolbarHeaderProps = {
cobranding: Cobranding | undefined
onShowLeftMenuClick: () => void
chatIsOpen: boolean
toggleChatOpen: () => void
reviewPanelOpen: boolean
toggleReviewPanelOpen: (e: React.MouseEvent) => void
historyIsOpen: boolean
toggleHistoryOpen: () => void
unreadMessageCount: number
onlineUsers: OnlineUser[]
goToUser: (user: OnlineUser) => void
isRestrictedTokenMember: boolean | undefined
hasPublishPermissions: boolean
chatVisible: boolean
projectName: string
renameProject: (name: string) => void
hasRenamePermissions: boolean
openShareModal: () => void
trackChangesVisible: boolean | undefined
}
const ToolbarHeader = React.memo(function ToolbarHeader({
cobranding,
onShowLeftMenuClick,
@@ -59,27 +81,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
hasRenamePermissions,
openShareModal,
trackChangesVisible,
}: {
cobranding: Cobranding | undefined
onShowLeftMenuClick: () => void
chatIsOpen: boolean
toggleChatOpen: () => void
reviewPanelOpen: boolean
toggleReviewPanelOpen: (e: React.MouseEvent) => void
historyIsOpen: boolean
toggleHistoryOpen: () => void
unreadMessageCount: number
onlineUsers: OnlineUser[]
goToUser: (user: OnlineUser) => void
isRestrictedTokenMember: boolean | undefined
hasPublishPermissions: boolean
chatVisible: boolean
projectName: string
renameProject: (name: string) => void
hasRenamePermissions: boolean
openShareModal: () => void
trackChangesVisible: boolean | undefined
}) {
}: ToolbarHeaderProps) {
const chatEnabled = getMeta('ol-capabilities')?.includes('chat')
const { t } = useTranslation()

View File

@@ -11,12 +11,15 @@ import { DocId } from '../../../../../types/project-settings'
import useExposedState from '@/shared/hooks/use-exposed-state'
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
type EditorOpenDocContextValue = {
export interface EditorOpenDocContextState {
currentDocumentId: DocId | null
setCurrentDocumentId: Dispatch<SetStateAction<DocId | null>>
openDocName: string | null
setOpenDocName: Dispatch<SetStateAction<string | null>>
currentDocument: DocumentContainer | null
}
interface EditorOpenDocContextValue extends EditorOpenDocContextState {
setCurrentDocumentId: Dispatch<SetStateAction<DocId | null>>
setOpenDocName: Dispatch<SetStateAction<string | null>>
setCurrentDocument: Dispatch<SetStateAction<DocumentContainer | null>>
}

View File

@@ -1,4 +1,4 @@
import { FC } from 'react'
import React, { FC, PropsWithChildren } from 'react'
import { ChatProvider } from '@/features/chat/context/chat-context'
import { ConnectionProvider } from './connection-context'
import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
@@ -32,7 +32,7 @@ import { CommandRegistryProvider } from './command-registry-context'
export const ReactContextRoot: FC<
React.PropsWithChildren<{
providers?: Record<string, FC>
providers?: Record<string, FC<PropsWithChildren>>
}>
> = ({ children, providers = {} }) => {
const Providers = {

View File

@@ -25,37 +25,40 @@ import usePersistedState from '@/shared/hooks/use-persisted-state'
export type IdeLayout = 'sideBySide' | 'flat'
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
export type LayoutContextValue = {
export type LayoutContextOwnStates = {
view: IdeView | null
chatIsOpen: boolean
reviewPanelOpen: boolean
miniReviewPanelVisible: boolean
leftMenuShown: boolean
loadingStyleSheet: boolean
pdfLayout: IdeLayout
projectSearchIsOpen: boolean
openFile: BinaryFile | null
}
export type LayoutContextValue = LayoutContextOwnStates & {
reattach: () => void
detach: () => void
detachIsLinked: boolean
detachRole: DetachRole
changeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
view: IdeView | null
setView: (view: IdeView | null) => void
chatIsOpen: boolean
setChatIsOpen: Dispatch<SetStateAction<LayoutContextValue['chatIsOpen']>>
reviewPanelOpen: boolean
setReviewPanelOpen: Dispatch<
SetStateAction<LayoutContextValue['reviewPanelOpen']>
>
miniReviewPanelVisible: boolean
setMiniReviewPanelVisible: Dispatch<
SetStateAction<LayoutContextValue['miniReviewPanelVisible']>
>
leftMenuShown: boolean
setLeftMenuShown: Dispatch<
SetStateAction<LayoutContextValue['leftMenuShown']>
>
loadingStyleSheet: boolean
setLoadingStyleSheet: Dispatch<
SetStateAction<LayoutContextValue['loadingStyleSheet']>
>
pdfLayout: IdeLayout
pdfPreviewOpen: boolean
projectSearchIsOpen: boolean
setProjectSearchIsOpen: Dispatch<SetStateAction<boolean>>
openFile: BinaryFile | null
setOpenFile: Dispatch<SetStateAction<BinaryFile | null>>
}

View File

@@ -80,6 +80,7 @@ function mockProviders() {
return {
EditorOpenDocProvider: makeEditorOpenDocProvider({
openDocName: 'main.tex',
currentDocumentId: null,
currentDocument: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
@@ -87,7 +88,7 @@ function mockProviders() {
on: () => {},
off: () => {},
leaveAndCleanUpPromise: () => Promise.resolve(),
},
} as any,
}),
}
}

View File

@@ -5,12 +5,13 @@ 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'
import type { LayoutContextOwnStates } from '@/shared/context/layout-context'
describe('<LayoutDropdownButton />', function () {
let openStub
let sendMBSpy
let openStub: sinon.SinonStub
let sendMBSpy: sinon.SinonSpy
const defaultLayout = {
const defaultLayout: Partial<LayoutContextOwnStates> = {
pdfLayout: 'flat',
view: 'pdf',
chatIsOpen: false,
@@ -161,9 +162,10 @@ describe('<LayoutDropdownButton />', function () {
})
describe('on detach', async function () {
let originalBroadcastChannel
const originalBroadcastChannel = window.BroadcastChannel
beforeEach(async function () {
window.BroadcastChannel = originalBroadcastChannel || true // ensure that window.BroadcastChannel is truthy
// @ts-expect-error
window.BroadcastChannel = true // ensure that window.BroadcastChannel is truthy
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'editor' },

View File

@@ -8,12 +8,16 @@ describe('<OnlineUsersWidget />', function () {
const defaultProps = {
onlineUsers: [
{
id: 'test_user',
user_id: 'test_user',
name: 'test_user',
email: 'test_email',
},
{
id: 'another_test_user',
user_id: 'another_test_user',
name: 'another_test_user',
email: 'another_test_email',
},
],
goToUser: () => {},
@@ -44,8 +48,10 @@ describe('<OnlineUsersWidget />', function () {
fireEvent.click(icon)
expect(props.goToUser).to.be.calledWith({
name: 'test_user',
id: 'test_user',
user_id: 'test_user',
name: 'test_user',
email: 'test_email',
})
})
})
@@ -55,12 +61,16 @@ describe('<OnlineUsersWidget />', function () {
...defaultProps,
onlineUsers: defaultProps.onlineUsers.concat([
{
id: 'user_3',
user_id: 'user_3',
name: 'user_3',
email: 'user_3',
},
{
id: 'user_4',
user_id: 'user_4',
name: 'user_4',
email: 'user_4',
},
]),
}
@@ -96,8 +106,10 @@ describe('<OnlineUsersWidget />', function () {
fireEvent.click(icon)
expect(testProps.goToUser).to.be.calledWith({
name: 'user_3',
id: 'user_3',
user_id: 'user_3',
name: 'user_3',
email: 'user_3',
})
})
})

View File

@@ -1,11 +1,13 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import ToolbarHeader from '../../../../../frontend/js/features/editor-navigation-toolbar/components/toolbar-header'
import ToolbarHeader, {
type ToolbarHeaderProps,
} from '../../../../../frontend/js/features/editor-navigation-toolbar/components/toolbar-header'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<ToolbarHeader />', function () {
const defaultProps = {
const defaultProps: ToolbarHeaderProps = {
onShowLeftMenuClick: () => {},
toggleChatOpen: () => {},
toggleReviewPanelOpen: () => {},
@@ -19,11 +21,12 @@ describe('<ToolbarHeader />', function () {
hasPublishPermissions: true,
chatVisible: true,
trackChangesVisible: true,
handleChangeLayout: () => {},
pdfLayout: 'sideBySide',
view: 'editor',
reattach: () => {},
detach: () => {},
cobranding: undefined,
isRestrictedTokenMember: false,
hasRenamePermissions: true,
historyIsOpen: false,
chatIsOpen: false,
reviewPanelOpen: false,
}
beforeEach(function () {
@@ -40,6 +43,8 @@ describe('<ToolbarHeader />', function () {
const props = {
...defaultProps,
cobranding: {
brandId: 12,
brandVariationId: 12,
brandVariationHomeUrl: 'http://cobranding',
brandVariationName: 'variation',
logoImgUrl: 'http://cobranding/logo',

View File

@@ -1,6 +1,7 @@
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import type { Socket } from '@/features/ide-react/connection/types/socket'
describe('<FileTreeRoot/>', function () {
beforeEach(function () {
@@ -245,9 +246,9 @@ describe('<FileTreeRoot/>', function () {
})
describe('when deselecting files', function () {
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
const rootFolder = [
{
_id: 'root-folder-id',

View File

@@ -1,11 +1,12 @@
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import type { Socket } from '@/features/ide-react/connection/types/socket'
describe('FileTree Create Folder Flow', function () {
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
cy.window().then(win => {
win.metaAttributesCache.set('ol-user', { id: 'user1' })
})

View File

@@ -1,6 +1,7 @@
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import type { Socket } from '@/features/ide-react/connection/types/socket'
describe('FileTree Delete Entity Flow', function () {
beforeEach(function () {
@@ -10,9 +11,9 @@ describe('FileTree Delete Entity Flow', function () {
})
describe('single entity', function () {
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
const rootFolder = [
{
_id: 'root-folder-id',
@@ -136,9 +137,9 @@ describe('FileTree Delete Entity Flow', function () {
})
describe('folders', function () {
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
const rootFolder = [
{
_id: 'root-folder-id',
@@ -207,9 +208,9 @@ describe('FileTree Delete Entity Flow', function () {
})
describe('multiple entities', function () {
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
const rootFolder = [
{
_id: 'root-folder-id',

View File

@@ -1,6 +1,7 @@
import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root'
import { EditorProviders } from '../../../helpers/editor-providers'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import type { Socket } from '@/features/ide-react/connection/types/socket'
describe('FileTree Rename Entity Flow', function () {
beforeEach(function () {
@@ -9,9 +10,9 @@ describe('FileTree Rename Entity Flow', function () {
})
})
let socket: SocketIOMock
let socket: SocketIOMock & Socket
beforeEach(function () {
socket = new SocketIOMock()
socket = new SocketIOMock() as any
const rootFolder = [
{
_id: 'root-folder-id',

View File

@@ -3,7 +3,14 @@
import { merge } from 'lodash'
import { SocketIOMock } from '@/ide/connection/SocketIoShim'
import { IdeContext } from '@/shared/context/ide-context'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, {
useCallback,
useEffect,
useState,
useMemo,
type FC,
type PropsWithChildren,
} from 'react'
import {
createReactScopeValueStore,
IdeReactContext,
@@ -12,12 +19,25 @@ import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
import { ConnectionContext } from '@/features/ide-react/context/connection-context'
import { EditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import {
EditorOpenDocContext,
type EditorOpenDocContextState,
} from '@/features/ide-react/context/editor-open-doc-context'
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
import useEventListener from '@/shared/hooks/use-event-listener'
import useDetachLayout from '@/shared/hooks/use-detach-layout'
import { LayoutContext } from '@/shared/context/layout-context'
import useExposedState from '@/shared/hooks/use-exposed-state'
import {
type IdeLayout,
type IdeView,
LayoutContext,
type LayoutContextValue,
} from '@/shared/context/layout-context'
import type { Socket } from '@/features/ide-react/connection/types/socket'
import type { PermissionsLevel } from '@/features/ide-react/types/permissions'
import type { Folder } from '../../../types/folder'
import type { SocketDebuggingInfo } from '@/features/ide-react/connection/types/connection-state'
import type { DocumentContainer } from '@/features/ide-react/editor/document-container'
// these constants can be imported in tests instead of
// using magic strings
@@ -41,10 +61,26 @@ const defaultUserSettings = {
mathPreview: true,
}
/**
* @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue
* @type Partial<LayoutContextValue>
*/
export type EditorProvidersProps = {
user?: { id: string; email: string }
projectId?: string
projectOwner?: { _id: string; email: string }
rootDocId?: string
imageName?: string
compiler?: string
socket?: Socket
isRestrictedTokenMember?: boolean
scope?: Record<string, any>
features?: Record<string, boolean>
projectFeatures?: Record<string, boolean>
permissionsLevel?: PermissionsLevel
children?: React.ReactNode
rootFolder?: Folder[]
layoutContext?: Partial<LayoutContextValue>
userSettings?: Record<string, any>
providers?: Record<string, React.FC<React.PropsWithChildren<any>>>
}
const layoutContextDefault = {
view: 'editor',
openFile: null,
@@ -55,7 +91,7 @@ const layoutContextDefault = {
projectSearchIsOpen: false,
pdfLayout: 'sideBySide',
loadingStyleSheet: false,
}
} satisfies Partial<LayoutContextValue>
export function EditorProviders({
user = { id: USER_ID, email: USER_EMAIL },
@@ -67,7 +103,7 @@ export function EditorProviders({
rootDocId = '_root_doc_id',
imageName = 'texlive-full:2024.1',
compiler = 'pdflatex',
socket = new SocketIOMock(),
socket = new SocketIOMock() as any as Socket,
isRestrictedTokenMember = false,
scope: defaultScope = {},
features = {
@@ -94,7 +130,7 @@ export function EditorProviders({
layoutContext = layoutContextDefault,
userSettings = {},
providers = {},
}) {
}: EditorProvidersProps) {
window.metaAttributesCache.set(
'ol-gitBridgePublicBaseUrl',
'https://git.overleaf.test'
@@ -121,7 +157,7 @@ export function EditorProviders({
on: () => {},
off: () => {},
leaveAndCleanUpPromise: async () => {},
},
} as any as DocumentContainer,
openDocName: null,
currentDocumentId: null,
},
@@ -150,7 +186,7 @@ export function EditorProviders({
ConnectionProvider: makeConnectionProvider(socket),
IdeReactProvider: makeIdeReactProvider(scope, socket),
EditorOpenDocProvider: makeEditorOpenDocProvider({
openDocId: scope.editor.currentDocumentId,
currentDocumentId: scope.editor.currentDocumentId,
openDocName: scope.editor.openDocName,
currentDocument: scope.editor.sharejs_doc,
}),
@@ -163,8 +199,8 @@ export function EditorProviders({
)
}
const makeConnectionProvider = socket => {
const ConnectionProvider = ({ children }) => {
const makeConnectionProvider = (socket: Socket) => {
const ConnectionProvider: FC<PropsWithChildren> = ({ children }) => {
const [value] = useState(() => ({
socket,
connectionState: {
@@ -174,7 +210,7 @@ const makeConnectionProvider = socket => {
reconnectAt: null,
forcedDisconnectDelay: 0,
lastConnectionAttempt: 0,
error: '',
error: '' as const,
},
isConnected: true,
isStillReconnecting: false,
@@ -182,6 +218,8 @@ const makeConnectionProvider = socket => {
tryReconnectNow: () => {},
registerUserActivity: () => {},
disconnect: () => {},
closeConnection: () => {},
getSocketDebuggingInfo: () => ({}) as SocketDebuggingInfo,
}))
return (
@@ -193,8 +231,11 @@ const makeConnectionProvider = socket => {
return ConnectionProvider
}
const makeIdeReactProvider = (scope, socket) => {
const IdeReactProvider = ({ children }) => {
const makeIdeReactProvider = (
scope: Record<string, unknown>,
socket: Socket
) => {
const IdeReactProvider: FC<PropsWithChildren> = ({ children }) => {
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
const [ideReactContextValue] = useState(() => ({
@@ -204,7 +245,9 @@ const makeIdeReactProvider = (scope, socket) => {
setStartedFreeTrial,
reportError: () => {},
projectJoined: true,
permissionsLevel: scope.permissionsLevel,
permissionsLevel: scope.permissionsLevel as PermissionsLevel,
setPermissionsLevel: () => {},
setOutOfSync: () => {},
}))
const [ideContextValue] = useState(() => {
@@ -247,13 +290,15 @@ const makeIdeReactProvider = (scope, socket) => {
return IdeReactProvider
}
export function makeEditorOpenDocProvider(initialValues) {
export function makeEditorOpenDocProvider(
initialValues: EditorOpenDocContextState
) {
const {
currentDocumentId: initialCurrentDocumentId,
openDocName: initialOpenDocName,
currentDocument: initialCurrentDocument,
} = initialValues
const EditorOpenDocProvider = ({ children }) => {
const EditorOpenDocProvider: FC<PropsWithChildren> = ({ children }) => {
const [currentDocumentId, setCurrentDocumentId] = useExposedState(
initialCurrentDocumentId,
'editor.open_doc_id'
@@ -285,13 +330,15 @@ export function makeEditorOpenDocProvider(initialValues) {
return EditorOpenDocProvider
}
const makeLayoutProvider = layoutContextOverrides => {
const makeLayoutProvider = (
layoutContextOverrides?: Partial<LayoutContextValue>
) => {
const layout = {
...layoutContextDefault,
...layoutContextOverrides,
}
const LayoutProvider = ({ children }) => {
const [view, setView] = useState(layout.view)
const LayoutProvider: FC<PropsWithChildren> = ({ children }) => {
const [view, setView] = useState<IdeView | null>(layout.view)
const [openFile, setOpenFile] = useState(layout.openFile)
const [chatIsOpen, setChatIsOpen] = useState(layout.chatIsOpen)
const [reviewPanelOpen, setReviewPanelOpen] = useState(
@@ -316,7 +363,7 @@ const makeLayoutProvider = layoutContextOverrides => {
}, [setReviewPanelOpen])
)
const changeLayout = useCallback(
(newLayout, newView = 'editor') => {
(newLayout: IdeLayout, newView: IdeView = 'editor') => {
setPdfLayout(newLayout)
setView(newLayout === 'sideBySide' ? 'editor' : newView)
},

View File

@@ -1,20 +0,0 @@
// Disable prop type checks for test harnesses
/* eslint-disable react/prop-types */
import { render } from '@testing-library/react'
import { EditorProviders } from './editor-providers'
export function renderWithEditorContext(
component,
contextProps,
renderOptions = {}
) {
const EditorProvidersWrapper = ({ children }) => (
<EditorProviders {...contextProps}>{children}</EditorProviders>
)
return render(component, {
wrapper: EditorProvidersWrapper,
...renderOptions,
})
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import { render, type RenderOptions } from '@testing-library/react'
import { EditorProviders, type EditorProvidersProps } from './editor-providers'
export function renderWithEditorContext(
component: React.ReactElement,
contextProps: EditorProvidersProps = {},
renderOptions: RenderOptions = {}
) {
const EditorProvidersWrapper = ({
children,
}: {
children: React.ReactNode
}) => <EditorProviders {...contextProps}>{children}</EditorProviders>
return render(component, {
wrapper: EditorProvidersWrapper,
...renderOptions,
})
}