Merge pull request #31397 from overleaf/mj-old-editor-toolbar-teardown

[web] Tear down old editor toolbar

GitOrigin-RevId: 8ba74abcc56e7bd476a9d6cae72f38486168c2ed
This commit is contained in:
Mathias Jakobsen
2026-02-11 09:07:14 +00:00
committed by Copybot
parent 4f284e15d5
commit cc1dedca7d
32 changed files with 463 additions and 1796 deletions
@@ -1,26 +0,0 @@
import { expect } from 'chai'
import { render, screen } from '@testing-library/react'
import ChatToggleButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/chat-toggle-button'
describe('<ChatToggleButton />', function () {
const defaultProps = {
chatIsOpen: false,
unreadMessageCount: 0,
onClick: () => {},
}
it('displays the number of unread messages', function () {
const props = {
...defaultProps,
unreadMessageCount: 113,
}
render(<ChatToggleButton {...props} />)
screen.getByText('113')
})
it("doesn't display the unread messages badge when the number of unread messages is zero", function () {
render(<ChatToggleButton {...defaultProps} />)
expect(screen.queryByText('0')).to.not.exist
})
})
@@ -1,237 +0,0 @@
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import { expect } from 'chai'
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: sinon.SinonStub
let sendMBSpy: sinon.SinonSpy
const defaultLayout: Partial<LayoutContextOwnStates> = {
pdfLayout: 'flat',
view: 'pdf',
chatIsOpen: false,
}
beforeEach(function () {
openStub = sinon.stub(window, 'open')
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
})
afterEach(function () {
openStub.restore()
sendMBSpy.restore()
fetchMock.removeRoutes().clearHistory()
})
it('should mark current layout option as selected', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: defaultLayout,
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('true')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
it('should not select any option in history view', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'history' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
it('should treat file and editor views the same way', async function () {
// Selected is aria-label, visually we show a checkmark
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: {
pdfLayout: 'flat',
view: 'file',
chatIsOpen: false,
},
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
expect(
screen
.getByRole('menuitem', {
name: 'Editor & PDF',
})
.getAttribute('aria-selected')
).to.equal('false')
)
expect(
screen
.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
.getAttribute('aria-selected')
).to.equal('false')
expect(
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.getAttribute('aria-selected')
).to.equal('true')
expect(
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.getAttribute('aria-selected')
).to.equal('false')
})
describe('on detach', async function () {
const originalBroadcastChannel = window.BroadcastChannel
beforeEach(async function () {
// @ts-expect-error
window.BroadcastChannel = true // ensure that window.BroadcastChannel is truthy
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'editor' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'PDF in separate tab',
})
.click()
)
})
afterEach(function () {
window.BroadcastChannel = originalBroadcastChannel
})
it('should show processing', async function () {
await screen.findByText('Layout processing')
})
it('should record event', function () {
sinon.assert.calledWith(sendMBSpy, 'project-layout-detach')
})
})
describe('on layout change / reattach', async function () {
beforeEach(async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderWithEditorContext(<LayoutDropdownButton />, {
layoutContext: { ...defaultLayout, view: 'editor' },
})
screen.getByRole('button', { name: 'Layout' }).click()
await waitFor(() =>
screen
.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
.click()
)
})
it('should not show processing', function () {
const processingText = screen.queryByText('Layout processing')
expect(processingText).to.not.exist
})
it('should record events', function () {
sinon.assert.calledWith(sendMBSpy, 'project-layout-reattach')
sinon.assert.calledWith(sendMBSpy, 'project-layout-change', {
layout: 'flat',
view: 'editor',
page: '/detacher',
'editor-redesign': 'enabled',
})
})
it('should select new menu item', function () {
screen.getByRole('menuitem', {
name: 'Editor only (hide PDF)',
})
})
})
})
@@ -1,115 +1,113 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { fireEvent, render, screen } from '@testing-library/react'
import { OnlineUsersWidget } from '@/features/editor-navigation-toolbar/components/online-users-widget'
import OnlineUsersWidget from '../../../../../frontend/js/features/editor-navigation-toolbar/components/online-users-widget'
const names = ['alice', 'bob', 'charlie', 'dave', 'erin', 'frank', 'grace']
function makeUsers(count: number) {
return Array.from({ length: count }, (_, index) => ({
id: `user_${index + 1}`,
user_id: `user_id_${index + 1}`,
name: names[index % names.length],
email: `${names[index % names.length]}_email`,
}))
}
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: (async () => {}) as any,
onlineUsers: makeUsers(2),
goToUser: sinon.stub(),
}
describe('with less than 4 users', function () {
describe('with less than 5 users', function () {
it('displays user initials', function () {
render(<OnlineUsersWidget {...defaultProps} />)
screen.getByText('t')
screen.getByText('a')
screen.getByText('b')
})
it('displays user name in a tooltip', async function () {
render(<OnlineUsersWidget {...defaultProps} />)
const icon = screen.getByText('t')
const icon = screen.getByText('a')
fireEvent.mouseOver(icon)
await screen.findByRole('tooltip', { name: 'test_user' })
await screen.findByRole('tooltip', { name: 'alice' })
})
it('calls "goToUser" when the user initial is clicked', function () {
const props = {
...defaultProps,
goToUser: sinon.stub(),
}
render(<OnlineUsersWidget {...props} />)
render(<OnlineUsersWidget {...defaultProps} />)
const icon = screen.getByText('t')
const icon = screen.getByText('a')
fireEvent.click(icon)
expect(props.goToUser).to.be.calledWith({
id: 'test_user',
user_id: 'test_user',
name: 'test_user',
email: 'test_email',
expect(defaultProps.goToUser).to.be.calledWith({
id: 'user_1',
user_id: 'user_id_1',
name: 'alice',
email: 'alice_email',
})
})
})
describe('with 4 users and more', function () {
describe('with 5 users', function () {
const props = {
...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',
},
]),
onlineUsers: makeUsers(5),
}
it('displays the count of users', function () {
it('displays user initials', function () {
render(<OnlineUsersWidget {...props} />)
screen.getByText('4')
screen.getByText('a')
screen.getByText('b')
screen.getByText('c')
screen.getByText('d')
screen.getByText('e')
})
})
describe('with more than 5 users', function () {
const props = {
...defaultProps,
onlineUsers: makeUsers(7),
}
it('displays a maximum of 4 user initials and an overflow icon', function () {
render(<OnlineUsersWidget {...props} />)
screen.getByText('a')
screen.getByText('b')
screen.getByText('c')
screen.getByText('d')
screen.getByText('+3')
})
it('displays user names on hover', function () {
it('displays the remaining users in a dropdown when the overflow icon is clicked', async function () {
render(<OnlineUsersWidget {...props} />)
const overflowButton = screen.getByText('+3')
fireEvent.click(overflowButton)
const toggleButton = screen.getByRole('button')
fireEvent.click(toggleButton)
screen.getByText('test_user')
screen.getByText('another_test_user')
screen.getByText('user_3')
screen.getByText('user_4')
await screen.findByText('erin')
await screen.findByText('frank')
await screen.findByText('grace')
})
it('calls "goToUser" when the user name is clicked', function () {
const testProps = {
...props,
goToUser: sinon.stub(),
it('calls "goToUser" when a user in the overflow dropdown is clicked', async function () {
const props = {
...defaultProps,
onlineUsers: makeUsers(7),
}
render(<OnlineUsersWidget {...testProps} />)
const toggleButton = screen.getByRole('button')
fireEvent.click(toggleButton)
render(<OnlineUsersWidget {...props} />)
const overflowButton = screen.getByText('+3')
fireEvent.click(overflowButton)
const icon = screen.getByText('user_3')
fireEvent.click(icon)
const frankButton = await screen.findByText('frank')
fireEvent.click(frankButton)
expect(testProps.goToUser).to.be.calledWith({
id: 'user_3',
user_id: 'user_3',
name: 'user_3',
email: 'user_3',
expect(props.goToUser).to.be.calledWith({
id: 'user_6',
user_id: 'user_id_6',
name: 'frank',
email: 'frank_email',
})
})
})
@@ -1,78 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { fireEvent, render, screen } from '@testing-library/react'
import ProjectNameEditableLabel from '../../../../../frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label'
describe('<ProjectNameEditableLabel />', function () {
const defaultProps = { projectName: 'test-project', onChange: () => {} }
it('displays the project name', function () {
render(<ProjectNameEditableLabel {...defaultProps} />)
screen.getByText('test-project')
})
describe('when the name is editable', function () {
const editableProps = { ...defaultProps, hasRenamePermissions: true }
it('displays an editable input when the edit button is clicked', function () {
render(<ProjectNameEditableLabel {...editableProps} />)
fireEvent.click(screen.getByRole('button'))
screen.getByRole('textbox')
})
it('displays an editable input when the project name is double clicked', function () {
render(<ProjectNameEditableLabel {...editableProps} />)
fireEvent.doubleClick(screen.getByText('test-project'))
screen.getByRole('textbox')
})
it('calls "onChange" when the project name is updated', function () {
const props = {
...editableProps,
onChange: sinon.stub(),
}
render(<ProjectNameEditableLabel {...props} />)
fireEvent.doubleClick(screen.getByText('test-project'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'new project name' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(props.onChange).to.be.calledWith('new project name')
})
it('calls "onChange" when the input loses focus', function () {
const props = {
...editableProps,
onChange: sinon.stub(),
}
render(<ProjectNameEditableLabel {...props} />)
fireEvent.doubleClick(screen.getByText('test-project'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'new project name' } })
fireEvent.blur(screen.getByRole('textbox'))
expect(props.onChange).to.be.calledWith('new project name')
})
})
describe('when the name is not editable', function () {
const nonEditableProps = { hasRenamePermissions: false, ...defaultProps }
it('the edit button is not displayed', function () {
render(<ProjectNameEditableLabel {...nonEditableProps} />)
expect(screen.queryByRole('button')).to.not.exist
})
it('does not display an editable input when the project name is double clicked', function () {
render(<ProjectNameEditableLabel {...nonEditableProps} />)
fireEvent.doubleClick(screen.getByText('test-project'))
expect(screen.queryByRole('textbox')).to.not.exist
})
})
})
@@ -1,121 +0,0 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
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: ToolbarHeaderProps = {
onShowLeftMenuClick: () => {},
toggleChatOpen: () => {},
toggleReviewPanelOpen: () => {},
toggleHistoryOpen: () => {},
unreadMessageCount: 0,
onlineUsers: [],
goToUser: (async () => {}) as any,
projectName: 'test project',
renameProject: () => {},
openShareModal: () => {},
hasPublishPermissions: true,
chatVisible: true,
trackChangesVisible: true,
cobranding: undefined,
isRestrictedTokenMember: false,
hasRenamePermissions: true,
historyIsOpen: false,
chatIsOpen: false,
reviewPanelOpen: false,
}
beforeEach(function () {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
describe('cobranding logo', function () {
it('is not displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
expect(screen.queryByRole('link', { name: 'variation' })).to.not.exist
})
it('is displayed when cobranding data is available', function () {
const props = {
...defaultProps,
cobranding: {
brandId: 12,
brandVariationId: 12,
brandVariationHomeUrl: 'http://cobranding',
brandVariationName: 'variation',
logoImgUrl: 'http://cobranding/logo',
},
}
renderWithEditorContext(<ToolbarHeader {...props} />)
screen.getByRole('link', { name: 'variation' })
})
})
describe('track changes toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Review')
})
it('is not displayed when "trackChangesVisible" prop is set to false', function () {
const props = {
...defaultProps,
trackChangesVisible: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Review')).to.not.exist
})
})
describe('History toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('History')
})
it('is not displayed when "isRestrictedTokenMember" prop is set to true', function () {
const props = {
...defaultProps,
isRestrictedTokenMember: true,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('History')).to.not.exist
})
})
describe('Chat toggle button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Chat')
})
it('is not displayed when "chatVisible" prop is set to false', function () {
const props = {
...defaultProps,
chatVisible: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Chat')).to.not.exist
})
})
describe('Publish button', function () {
it('is displayed by default', function () {
renderWithEditorContext(<ToolbarHeader {...defaultProps} />)
screen.getByText('Submit')
})
it('is not displayed for users with no publish permissions', function () {
const props = {
...defaultProps,
hasPublishPermissions: false,
}
renderWithEditorContext(<ToolbarHeader {...props} />)
expect(screen.queryByText('Submit')).to.not.exist
})
})
})