| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type {BookmarkElement, BookmarksAppElement, BookmarksFolderNodeElement, BookmarksItemElement, BookmarksListElement, DndManager} from 'chrome://bookmarks/bookmarks.js'; |
| import {BookmarkManagerApiProxyImpl, BrowserProxyImpl, DragInfo, overrideFolderOpenerTimeoutDelay, setDebouncerForTesting} from 'chrome://bookmarks/bookmarks.js'; |
| import {assertDeepEquals, assertEquals, assertFalse, assertNotReached, assertTrue} from 'chrome://webui-test/chai_assert.js'; |
| import {middleOfNode, topLeftOfNode} from 'chrome://webui-test/mouse_mock_interactions.js'; |
| import {eventToPromise, microtasksFinished} from 'chrome://webui-test/test_util.js'; |
| |
| import {TestBookmarkManagerApiProxy} from './test_bookmark_manager_api_proxy.js'; |
| import {TestBookmarksBrowserProxy} from './test_browser_proxy.js'; |
| import {TestStore} from './test_store.js'; |
| import {TestTimerProxy} from './test_timer_proxy.js'; |
| import {createFolder, createItem, findFolderNode, getAllFoldersOpenState, normalizeIterable, replaceBody, testTree} from './test_util.js'; |
| |
| suite('drag and drop', function() { |
| let app: BookmarksAppElement; |
| let list: BookmarksListElement; |
| let rootFolderNode: BookmarksFolderNodeElement; |
| let store: TestStore; |
| let dndManager: DndManager; |
| let bookmarkManagerApi: TestBookmarkManagerApiProxy; |
| |
| enum DragStyle { |
| NONE = 0, |
| ON = 1, |
| ABOVE = 2, |
| BELOW = 3, |
| } |
| |
| function getFolderNode(id: string) { |
| return findFolderNode(rootFolderNode, id) as BookmarksFolderNodeElement; |
| } |
| |
| function getListItem(id: string) { |
| const items = list.shadowRoot.querySelectorAll('bookmarks-item'); |
| for (let i = 0; i < items.length; i++) { |
| if (items[i]!.itemId === id) { |
| return items[i] as BookmarksItemElement; |
| } |
| } |
| assertNotReached(); |
| } |
| |
| function dispatchDragEvent( |
| type: string, node: HTMLElement, xy?: {x: number, y: number}) { |
| xy = xy || middleOfNode(node); |
| const props = { |
| bubbles: true, |
| cancelable: true, |
| composed: true, |
| clientX: xy.x, |
| clientY: xy.y, |
| // Make this a primary input. |
| buttons: 1, |
| }; |
| const e = new DragEvent(type, props); |
| node.dispatchEvent(e); |
| } |
| |
| function bottomRightOfNode(target: HTMLElement) { |
| const rect = target.getBoundingClientRect(); |
| return {y: rect.top + rect.height, x: rect.left + rect.width}; |
| } |
| |
| function assertDragStyle(bookmarkElement: BookmarkElement, style: DragStyle) { |
| const dragStyles: {[key: string]: string} = {}; |
| dragStyles[DragStyle.ON] = 'drag-on'; |
| dragStyles[DragStyle.ABOVE] = 'drag-above'; |
| dragStyles[DragStyle.BELOW] = 'drag-below'; |
| |
| const classList = bookmarkElement.getDropTarget()!.classList; |
| Object.entries(dragStyles).forEach(([dragStyle, value]) => { |
| assertEquals( |
| dragStyle === style.toString(), classList.contains(value), |
| value + (dragStyle === style.toString() ? ' missing' : ' found') + |
| ' in classList ' + classList); |
| }); |
| } |
| |
| function createDragData(ids: string[], sameProfile: boolean = true) { |
| return { |
| elements: ids.map( |
| id => store.data.nodes[id] as chrome.bookmarks.BookmarkTreeNode), |
| sameProfile: sameProfile, |
| }; |
| } |
| |
| async function simulateDragStart(dragElement: HTMLElement) { |
| dispatchDragEvent('dragstart', dragElement); |
| const idList = await bookmarkManagerApi.whenCalled('startDrag'); |
| bookmarkManagerApi.resetResolver('startDrag'); |
| dndManager.getDragInfoForTesting()!.setNativeDragData( |
| createDragData(idList)); |
| move(dragElement, topLeftOfNode(dragElement)); |
| } |
| |
| function move(target: HTMLElement, dest?: {x: number, y: number}) { |
| dispatchDragEvent('dragover', target, dest || middleOfNode(target)); |
| } |
| |
| function getDragIds() { |
| return dndManager.getDragInfoForTesting()!.dragData!.elements.map( |
| (x) => x.id); |
| } |
| |
| setup(function() { |
| const nodes = testTree( |
| createFolder( |
| '1', |
| [ |
| createFolder( |
| '11', |
| [ |
| createFolder( |
| '111', |
| [ |
| createItem('1111'), |
| ]), |
| createFolder('112', []), |
| ]), |
| createItem('12'), |
| createItem('13'), |
| createFolder('14', []), |
| createFolder('15', []), |
| ]), |
| createFolder('2', [])); |
| store = new TestStore({ |
| nodes: nodes, |
| folderOpenState: getAllFoldersOpenState(nodes), |
| selectedFolder: '1', |
| }); |
| store.replaceSingleton(); |
| |
| bookmarkManagerApi = new TestBookmarkManagerApiProxy(); |
| BookmarkManagerApiProxyImpl.setInstance(bookmarkManagerApi); |
| |
| const testBrowserProxy = new TestBookmarksBrowserProxy(); |
| BrowserProxyImpl.setInstance(testBrowserProxy); |
| app = document.createElement('bookmarks-app'); |
| replaceBody(app); |
| list = |
| app.shadowRoot.querySelector<BookmarksListElement>('bookmarks-list')!; |
| rootFolderNode = app.shadowRoot.querySelector<BookmarksFolderNodeElement>( |
| 'bookmarks-folder-node')!; |
| dndManager = app.getDndManagerForTesting() as DndManager; |
| dndManager!.setTimerProxyForTesting(new TestTimerProxy()); |
| |
| // Wait for the API listener to call the browser proxy, since this |
| // indicates initialization is done. |
| return Promise |
| .all([ |
| testBrowserProxy.whenCalled('getIncognitoAvailability'), |
| eventToPromise('viewport-filled', list.$.list), |
| ]) |
| .then(() => { |
| return microtasksFinished(); |
| }); |
| }); |
| |
| test('dragInfo isDraggingFolderToDescendant', function() { |
| const dragInfo = new DragInfo(); |
| const nodes = store.data.nodes; |
| dragInfo.setNativeDragData(createDragData(['11'])); |
| assertTrue(dragInfo.isDraggingFolderToDescendant('111', nodes)); |
| assertFalse(dragInfo.isDraggingFolderToDescendant('1', nodes)); |
| assertFalse(dragInfo.isDraggingFolderToDescendant('2', nodes)); |
| |
| dragInfo.setNativeDragData(createDragData(['1'])); |
| assertTrue(dragInfo.isDraggingFolderToDescendant('14', nodes)); |
| assertTrue(dragInfo.isDraggingFolderToDescendant('111', nodes)); |
| assertFalse(dragInfo.isDraggingFolderToDescendant('2', nodes)); |
| }); |
| |
| test('drag in list', async function() { |
| const dragElement = getListItem('13'); |
| let dragTarget = getListItem('12'); |
| |
| await simulateDragStart(dragElement); |
| |
| assertDeepEquals(['13'], getDragIds()); |
| |
| // Bookmark items cannot be dragged onto other items. |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(document.body); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| |
| // Bookmark items can be dragged onto folders. |
| dragTarget = getListItem('11'); |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| |
| // There are no valid drop locations for dragging an item onto itself. |
| move(dragElement); |
| |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| assertDragStyle(dragElement, DragStyle.NONE); |
| }); |
| |
| test('reorder folder nodes', async function() { |
| const dragElement = getFolderNode('112'); |
| const dragTarget = getFolderNode('111'); |
| |
| await simulateDragStart(dragElement); |
| |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| }); |
| |
| test('drag an item into a sidebar folder', async function() { |
| const dragElement = getListItem('13'); |
| let dragTarget = getFolderNode('2'); |
| await simulateDragStart(dragElement); |
| |
| // Items can only be dragged onto sidebar folders, not above or below. |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| // Items cannot be dragged onto their parent folders. |
| dragTarget = getFolderNode('1'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| }); |
| |
| test('drag a folder into a descendant', async function() { |
| const dragElement = getFolderNode('11'); |
| const dragTarget = getFolderNode('112'); |
| |
| // Folders cannot be dragged into their descendants. |
| await simulateDragStart(dragElement); |
| |
| move(dragTarget); |
| |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| }); |
| |
| test('drag item into sidebar folder with descendants', async function() { |
| const dragElement = getFolderNode('15'); |
| const dragTarget = getFolderNode('11'); |
| |
| await simulateDragStart(dragElement); |
| |
| // Drags below an open folder are not allowed. |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| dispatchDragEvent('dragend', dragElement); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| |
| store.data.folderOpenState.set('11', false); |
| store.notifyObservers(); |
| |
| await simulateDragStart(dragElement); |
| |
| // Drags below a closed folder are allowed. |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| }); |
| |
| test('drag multiple list items', async function() { |
| // Dragging multiple items. |
| store.data.selection.items = new Set(['13', '15']); |
| let dragElement = getListItem('13'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['13', '15'], getDragIds()); |
| |
| // The dragged items should not be allowed to be dragged around any selected |
| // item. |
| let dragTarget = getListItem('13'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| |
| dragTarget = getListItem('14'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| dragTarget = getListItem('15'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| |
| dispatchDragEvent('dragend', dragElement); |
| |
| // Dragging an unselected item should only drag the unselected item. |
| dragElement = getListItem('14'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['14'], getDragIds()); |
| dispatchDragEvent('dragend', dragElement); |
| |
| // Dragging a folder node should only drag the node. |
| dragElement = getListItem('11'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['11'], getDragIds()); |
| }); |
| |
| test('drag multiple list items preserve displaying order', async function() { |
| // Dragging multiple items with different selection order. |
| store.data.selection.items = new Set(['15', '13']); |
| const dragElement = getListItem('13'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['13', '15'], getDragIds()); |
| }); |
| |
| test('bookmarks from different profiles', function() { |
| bookmarkManagerApi.onDragEnter.callListeners(createDragData(['11'], false)); |
| |
| // All positions should be allowed even with the same bookmark id if the |
| // drag element is from a different profile. |
| let dragTarget: BookmarkElement = getListItem('11'); |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| |
| // Folders from other profiles should be able to be dragged into |
| // descendants in this profile. |
| dragTarget = getFolderNode('112'); |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| }); |
| |
| test('drag from sidebar to list', async function() { |
| let dragElement: BookmarkElement = getFolderNode('112'); |
| let dragTarget = getListItem('13'); |
| |
| // Drag a folder onto the list. |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['112'], getDragIds()); |
| |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| dispatchDragEvent('dragend', dragTarget); |
| |
| // Folders should not be able to dragged onto themselves in the list. |
| bookmarkManagerApi.onDragEnter.callListeners(createDragData(['11'])); |
| dragElement = getListItem('11'); |
| move(dragElement); |
| assertDragStyle(dragElement, DragStyle.NONE); |
| |
| // Ancestors should not be able to be dragged onto descendant |
| // displayed lists. |
| store.data.selectedFolder = '111'; |
| store.notifyObservers(); |
| await eventToPromise('viewport-filled', list.$.list); |
| |
| bookmarkManagerApi.onDragEnter.callListeners(createDragData(['11'])); |
| dragTarget = getListItem('1111'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| }); |
| |
| test('drags with search', function() { |
| store.data.search.term = 'Asgore'; |
| store.data.search.results = ['11', '13', '2']; |
| store.data.selectedFolder = ''; |
| store.notifyObservers(); |
| |
| // Search results should not be able to be dragged onto, but can be dragged |
| // from. |
| bookmarkManagerApi.onDragEnter.callListeners(createDragData(['2'])); |
| let dragTarget: BookmarkElement = getListItem('13'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| |
| // Drags onto folders should work as per usual. |
| dragTarget = getFolderNode('112'); |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| move(dragTarget, middleOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| }); |
| |
| // This is a regression test for https://crbug.com/41465126. |
| test( |
| 'drag bookmark that is not in selected folder but in search result', |
| async function() { |
| store.data.search.term = 'Asgore'; |
| store.data.search.results = ['11', '13', '2']; |
| store.data.selectedFolder = ''; |
| store.notifyObservers(); |
| |
| await simulateDragStart(getListItem('13')); |
| |
| assertDeepEquals(['13'], getDragIds()); |
| }); |
| |
| test('simple native drop end to end', async function() { |
| const dragElement = getListItem('13'); |
| const dragTarget = getListItem('12'); |
| |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['13'], getDragIds()); |
| |
| dispatchDragEvent('dragover', dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| |
| setDebouncerForTesting(); |
| dispatchDragEvent('drop', dragTarget); |
| |
| const [dropParentId, dropIndex] = |
| await bookmarkManagerApi.whenCalled('drop'); |
| |
| assertEquals('1', dropParentId); |
| assertEquals(1, dropIndex); |
| |
| dispatchDragEvent('dragend', dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| }); |
| |
| test('auto expander', async function() { |
| overrideFolderOpenerTimeoutDelay(0); |
| store.setReducersEnabled(true); |
| |
| store.data.folderOpenState.set('11', false); |
| store.data.folderOpenState.set('14', false); |
| store.data.folderOpenState.set('15', false); |
| store.notifyObservers(); |
| await microtasksFinished(); |
| |
| const dragElement = getFolderNode('15'); |
| await simulateDragStart(dragElement); |
| |
| // Dragging onto folders without children doesn't open the folder. |
| let dragTarget = getFolderNode('14'); |
| move(dragTarget); |
| await microtasksFinished(); |
| assertFalse(dragTarget.isOpen); |
| |
| // Dragging onto itself doesn't open the folder. |
| move(dragElement); |
| await microtasksFinished(); |
| assertFalse(dragElement.isOpen); |
| |
| // Dragging onto an open folder doesn't affect the folder. |
| dragTarget = getFolderNode('1'); |
| assertTrue(dragTarget.isOpen); |
| move(dragTarget); |
| await microtasksFinished(); |
| assertTrue(dragTarget.isOpen); |
| |
| dragTarget = getFolderNode('11'); |
| |
| // Dragging off of a closed folder doesn't open it. |
| move(dragTarget); |
| move(list); |
| await microtasksFinished(); |
| assertFalse(dragTarget.isOpen); |
| |
| // Dragging onto a folder with DragStyle.BELOW doesn't open it. |
| move(dragTarget, bottomRightOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.BELOW); |
| await microtasksFinished(); |
| assertFalse(dragTarget.isOpen); |
| |
| // Dragging onto a folder with DragStyle.ABOVE doesn't open it. |
| move(dragTarget, topLeftOfNode(dragTarget)); |
| assertDragStyle(dragTarget, DragStyle.ABOVE); |
| await microtasksFinished(); |
| assertFalse(dragTarget.isOpen); |
| |
| // Dragging onto a closed folder with children opens it. |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.ON); |
| await microtasksFinished(); |
| assertTrue(dragTarget.isOpen); |
| }); |
| |
| test('drag item selects/deselects items', async function() { |
| store.setReducersEnabled(true); |
| |
| store.data.selection.items = new Set(['13', '15']); |
| store.notifyObservers(); |
| |
| // Dragging an item not in the selection selects the dragged item and |
| // deselects the previous selection. |
| let dragElement: BookmarkElement = getListItem('14'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals(['14'], normalizeIterable(store.data.selection.items)); |
| dispatchDragEvent('dragend', dragElement); |
| |
| // Dragging a folder node deselects any selected items in the bookmark list. |
| dragElement = getFolderNode('15'); |
| await simulateDragStart(dragElement); |
| assertDeepEquals([], normalizeIterable(store.data.selection.items)); |
| dispatchDragEvent('dragend', dragElement); |
| }); |
| |
| test('cannot drag items when editing is disabled', function() { |
| store.data.prefs.canEdit = false; |
| store.notifyObservers(); |
| |
| const dragElement = getFolderNode('11'); |
| |
| dispatchDragEvent('dragstart', dragElement); |
| assertFalse(dndManager.getDragInfoForTesting()!.isDragValid()); |
| }); |
| |
| test('cannot start dragging unmodifiable items', function() { |
| store.data.nodes['2']!.unmodifiable = 'managed'; |
| store.notifyObservers(); |
| |
| let dragElement = getFolderNode('1'); |
| dispatchDragEvent('dragstart', dragElement); |
| assertFalse(dndManager.getDragInfoForTesting()!.isDragValid()); |
| |
| dragElement = getFolderNode('2'); |
| dispatchDragEvent('dragstart', dragElement); |
| assertFalse(dndManager.getDragInfoForTesting()!.isDragValid()); |
| }); |
| |
| test('cannot drag onto folders with unmodifiable children', async function() { |
| store.data.nodes['2']!.unmodifiable = 'managed'; |
| store.notifyObservers(); |
| |
| const dragElement = getListItem('12'); |
| await simulateDragStart(dragElement); |
| |
| // Can't drag onto the unmodifiable node. |
| const dragTarget = getFolderNode('2'); |
| move(dragTarget); |
| assertDragStyle(dragTarget, DragStyle.NONE); |
| }); |
| }); |