diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/base/test/browser/browser_threadTreeDeleting.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/mail/base/test/browser/browser_threadTreeDeleting.js | 572 |
1 files changed, 572 insertions, 0 deletions
diff --git a/comm/mail/base/test/browser/browser_threadTreeDeleting.js b/comm/mail/base/test/browser/browser_threadTreeDeleting.js new file mode 100644 index 0000000000..e034fbbfb3 --- /dev/null +++ b/comm/mail/base/test/browser/browser_threadTreeDeleting.js @@ -0,0 +1,572 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +requestLongerTimeout(2); + +const { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +let tabmail = document.getElementById("tabmail"); +let about3Pane = tabmail.currentAbout3Pane; +let threadTree = about3Pane.threadTree; +// Not `currentAboutMessage` as (a) that's null right now, and (b) we'll be +// testing things that happen when about:message is hidden. +let aboutMessage = about3Pane.messageBrowser.contentWindow; +let messagePaneBrowser = aboutMessage.getMessagePaneBrowser(); +let multiMessageView = about3Pane.multiMessageBrowser.contentWindow; +let generator = new MessageGenerator(); +let rootFolder, sourceMessageIDs; + +add_setup(async function () { + MailServices.accounts.createLocalMailAccount(); + let account = MailServices.accounts.accounts[0]; + account.addIdentity(MailServices.accounts.createIdentity()); + rootFolder = account.incomingServer.rootFolder.QueryInterface( + Ci.nsIMsgLocalMailFolder + ); + + registerCleanupFunction(() => { + MailServices.accounts.removeAccount(account, false); + }); +}); + +/** Test a real folder, unthreaded. */ +add_task(async function testUnthreaded() { + let folderA = rootFolder + .createLocalSubfolder("threadTreeDeletingA") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderA.addMessageBatch( + generator.makeMessages({ count: 15 }).map(message => message.toMboxString()) + ); + + sourceMessageIDs = Array.from(folderA.messages, m => m.messageId); + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: folderA.URI, + }); + await ensure_cards_view(); + goDoCommand("cmd_sort", { target: { value: "unthreaded" } }); + + await subtest(); +}); + +/** Test a real folder with threads. */ +add_task(async function testThreaded() { + let folderB = rootFolder + .createLocalSubfolder("threadTreeDeletingB") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderB.addMessageBatch( + [ + // No real reason for the values here other than the total count. + ...generator.makeMessages({ count: 4 }), + ...generator.makeMessages({ count: 6, msgsPerThread: 3 }), + ...generator.makeMessages({ count: 1 }), + ...generator.makeMessages({ count: 2, msgsPerThread: 2 }), + ...generator.makeMessages({ count: 2 }), + ].map(message => message.toMboxString()) + ); + + sourceMessageIDs = Array.from(folderB.messages, m => m.messageId); + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: folderB.URI, + }); + goDoCommand("cmd_sort", { target: { value: "threaded" } }); + goDoCommand("cmd_expandAllThreads"); + + await subtest(); +}); + +/** Test a virtual folder with a single backing folder. */ +add_task(async function testSingleVirtual() { + let folderC = rootFolder + .createLocalSubfolder("threadTreeDeletingC") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderC.addMessageBatch( + generator.makeMessages({ count: 15 }).map(message => message.toMboxString()) + ); + + let virtualFolderC = rootFolder.createLocalSubfolder( + "threadTreeDeletingVirtualC" + ); + virtualFolderC.setFlag(Ci.nsMsgFolderFlags.Virtual); + let folderInfoC = virtualFolderC.msgDatabase.dBFolderInfo; + // Search for something instead of all messages, as the "ALL" search could + // detected and the backing folder displayed instead, defeating the point of + // this test. + folderInfoC.setCharProperty("searchStr", "AND (date,is after,31-Dec-1999)"); + folderInfoC.setCharProperty("searchFolderUri", folderC.URI); + + sourceMessageIDs = Array.from(folderC.messages, m => m.messageId); + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: virtualFolderC.URI, + }); + + await subtest(); +}); + +/** Test a virtual folder with multiple backing folders. */ +add_task(async function testXFVirtual() { + let folderD = rootFolder + .createLocalSubfolder("threadTreeDeletingD") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderD.addMessageBatch( + generator.makeMessages({ count: 4 }).map(message => message.toMboxString()) + ); + + let folderE = rootFolder + .createLocalSubfolder("threadTreeDeletingE") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderE.addMessageBatch( + generator + .makeMessages({ count: 11, msgsPerThread: 3 }) + .map(message => message.toMboxString()) + ); + + let virtualFolderDE = rootFolder.createLocalSubfolder( + "threadTreeDeletingVirtualDE" + ); + virtualFolderDE.setFlag(Ci.nsMsgFolderFlags.Virtual); + let folderInfoY = virtualFolderDE.msgDatabase.dBFolderInfo; + folderInfoY.setCharProperty("searchStr", "AND (date,is after,31-Dec-1999)"); + folderInfoY.setCharProperty( + "searchFolderUri", + `${folderD.URI}|${folderE.URI}` + ); + + sourceMessageIDs = [ + ...Array.from(folderD.messages, m => m.messageId), + ...Array.from(folderE.messages, m => m.messageId), + ]; + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: virtualFolderDE.URI, + }); + goDoCommand("cmd_sort", { target: { value: "threaded" } }); + goDoCommand("cmd_expandAllThreads"); + + await subtest(); +}); + +/** Test a real folder with a quick filter applied. */ +add_task(async function testQuickFiltered() { + let folderF = rootFolder + .createLocalSubfolder("threadTreeDeletingF") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderF.addMessageBatch( + generator.makeMessages({ count: 30 }).map(message => message.toMboxString()) + ); + let flaggedMessages = []; + let i = 0; + for (let message of folderF.messages) { + if (i++ % 2) { + flaggedMessages.push(message); + } + } + folderF.markMessagesFlagged(flaggedMessages, true); + + sourceMessageIDs = flaggedMessages.map(m => m.messageId); + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: folderF.URI, + }); + let filterer = about3Pane.quickFilterBar.filterer; + filterer.clear(); + filterer.visible = true; + filterer.setFilterValue("starred", true); + about3Pane.quickFilterBar.updateSearch(); + + await subtest(); +}); + +/** Test a folder sorted by date descending. */ +add_task(async function testSortDescending() { + let folderG = rootFolder + .createLocalSubfolder("threadTreeDeletingG") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderG.addMessageBatch( + generator.makeMessages({ count: 15 }).map(message => message.toMboxString()) + ); + + sourceMessageIDs = Array.from(folderG.messages, m => m.messageId).reverse(); + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: folderG.URI, + }); + goDoCommand("cmd_sort", { target: { value: "descending" } }); + + await subtest(); +}); + +/** Test a folder sorted by subject. */ +add_task(async function testSortBySubject() { + let folderH = rootFolder + .createLocalSubfolder("threadTreeDeletingH") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderH.addMessageBatch( + generator.makeMessages({ count: 15 }).map(message => message.toMboxString()) + ); + + sourceMessageIDs = Array.from(folderH.messages) + .sort((m1, m2) => (m1.subject < m2.subject ? -1 : 1)) + .map(m => m.messageId); + + about3Pane.restoreState({ + messagePaneVisible: true, + folderURI: folderH.URI, + }); + goDoCommand("cmd_sort", { target: { value: "bySubject" } }); + + await subtest(); +}); + +/** + * Tests that deleting the selected row while smooth-scrolling does not break + * the scrolling and leave the tree in a bad scroll position. + */ +add_task(async function testDeletionWhileScrolling() { + let folderI = rootFolder + .createLocalSubfolder("threadTreeDeletingI") + .QueryInterface(Ci.nsIMsgLocalMailFolder); + folderI.addMessageBatch( + generator + .makeMessages({ count: 500 }) + .map(message => message.toMboxString()) + ); + + await ensure_table_view(); + about3Pane.restoreState({ + messagePaneVisible: false, + folderURI: folderI.URI, + }); + + const scrollListener = { + async promiseScrollingStopped() { + this.lastTime = Date.now(); + await TestUtils.waitForCondition( + () => Date.now() - this.lastTime > 1000, + "waiting for scrolling to stop" + ); + delete this.direction; + delete this.lastPosition; + }, + setScrollExpectation(direction) { + this.direction = direction; + this.lastPosition = threadTree.scrollTop; + }, + setNoScrollExpectation() { + this.direction = 0; + }, + handleEvent(event) { + if (this.direction === 0) { + Assert.report(true, undefined, undefined, "unexpected scroll event"); + return; + } + + const position = threadTree.scrollTop; + if (this.direction == -1) { + Assert.lessOrEqual(position, this.lastPosition); + } else if (this.direction == 1) { + Assert.greaterOrEqual(position, this.lastPosition); + } + this.lastPosition = position; + this.lastTime = Date.now(); + }, + }; + + async function delayThenPress(millis, key) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, millis)); + if (key) { + EventUtils.synthesizeKey(key, {}, about3Pane); + await TestUtils.waitForTick(); + } + } + + threadTree.addEventListener("scroll", scrollListener); + threadTree.table.body.focus(); + threadTree.selectedIndex = 299; + await scrollListener.promiseScrollingStopped(); + + let stopPromise = scrollListener.promiseScrollingStopped(); + scrollListener.setScrollExpectation(-1); + + // Page up a few times then delete some messages. + + await delayThenPress(0, "VK_PAGE_UP"); + await delayThenPress(60, "VK_PAGE_UP"); + await delayThenPress(60, "VK_PAGE_UP"); + await delayThenPress(400, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + + await stopPromise; + Assert.equal( + threadTree.getFirstVisibleIndex(), + threadTree.selectedIndex, + "selected row should be the first visible row" + ); + + // Page down a few times then delete some messages. + + stopPromise = scrollListener.promiseScrollingStopped(); + scrollListener.setScrollExpectation(1); + + await delayThenPress(60, "VK_PAGE_DOWN"); + await delayThenPress(60, "VK_PAGE_DOWN"); + await delayThenPress(60, "VK_PAGE_DOWN"); + await delayThenPress(300, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + + await stopPromise; + Assert.equal( + threadTree.getLastVisibleIndex(), + threadTree.selectedIndex, + "selected row should be the last visible row" + ); + + // Select a message somewhere in the middle then delete it. + + scrollListener.setNoScrollExpectation(); + threadTree.selectedIndex -= 10; + await delayThenPress(80, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + await delayThenPress(80, "VK_DELETE"); + + await delayThenPress(1000); + Assert.less( + threadTree.getFirstVisibleIndex(), + threadTree.selectedIndex, + "selected row should be below the first visible row" + ); + Assert.greater( + threadTree.getLastVisibleIndex(), + threadTree.selectedIndex, + "selected row should be above the last visible row" + ); + + threadTree.removeEventListener("scroll", scrollListener); +}); + +async function subtest() { + await TestUtils.waitForCondition( + () => threadTree.table.body.rows.length == 15, + "waiting for all of the table rows" + ); + + let dbView = about3Pane.gDBView; + let subjects = []; + for (let i = 0; i < 15; i++) { + subjects.push(dbView.cellTextForColumn(i, "subjectCol")); + } + verifySubjects(subjects); + + threadTree.table.body.focus(); + threadTree.selectedIndex = 3; + await messageLoaded(3); + + // Delete a single message. + await doDeleteCommand(4); + await verifySelection(14, [3], 3); + verifySubjects([subjects[0], subjects[1], subjects[2], ...subjects.slice(4)]); + + // Delete a single message. + await doDeleteCommand(5); + await verifySelection(13, [3], 3); + verifySubjects([subjects[0], subjects[1], subjects[2], ...subjects.slice(5)]); + + // Delete a single message by clicking the about:message Delete button. + await doDeleteClick(6); + await verifySelection(12, [3], 3); + verifySubjects([subjects[0], subjects[1], subjects[2], ...subjects.slice(6)]); + + // Delete adjacent messages. + threadTree.selectedIndices = [3, 4, 5]; + threadTree.currentIndex = 6; + await doDeleteCommand(9); + await verifySelection(9, [3], 3); + verifySubjects([subjects[0], subjects[1], subjects[2], ...subjects.slice(9)]); + + // Delete non-adjacent messages. + threadTree.selectedIndices = [2, 4]; + threadTree.currentIndex = 4; + // We should select the message below the current index, but we select the + // message below the first selected one. + await doDeleteCommand(9); + await verifySelection(7, [2], 2); + verifySubjects([ + subjects[0], + subjects[1], + subjects[9], + ...subjects.slice(11), + ]); + + // Delete the last message. + threadTree.selectedIndex = 6; + await messageLoaded(14); + await doDeleteCommand(13); + await verifySelection(6, [5], 5); + verifySubjects([ + subjects[0], + subjects[1], + subjects[9], + ...subjects.slice(11, 14), + ]); + + // Now cause a delete to happen from outside the UI. + await doDeleteExternal(1); + await verifySelection(5, [4], 4); + verifySubjects([subjects[0], subjects[9], ...subjects.slice(11, 14)]); + + // Delete the selected message from outside the UI. + threadTree.selectedIndex = 2; + await messageLoaded(11); + await doDeleteExternal(2, 12); + await verifySelection(4, [2], 2); + verifySubjects([subjects[0], subjects[9], ...subjects.slice(12, 14)]); +} + +async function messageLoaded(index) { + await BrowserTestUtils.browserLoaded(messagePaneBrowser); + Assert.equal( + aboutMessage.gMessage.messageId, + sourceMessageIDs[index], + "correct message loaded" + ); +} + +async function _doDelete(callback, index, expectedLoad) { + let selectCount = 0; + let onSelect = () => selectCount++; + threadTree.addEventListener("select", onSelect); + + let selectPromise; + if (expectedLoad !== undefined) { + selectPromise = BrowserTestUtils.waitForEvent(threadTree, "select"); + } + await callback(); + if (selectPromise) { + await selectPromise; + await messageLoaded(expectedLoad); + } + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 25)); + if (selectPromise) { + Assert.equal(selectCount, 1, "'select' event should've happened only once"); + } else { + Assert.equal(selectCount, 0, "'select' event should not have happened"); + } + + threadTree.removeEventListener("select", onSelect); +} + +async function doDeleteCommand(expectedLoad) { + await _doDelete( + function () { + goDoCommand("cmd_delete"); + }, + undefined, + expectedLoad + ); +} + +async function doDeleteClick(expectedLoad) { + await _doDelete( + function () { + let messageView = + threadTree.selectedIndices.length == 1 + ? aboutMessage + : multiMessageView; + EventUtils.synthesizeMouseAtCenter( + messageView.document.getElementById("hdrTrashButton"), + {}, + messageView + ); + }, + undefined, + expectedLoad + ); +} + +async function doDeleteExternal(index, expectedLoad) { + await _doDelete( + function () { + let message = about3Pane.gDBView.getMsgHdrAt(index); + message.folder.deleteMessages( + [message], // messages + null, // msgWindow + true, // deleteStorage + false, // isMove + null, // listener + false // canUndo + ); + }, + index, + expectedLoad + ); +} + +async function verifySelection(rowCount, selectedIndices, currentIndex) { + Assert.equal(threadTree.view.rowCount, rowCount, "row count of view"); + await TestUtils.waitForCondition( + () => threadTree.table.body.rows.length == rowCount, + "waiting table row count to match the view's row count" + ); + + Assert.deepEqual( + threadTree.selectedIndices, + selectedIndices, + "table's selected indices" + ); + let selectedRows = Array.from(threadTree.querySelectorAll(".selected")); + Assert.equal( + selectedRows.length, + selectedIndices.length, + "number of rows with .selected class" + ); + for (let index of selectedIndices) { + let row = threadTree.getRowAtIndex(index); + Assert.ok( + selectedRows.includes(row), + `.selected row at ${index} is expected` + ); + } + + Assert.equal(threadTree.currentIndex, currentIndex, "table's current index"); + let currentRows = threadTree.querySelectorAll(".current"); + Assert.equal(currentRows.length, 1, "one row should have .current"); + Assert.equal( + currentRows[0], + threadTree.getRowAtIndex(currentIndex), + `.current row at ${currentIndex} is expected` + ); + + let contextTargetRows = threadTree.querySelectorAll(".context-menu-target"); + Assert.equal( + contextTargetRows.length, + 0, + "no rows should have .context-menu-target" + ); +} + +function verifySubjects(expectedSubjects) { + let actualSubjects = Array.from( + threadTree.table.body.rows, + row => + row.querySelector(".thread-card-subject-container > .subject").textContent + ); + Assert.equal(actualSubjects.length, expectedSubjects.length, "row count"); + for (let i = 0; i < expectedSubjects.length; i++) { + Assert.equal(actualSubjects[i], expectedSubjects[i], `subject at ${i}`); + } +} |