Files
huly-platform/desktop/src/__test__/ui/menuBuilder.test.ts
T
Denis 'GeneralGDA' GladkiyandGitHub 84431f7c6b Drawing board undo/redo and bug fixes (#9726)
* Undo-redo for screenshot drawer, several bugfixes.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Undo-redo for collaborative drawer.

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Drawing board refinement: bugfixes, component refactoring and unit testing

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* More tests, canvas resize fix, types for different points

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Version lock

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Names refactoring

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Formatting

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

* Formatting

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>

---------

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>
2025-08-28 11:54:27 +06:00

366 lines
13 KiB
TypeScript

//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import '@testing-library/jest-dom'
import { MenuBuilder } from '../../ui/titleBarMenu'
import { MenuBarAction } from '../../ui/types'
describe('MenuBuilder', () => {
let systemUnderTest: MenuBuilder
beforeEach(() => {
systemUnderTest = new MenuBuilder()
document.body.innerHTML = ''
})
function verifyTopLevelMenu (menuBar: HTMLElement, topLevelMenu: number, expectedMenuName: string, expectedAccelerator: string): void {
const menuButton = menuBar.children[topLevelMenu].children[0] as HTMLElement
expect(menuButton.dataset.menu).toBe(expectedMenuName)
expect(menuButton.dataset.accelerator).toBe(expectedAccelerator)
}
describe('addTopLevelMenu', () => {
test('add a top-level menu', () => {
systemUnderTest.addTopLevelMenu('File', 'f')
const builtMenu = systemUnderTest.build()
expect(builtMenu.children).toHaveLength(1)
verifyTopLevelMenu(builtMenu, 0, 'file', 'f')
})
test('method chaining', () => {
const result = systemUnderTest
.addTopLevelMenu('File', 'f')
.addTopLevelMenu('Edit', 'e')
expect(result).toBe(systemUnderTest)
})
test('add multiple top-level menus', () => {
systemUnderTest
.addTopLevelMenu('File', 'f')
.addTopLevelMenu('Edit', 'e')
.addTopLevelMenu('View', 'v')
const builtMenu = systemUnderTest.build()
expect(builtMenu.children).toHaveLength(3)
verifyTopLevelMenu(builtMenu, 0, 'file', 'f')
verifyTopLevelMenu(builtMenu, 1, 'edit', 'e')
verifyTopLevelMenu(builtMenu, 2, 'view', 'v')
})
})
describe('addMenuItem', () => {
beforeEach(() => {
systemUnderTest.addTopLevelMenu('File', 'f')
})
test('add a menu item to existing top-level menu', () => {
systemUnderTest.addMenuItem(0, 'New', 'redo', 'Ctrl+N')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
expect(dropdown.children).toHaveLength(1)
const menuItem = dropdown.children[0] as HTMLElement
expect(menuItem.dataset.action).toBe('redo')
})
test('add menu item with custom accelerator character', () => {
systemUnderTest.addMenuItem(0, 'Save As', 'select-all', 'Ctrl+Shift+S', 'a')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
const menuItem = dropdown.children[0] as HTMLElement
expect(menuItem.dataset.accelerator).toBe('a')
})
test('default accelerator', () => {
systemUnderTest.addMenuItem(0, 'Open', 'undo', 'Ctrl+O')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
const menuItem = dropdown.children[0] as HTMLElement
expect(menuItem.dataset.accelerator).toBe('o')
})
test.each([[999], [-1]])('invalid top-level menu index', (input: number) => {
systemUnderTest.addMenuItem(input, 'Invalid', 'invalid' as MenuBarAction, 'Ctrl+I')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
expect(dropdown.children).toHaveLength(0)
})
test('add multiple menu items to the same top-level menu', () => {
systemUnderTest
.addMenuItem(0, 'New', 'paste', 'Ctrl+N')
.addMenuItem(0, 'Open', 'cut', 'Ctrl+O')
.addMenuItem(0, 'Save', 'copy', 'Ctrl+S')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
expect(dropdown.children).toHaveLength(3)
})
})
describe('addSeparator', () => {
beforeEach(() => {
systemUnderTest.addTopLevelMenu('File', 'f')
})
test('add a separator', () => {
systemUnderTest.addSeparator(0)
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
expect(dropdown.children).toHaveLength(1)
expect(dropdown.children[0]).toHaveClass('desktop-app-dropdown-separator')
})
test.each([[999], [-1]])('invalid top-level menu index', (input: number) => {
systemUnderTest.addSeparator(999)
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
expect(dropdown.children).toHaveLength(0)
})
})
describe('build', () => {
test('build empty menu bar', () => {
const builtMenu = systemUnderTest.build()
expect(builtMenu).toHaveClass('desktop-app-menu-bar')
expect(builtMenu.children).toHaveLength(0)
})
test('menu bar with complex structure of a single top-level menu', () => {
const topLevelMenu = 0
systemUnderTest
.addTopLevelMenu('File', 'f')
.addMenuItem(topLevelMenu, 'New', 'paste', 'Ctrl+N')
.addSeparator(topLevelMenu)
.addMenuItem(topLevelMenu, 'Exit', 'exit', 'Alt+F4')
const builtMenu = systemUnderTest.build()
expect(builtMenu).toHaveClass('desktop-app-menu-bar')
expect(builtMenu.children).toHaveLength(1)
const topLevelMenuItem = builtMenu.children[0]
expect(topLevelMenuItem).toHaveClass('desktop-app-menu-item')
expect(topLevelMenuItem.children).toHaveLength(2)
const topButton = topLevelMenuItem.children[0]
expect(topButton).toHaveClass('desktop-app-top-menu-button')
const dropdown = topLevelMenuItem.children[1]
expect(dropdown).toHaveClass('desktop-app-dropdown-menu')
expect(dropdown.id).toBe('file-menu')
expect(dropdown.children).toHaveLength(3) // New, separator, Exit
})
test('accelerator at the beginning of label', () => {
systemUnderTest.addTopLevelMenu('File', 'f')
const builtMenu = systemUnderTest.build()
const topButton = builtMenu.children[0].children[0]
expect(topButton.textContent).toBe('File')
const acceleratorSpan = topButton.querySelector('.desktop-app-accelerator')
expect(acceleratorSpan).toBeTruthy()
expect(acceleratorSpan?.textContent).toBe('F')
})
test('accelerator in the middle of label', () => {
systemUnderTest.addTopLevelMenu('Edit', 'i') // 'i' is in the middle of 'Edit'
const builtMenu = systemUnderTest.build()
const topButton = builtMenu.children[0].children[0]
expect(topButton.textContent).toBe('Edit')
const acceleratorSpan = topButton.querySelector('.desktop-app-accelerator')
expect(acceleratorSpan).toBeTruthy()
expect(acceleratorSpan?.textContent).toBe('i')
})
test('accelerator not found in label', () => {
systemUnderTest.addTopLevelMenu('File', 'z') // 'z' is not in 'File'
const builtMenu = systemUnderTest.build()
const topButton = builtMenu.children[0].children[0]
expect(topButton.textContent).toBe('File')
expect(topButton.querySelector('.desktop-app-accelerator')).toBeNull()
})
test('dropdown items with shortcuts', () => {
systemUnderTest
.addTopLevelMenu('File', 'f')
.addMenuItem(0, 'New', 'paste', 'Ctrl+N')
const builtMenu = systemUnderTest.build()
const dropdownItem = builtMenu.children[0].children[1].children[0] as HTMLElement
expect(dropdownItem).toHaveClass('desktop-app-dropdown-item')
expect(dropdownItem.dataset.action).toBe('paste')
const shortcutSpan = dropdownItem.querySelector('.desktop-app-shortcut')
expect(shortcutSpan).toBeTruthy()
expect(shortcutSpan?.textContent).toBe('Ctrl+N')
})
test('without shortcuts when empty', () => {
systemUnderTest
.addTopLevelMenu('File', 'f')
.addMenuItem(0, 'New', 'paste', '')
const builtMenu = systemUnderTest.build()
const dropdownItem = builtMenu.children[0].children[1].children[0]
const shortcutSpan = dropdownItem.querySelector('.desktop-app-shortcut')
expect(shortcutSpan).toBeNull()
})
test('accelerator characters in dropdown items', () => {
systemUnderTest
.addTopLevelMenu('File', 'f')
.addMenuItem(0, 'New', 'paste', 'Ctrl+N')
.addMenuItem(0, 'Open', 'cut', 'Ctrl+O', 'p')
const builtMenu = systemUnderTest.build()
const dropdown = builtMenu.children[0].children[1]
const newItem = dropdown.children[0] as HTMLElement
const openItem = dropdown.children[1] as HTMLElement
expect(newItem.dataset.accelerator).toBe('n')
expect(openItem.dataset.accelerator).toBe('p')
const newAccelerator = newItem.querySelector('.desktop-app-accelerator')
const openAccelerator = openItem.querySelector('.desktop-app-accelerator')
expect(newAccelerator?.textContent).toBe('N')
expect(openAccelerator?.textContent).toBe('p')
})
test('complex menu structure', () => {
systemUnderTest
.addTopLevelMenu('File', 'f')
.addMenuItem(0, 'New', 'paste', 'Ctrl+N')
.addMenuItem(0, 'Open', 'cut', 'Ctrl+O')
.addSeparator(0)
.addMenuItem(0, 'Save', 'cut', 'Ctrl+S')
.addMenuItem(0, 'Save As', 'cut', 'Ctrl+Shift+S')
.addSeparator(0)
.addMenuItem(0, 'Exit', 'cut', 'Alt+F4')
.addTopLevelMenu('Edit', 'e')
.addMenuItem(1, 'Undo', 'cut', 'Ctrl+Z')
.addMenuItem(1, 'Redo', 'cut', 'Ctrl+Y')
const builtMenu = systemUnderTest.build()
// Should have 2 top-level menus
expect(builtMenu.children).toHaveLength(2)
// File menu should have 7 items (5 menu items + 2 separators)
const fileDropdown = builtMenu.children[0].children[1]
expect(fileDropdown.children).toHaveLength(7)
// Edit menu should have 2 items
const editDropdown = builtMenu.children[1].children[1]
expect(editDropdown.children).toHaveLength(2)
})
})
describe('edge cases', () => {
test('empty labels', () => {
systemUnderTest
.addTopLevelMenu('', 'f')
.addMenuItem(0, '', 'cut', 'shortcut')
const builtMenu = systemUnderTest.build()
expect(builtMenu.children).toHaveLength(1)
const dropdown = builtMenu.children[0].children[1]
expect(dropdown).toHaveClass('desktop-app-dropdown-menu')
expect(dropdown.children).toHaveLength(1)
})
test('special characters in labels', () => {
systemUnderTest
.addTopLevelMenu('File & Edit', 'f')
.addMenuItem(0, 'Save & Exit', 'cut', 'Ctrl+S')
const builtMenu = systemUnderTest.build()
expect(builtMenu.children).toHaveLength(1)
const topButton = builtMenu.children[0].children[0]
expect(topButton.textContent).toBe('File & Edit')
})
test('case-insensitive accelerator matching', () => {
systemUnderTest
.addTopLevelMenu('File', 'F') // uppercase F
.addMenuItem(0, 'New', 'paste', 'Ctrl+N', 'N') // uppercase N
const builtMenu = systemUnderTest.build()
const dropdownItem = builtMenu.children[0].children[1].children[0] as HTMLElement
expect(dropdownItem.dataset.accelerator).toBe('n')
const topButton = builtMenu.children[0].children[0] as HTMLElement
expect(topButton.dataset.accelerator).toBe('f')
// Check that the actual character from the label is preserved
const topAccelerator = topButton.querySelector('.desktop-app-accelerator')
const itemAccelerator = dropdownItem.querySelector('.desktop-app-accelerator')
expect(topAccelerator?.textContent).toBe('F')
expect(itemAccelerator?.textContent).toBe('N')
})
test('unicode characters', () => {
systemUnderTest
.addTopLevelMenu('Файл', 'ф')
.addMenuItem(0, 'Новый', 'undo', 'Ctrl+N')
const builtMenu = systemUnderTest.build()
const topButton = builtMenu.children[0].children[0]
expect(topButton.textContent).toBe('Файл')
const acceleratorSpan = topButton.querySelector('.desktop-app-accelerator')
expect(acceleratorSpan?.textContent).toBe('Ф')
})
})
})