[web] Disable various editor buttons during network stall (#34247)

Co-authored-by: Davinder Singh <davibaweja@gmail.com>
GitOrigin-RevId: 9e169f3591cfff2fdae63b461b946fde4611f2d7
This commit is contained in:
Mathias Jakobsen
2026-09-17 11:25:49 +00:00
committed by Copybot
co-authored by Davinder Singh
parent 8741846df8
commit 1acb00407e
21 changed files with 351 additions and 20 deletions
@@ -9,11 +9,13 @@ export default function FileTreeActionButton({
description,
onClick,
iconType,
disabled,
}: {
id: string
description: string
onClick: () => void
iconType: AvailableUnfilledIcon
disabled?: boolean
}) {
return (
<OLTooltip
@@ -21,7 +23,11 @@ export default function FileTreeActionButton({
description={description}
overlayProps={{ placement: 'bottom' }}
>
<button className="btn file-tree-toolbar-action-button" onClick={onClick}>
<button
className="btn file-tree-toolbar-action-button"
onClick={onClick}
disabled={disabled}
>
<MaterialIcon
unfilled
type={iconType}
@@ -7,6 +7,7 @@ import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provi
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import FileTreeActionButton from './file-tree-action-button'
import { useRailContext } from '../../ide-react/context/rail-context'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
export default function FileTreeActionButtons({
fileTreeExpanded,
@@ -17,6 +18,7 @@ export default function FileTreeActionButtons({
const { fileTreeReadOnly } = useFileTreeData()
const { write } = usePermissionsContext()
const { handlePaneCollapse } = useRailContext()
const isDisabledDueToNetworkStall = useIsNetworkStalled()
const {
canCreate,
@@ -37,11 +39,13 @@ export default function FileTreeActionButtons({
eventTracking.sendMB('new-file-click', { location })
startCreatingDocOrFile()
},
disabled: isDisabledDueToNetworkStall,
},
{
label: t('new_folder'),
id: 'new_folder',
handler: startCreatingFolder,
disabled: isDisabledDueToNetworkStall,
},
{
label: t('upload_file'),
@@ -50,6 +54,7 @@ export default function FileTreeActionButtons({
eventTracking.sendMB('upload-click', { location })
startUploadingDocOrFile()
},
disabled: isDisabledDueToNetworkStall,
},
]
}, [
@@ -60,6 +65,7 @@ export default function FileTreeActionButtons({
startCreatingFolder,
startUploadingDocOrFile,
write,
isDisabledDueToNetworkStall,
])
if (fileTreeReadOnly) return null
@@ -84,6 +90,7 @@ export default function FileTreeActionButtons({
description={t('new_file')}
onClick={createWithAnalytics}
iconType="note_add"
disabled={isDisabledDueToNetworkStall}
/>
)}
{canCreate && (
@@ -92,6 +99,7 @@ export default function FileTreeActionButtons({
description={t('new_folder')}
onClick={startCreatingFolder}
iconType="create_new_folder"
disabled={isDisabledDueToNetworkStall}
/>
)}
{canCreate && (
@@ -100,6 +108,7 @@ export default function FileTreeActionButtons({
description={t('upload')}
onClick={uploadWithAnalytics}
iconType="upload"
disabled={isDisabledDueToNetworkStall}
/>
)}
{canBulkDelete && (
@@ -8,6 +8,7 @@ import {
OLDropdownItem,
} from '@/shared/components/ol/ol-dropdown-menu'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
function FileTreeItemMenuItems() {
const { t } = useTranslation()
@@ -47,11 +48,18 @@ function FileTreeItemMenuItems() {
startUploadingDocOrFile()
}, [startUploadingDocOrFile])
const isDisabledDueToNetworkStall = useIsNetworkStalled()
return (
<>
{canRename ? (
<li role="none">
<OLDropdownItem onClick={startRenaming}>{t('rename')}</OLDropdownItem>
<OLDropdownItem
onClick={startRenaming}
disabled={isDisabledDueToNetworkStall}
>
{t('rename')}
</OLDropdownItem>
</li>
) : null}
{downloadPath ? (
@@ -60,6 +68,7 @@ function FileTreeItemMenuItems() {
href={downloadPath}
onClick={downloadWithAnalytics}
download={selectedFileName ?? undefined}
disabled={isDisabledDueToNetworkStall}
>
{t('download')}
</OLDropdownItem>
@@ -69,7 +78,10 @@ function FileTreeItemMenuItems() {
<>
<OLDropdownDivider />
<li role="none">
<OLDropdownItem onClick={setRootDocId}>
<OLDropdownItem
onClick={setRootDocId}
disabled={isDisabledDueToNetworkStall}
>
{t('set_as_main_document')}
</OLDropdownItem>
</li>
@@ -79,7 +91,10 @@ function FileTreeItemMenuItems() {
<>
<OLDropdownDivider />
<li role="none">
<OLDropdownItem onClick={startDeleting}>
<OLDropdownItem
onClick={startDeleting}
disabled={isDisabledDueToNetworkStall}
>
{t('delete')}
</OLDropdownItem>
</li>
@@ -89,17 +104,26 @@ function FileTreeItemMenuItems() {
<>
<OLDropdownDivider />
<li role="none">
<OLDropdownItem onClick={createWithAnalytics}>
<OLDropdownItem
onClick={createWithAnalytics}
disabled={isDisabledDueToNetworkStall}
>
{t('new_file')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem onClick={startCreatingFolder}>
<OLDropdownItem
onClick={startCreatingFolder}
disabled={isDisabledDueToNetworkStall}
>
{t('new_folder')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem onClick={uploadWithAnalytics}>
<OLDropdownItem
onClick={uploadWithAnalytics}
disabled={isDisabledDueToNetworkStall}
>
{t('upload')}
</OLDropdownItem>
</li>
@@ -26,6 +26,7 @@ import { Folder } from '../../../../../types/folder'
import { FileTreeEntity } from '../../../../../types/file-tree-entity'
import { isMac } from '@/shared/utils/os'
import useEventListener from '@/shared/hooks/use-event-listener'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
const FileTreeSelectableContext = createContext<
| {
@@ -253,6 +254,7 @@ export function useSelectableEntity(id: string, type: string) {
const { view, setView } = useLayoutContext()
const { setContextMenuCoords } = useFileTreeMainContext()
const { fileTreeData } = useFileTreeData()
const isDisabledDueToNetworkStall = useIsNetworkStalled()
const {
selectedEntityIds,
selectOrMultiSelectEntity,
@@ -262,6 +264,11 @@ export function useSelectableEntity(id: string, type: string) {
const isSelected = selectedEntityIds.has(id)
// block opening a different doc/file while saving is stalled. Folders only
// expand or keep the current view, so they stay interactive.
const isDisabled =
isDisabledDueToNetworkStall && (type === 'doc' || type === 'file')
const buildSelectedRange = useCallback(
(id: string) => {
const selected = []
@@ -352,44 +359,63 @@ export function useSelectableEntity(id: string, type: string) {
const handleClick = useCallback(
(ev: any) => {
if (isDisabled) {
return
}
handleEvent(ev)
if (!ev.ctrlKey && !ev.metaKey) {
setContextMenuCoords(null)
}
},
[handleEvent, setContextMenuCoords]
[isDisabled, handleEvent, setContextMenuCoords]
)
const handleKeyPress = useCallback(
(ev: any) => {
if (isDisabled) {
return
}
if (ev.key === 'Enter' || ev.key === ' ') {
handleEvent(ev)
}
},
[handleEvent]
[isDisabled, handleEvent]
)
const handleContextMenu = useCallback(
(ev: any) => {
// make sure the right-clicked entity gets selected
if (isDisabled) {
return
}
if (!selectedEntityIds.has(id)) {
handleEvent(ev)
}
},
[id, handleEvent, selectedEntityIds]
[id, handleEvent, selectedEntityIds, isDisabled]
)
const isVisuallySelected =
!isRootFolderSelected && isSelected && view !== 'pdf'
const props = useMemo(
() => ({
className: classNames({ selected: isVisuallySelected }),
className: classNames({
selected: isVisuallySelected,
'entity-disabled': isDisabled,
}),
'aria-selected': isVisuallySelected,
'aria-disabled': isDisabled || undefined,
onClick: handleClick,
onContextMenu: handleContextMenu,
onKeyPress: handleKeyPress,
}),
[handleClick, handleContextMenu, handleKeyPress, isVisuallySelected]
[
handleClick,
handleContextMenu,
handleKeyPress,
isVisuallySelected,
isDisabled,
]
)
return { isSelected, props }
@@ -17,6 +17,7 @@ import CommandDropdown, {
MenuStructure,
} from './command-dropdown'
import { useRailContext } from '../../context/rail-context'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
import WordCountModal from '@/features/word-count-modal/components/word-count-modal'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
@@ -44,6 +45,7 @@ export const ToolbarMenuBar = () => {
const { setView, view } = useLayoutContext()
const { pdfUrl } = useCompileContext()
const wordCountEnabled = pdfUrl || isSplitTestEnabled('word-count-client')
const isDisabledDueToNetworkStall = useIsNetworkStalled()
const [showWordCountModal, setShowWordCountModal] = useState(false)
const [showCloneProjectModal, setShowCloneProjectModal] = useState(false)
const openProject = useOpenProject()
@@ -59,6 +61,7 @@ export const ToolbarMenuBar = () => {
{
type: 'command',
label: t('show_version_history'),
disabled: isDisabledDueToNetworkStall,
handler: () => {
setView(view === 'history' ? 'editor' : 'history')
},
@@ -76,14 +79,14 @@ export const ToolbarMenuBar = () => {
{
type: 'command',
label: t('make_a_copy'),
disabled: anonymous,
disabled: anonymous || isDisabledDueToNetworkStall,
handler: () => {
setShowCloneProjectModal(true)
},
id: 'copy_project',
},
],
[t, setView, view, wordCountEnabled, anonymous]
[t, setView, view, wordCountEnabled, anonymous, isDisabledDueToNetworkStall]
)
const fileMenuStructure: MenuStructure = useMemo(
() => [
@@ -4,11 +4,13 @@ import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCallback } from 'react'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
export default function ShowHistoryButton() {
const { t } = useTranslation()
const { view, setView, restoreView } = useLayoutContext()
const { sendEvent } = useEditorAnalytics()
const isDisabledDueToNetworkStall = useIsNetworkStalled()
const toggleHistoryOpen = useCallback(() => {
const action = view === 'history' ? 'close' : 'open'
@@ -32,6 +34,7 @@ export default function ShowHistoryButton() {
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
onClick={toggleHistoryOpen}
accessibilityLabel={t('history')}
disabled={isDisabledDueToNetworkStall}
/>
</OLTooltip>
</div>
@@ -34,6 +34,7 @@ type IdeReactContextValue = {
projectJoined: boolean
permissionsLevel: PermissionsLevel
setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
outOfSync: boolean
setOutOfSync: (value: boolean) => void
}
@@ -143,6 +144,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
setStartedFreeTrial,
permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
setPermissionsLevel,
outOfSync,
setOutOfSync,
projectId,
reportError,
@@ -18,11 +18,14 @@ const intermittentConnectionImprovementsEnabled = isSplitTestEnabled(
'intermittent-connection-improvements'
)
const STALL_AFTER_SECONDS = 10 // treat saving as stalled after this time
// lock the editor after this time if unsaved
const MAX_UNSAVED_SECONDS = intermittentConnectionImprovementsEnabled ? 600 : 30
type UnsavedDocsContextValue = {
unsavedDocs: Map<string, number>
isSavingStalled: boolean
isLocked: boolean
}
@@ -94,6 +97,7 @@ export const UnsavedDocsProvider: FC<React.PropsWithChildren> = ({
}, [openDocs, debugTimers])
const maxUnsavedSeconds = Math.max(0, ...unsavedDocs.values())
const isSavingStalled = maxUnsavedSeconds > STALL_AFTER_SECONDS
// lock the editor if at least one doc has been unsaved for too long
useEffect(() => {
@@ -123,8 +127,8 @@ export const UnsavedDocsProvider: FC<React.PropsWithChildren> = ({
}, [unsavedDocs])
const value = useMemo(
() => ({ unsavedDocs, isLocked }),
[unsavedDocs, isLocked]
() => ({ unsavedDocs, isSavingStalled, isLocked }),
[unsavedDocs, isSavingStalled, isLocked]
)
return (
@@ -0,0 +1,10 @@
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useUnsavedDocsContext } from '@/features/ide-react/context/unsaved-docs-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
export default function useIsNetworkStalled(): boolean {
const enabled = useFeatureFlag('intermittent-connection-improvements')
const { isSavingStalled } = useUnsavedDocsContext()
const { outOfSync } = useIdeReactContext()
return enabled && (isSavingStalled || outOfSync)
}
@@ -21,6 +21,7 @@ import {
TAB_USER_EDIT_EVENT,
tabsEvents,
} from '@/features/source-editor/extensions/tabs-listener'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
type TabProps = {
tab: EditorFileTab
@@ -65,6 +66,7 @@ export const Tab = memo(function Tab({
onTabDrop,
}: TabProps) {
const { t } = useTranslation()
const isDisabledDueToNetworkStall = useIsNetworkStalled()
const tabRef = useRef<HTMLDivElement>(null)
const [dropTargetPosition, setDropTargetPosition] = useState<
@@ -135,20 +137,20 @@ export const Tab = memo(function Tab({
const onClick = useCallback(
(e: React.MouseEvent) => {
if (e.button === 0) {
if (e.button === 0 && !isDisabledDueToNetworkStall) {
openTab(tab.id)
}
},
[openTab, tab]
[openTab, tab, isDisabledDueToNetworkStall]
)
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
if (e.key === 'Enter' && !isDisabledDueToNetworkStall) {
openTab(tab.id)
}
},
[openTab, tab]
[openTab, tab, isDisabledDueToNetworkStall]
)
const onCloseClick = useCallback(
@@ -213,6 +215,7 @@ export const Tab = memo(function Tab({
onDoubleClick={onDoubleClick}
role="tab"
aria-selected={isSelected}
aria-disabled={isDisabledDueToNetworkStall}
key={tab.id}
onClick={onClick}
onMouseUp={onMouseUp}
@@ -225,6 +228,7 @@ export const Tab = memo(function Tab({
'tab-drop-left': dropTargetPosition === 'left',
'tab-drop-right': dropTargetPosition === 'right',
'tab-temporary': tab.lifetime === 'temporary',
'tab-disabled': isDisabledDueToNetworkStall,
})}
>
<div className="editor-file-tab-content">
@@ -160,6 +160,12 @@ $dropdown-item-min-height: 36px;
color: var(--dropdown-text-active);
}
&.disabled,
&[aria-disabled='true'] {
color: var(--content-disabled);
pointer-events: none;
}
&.btn-link {
text-decoration: none;
@@ -4,6 +4,7 @@
--file-tree-item-color: var(--content-primary-dark);
--file-tree-bg: var(--bg-dark-primary);
--file-tree-item-selected-color: var(--green-10);
--file-tree-item-disabled-color: var(--content-disabled-dark);
--file-tree-item-dragging-bg: var(--green-40);
--file-tree-item-dragging-color: var(--content-primary-dark);
--file-tree-item-icon-dragging-color: var(--file-tree-item-dragging-color);
@@ -20,6 +21,7 @@
--file-tree-item-color: var(--content-primary);
--file-tree-bg: var(--white);
--file-tree-item-selected-color: var(--green-70);
--file-tree-item-disabled-color: var(--content-disabled);
--file-tree-item-dragging-bg: var(--green-60);
--file-tree-item-dragging-color: var(--white);
--file-tree-item-icon-dragging-color: var(--file-tree-item-dragging-color);
@@ -91,6 +93,11 @@
&:active {
color: var(--file-tree-expand-button-color);
}
&[disabled],
&:disabled {
border-color: transparent;
}
}
.file-tree-folder-list {
@@ -503,6 +510,13 @@
}
}
}
ul.file-tree-list li.entity-disabled > .entity > .entity-name {
&,
& .file-tree-icon {
color: var(--file-tree-item-disabled-color) !important;
}
}
}
// while dragging, the previously selected item gets no highlight
@@ -162,6 +162,14 @@
font-style: italic;
}
&.tab-disabled {
.editor-file-tab-content,
.editor-file-tab-icon .file-tree-icon,
.editor-file-tab-icon .linked-file-highlight {
color: var(--content-disabled-themed);
}
}
.editor-file-tab-action {
display: flex;
align-items: center;
@@ -1,5 +1,6 @@
import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc'
import { EditorProviders } from '../../../helpers/editor-providers'
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
import { FileTreeProvider } from '../helpers/file-tree-provider'
describe('<FileTreeDoc/>', function () {
@@ -74,4 +75,47 @@ describe('<FileTreeDoc/>', function () {
cy.findByRole('treeitem').click({ ctrlKey: true, cmdKey: true })
cy.findByRole('treeitem', { selected: true })
})
describe('Network stall', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }],
fileRefs: [],
folders: [],
},
]
beforeEach(function () {
cy.clock()
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'intermittent-connection-improvements': 'enabled',
})
})
cy.then(() => {
const now = performance.now()
cy.mount(
<EditorProviders
rootFolder={rootFolder as any}
providers={{
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
now - 11_000
),
}}
>
<FileTreeProvider>
<FileTreeDoc name="foo.tex" id="123abc" />
</FileTreeProvider>
</EditorProviders>
)
})
})
it('disables doc when saving is stalled', function () {
cy.tick(1000)
cy.findByRole('treeitem').should('have.attr', 'aria-disabled', 'true')
})
})
})
@@ -1,5 +1,6 @@
import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar'
import { EditorProviders } from '../../../helpers/editor-providers'
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
import { FileTreeProvider } from '../helpers/file-tree-provider'
describe('<FileTreeToolbar/>', function () {
@@ -55,4 +56,39 @@ describe('<FileTreeToolbar/>', function () {
cy.findAllByRole('button', { name: 'Upload' })
cy.findAllByRole('button', { name: 'Close' })
})
describe('Network stall', function () {
beforeEach(function () {
cy.clock()
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'intermittent-connection-improvements': 'enabled',
})
})
cy.then(() => {
const now = performance.now()
cy.mount(
<EditorProviders
rootDocId=""
providers={{
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
now - 11_000
),
}}
>
<FileTreeProvider>
<FileTreeToolbar />
</FileTreeProvider>
</EditorProviders>
)
})
})
it('disables action buttons when saving is stalled', function () {
cy.tick(1000)
cy.findByRole('button', { name: 'New file' }).should('be.disabled')
cy.findByRole('button', { name: 'New folder' }).should('be.disabled')
cy.findByRole('button', { name: 'Upload' }).should('be.disabled')
})
})
})
@@ -20,6 +20,7 @@ const defaultIdeReactContextValue = {
const defaultUnsavedDocsContextValue = {
unsavedDocs: new Map<string, number>(),
isLocked: false,
isSavingStalled: false,
}
const mount = ({
@@ -8,6 +8,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context'
const defaultUnsavedDocsContextValue = {
unsavedDocs: new Map<string, number>([['doc1', 16]]),
isLocked: false,
isSavingStalled: false,
}
const mockSocket = {
@@ -48,6 +48,7 @@ function mount(
projectJoined: true,
permissionsLevel: 'owner' as PermissionsLevel,
setPermissionsLevel: onSetPermissionsLevel,
outOfSync: false,
setOutOfSync: () => {},
}))
@@ -0,0 +1,54 @@
import React, { type FC, type PropsWithChildren } from 'react'
import {
EditorManager,
EditorManagerContext,
} from '@/features/ide-react/context/editor-manager-context'
export function makeEditorManagerProviderWithStaleDocs(
pendingAt: number,
stubs?: {
openDocWithId?: any
openDoc?: any
openFileWithId?: any
}
) {
const staleDocs = [
{
doc_id: 'stale-doc',
getInflightOpCreatedAt: () => undefined,
getPendingOpCreatedAt: () => pendingAt,
},
]
const EditorManagerProviderWithStaleDocs: FC<PropsWithChildren> = ({
children,
}) => {
const value = {
getEditorType: () => null,
getCurrentDocValue: () => null,
getCurrentDocumentId: () => null,
setIgnoringExternalUpdates: () => {},
openDocWithId: stubs?.openDocWithId ?? cy.stub().resolves(),
openDoc: stubs?.openDoc ?? cy.stub().resolves(),
openDocs: {
awaitBufferedOps: cy.stub().resolves(),
unsavedDocs: () => staleDocs,
hasUnsavedChanges: () => true,
getUnsavedOpsSize: () => ({
pendingOpsLength: 0,
inflightOpsLength: 0,
}),
} as any,
openFileWithId: stubs?.openFileWithId ?? cy.stub(),
openInitialDoc: cy.stub().resolves(),
isLoading: false,
jumpToLine: () => {},
debugTimers: { current: {} },
} as unknown as EditorManager
return (
<EditorManagerContext.Provider value={value}>
{children}
</EditorManagerContext.Provider>
)
}
return EditorManagerProviderWithStaleDocs
}
@@ -1,5 +1,6 @@
import React, { FC, useEffect, useRef, useState } from 'react'
import { EditorProviders } from '../../../helpers/editor-providers'
import { makeEditorManagerProviderWithStaleDocs } from '../../ide-react/helpers/editor-manager-provider-with-stale-docs'
import { TabsContainer } from '../../../../../frontend/js/features/source-editor/components/tabs/tabs-container'
import {
FileTreeDocumentFindResult,
@@ -1177,6 +1178,79 @@ describe('File Tabs', function () {
})
})
describe('Network stall', function () {
function mountWithStaleDocs() {
cy.then(() => {
const now = performance.now()
const openDocWithId = cy.stub().as('openDocWithId').resolves()
const openDoc = cy.stub().as('openDoc').resolves()
const openFileWithId = cy.stub().as('openFileWithId')
cy.mount(
<EditorProviders
rootFolder={defaultRootFolder as any}
rootDocId={DOC_IDS.main}
providers={{
EditorManagerProvider: makeEditorManagerProviderWithStaleDocs(
now - 11_000,
{ openDocWithId, openDoc, openFileWithId }
),
EditorViewProvider: makeEditorViewProvider(),
}}
>
<FileTree />
<TabsContainer />
</EditorProviders>
)
})
}
beforeEach(function () {
cy.clock()
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'editor-tabs': 'enabled',
'intermittent-connection-improvements': 'enabled',
})
})
mountWithStaleDocs()
})
it('disables tabs when saving is stalled', function () {
cy.then(() => selectDoc(DOC_IDS.main))
cy.tick(1000)
cy.findByRole('tab', { name: /main\.tex/ }).should(
'have.attr',
'aria-disabled',
'true'
)
})
it('does not switch the active tab when clicking a disabled tab', function () {
cy.then(() => selectDoc(DOC_IDS.main))
cy.then(() => selectDoc(DOC_IDS.intro))
cy.tick(1000)
cy.get('@openDocWithId').invoke('resetHistory')
cy.findByRole('tab', { name: /main\.tex/ }).click()
cy.get('@openDocWithId').should('not.have.been.called')
})
it('does not disable tabs when the feature flag is off', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-splitTestVariants', {
'editor-tabs': 'enabled',
})
})
mountWithStaleDocs()
cy.then(() => selectDoc(DOC_IDS.main))
cy.tick(1000)
cy.findByRole('tab', { name: /main\.tex/ }).should(
'not.have.attr',
'aria-disabled',
'true'
)
})
})
describe('SplitTestBadge', function () {
it('renders the labs badge icon in the tabs container', function () {
cy.window().then(win => {
@@ -393,6 +393,7 @@ const makeIdeReactProvider = (
projectJoined: true,
permissionsLevel: scope.permissionsLevel as PermissionsLevel,
setPermissionsLevel: () => {},
outOfSync: false,
setOutOfSync: () => {},
}))