diff --git a/app/index.html b/app/index.html index 2bafb81..a8310a5 100644 --- a/app/index.html +++ b/app/index.html @@ -31,7 +31,13 @@
- +
diff --git a/app/lib/search/searchMain.js b/app/lib/search/searchMain.js index 1c058b1..fbe832e 100644 --- a/app/lib/search/searchMain.js +++ b/app/lib/search/searchMain.js @@ -1,15 +1,24 @@ const ipc = require("../ipc/ipcMain") const menu = require("../main/menu") +const shared = require("./searchShared") + +const FIND_MENU_ID = "find" const FIND_NEXT_MENU_ID = "find-next" const FIND_PREVIOUS_MENU_ID = "find-previous" let _mainMenu +exports.FIND_MENU_ID = FIND_MENU_ID + exports.FIND_NEXT_MENU_ID = FIND_NEXT_MENU_ID exports.FIND_PREVIOUS_MENU_ID = FIND_PREVIOUS_MENU_ID +exports.SEARCH_RESULT_CLASS = shared.SEARCH_RESULT_CLASS + +exports.SELECTED_SEARCH_RESULT_ID = shared.SELECTED_SEARCH_RESULT_ID + exports.init = mainMenu => { _mainMenu = mainMenu diff --git a/app/lib/search/searchRenderer.js b/app/lib/search/searchRenderer.js index 416f952..fc3a33e 100644 --- a/app/lib/search/searchRenderer.js +++ b/app/lib/search/searchRenderer.js @@ -1,11 +1,11 @@ const ipc = require("../ipc/ipcRenderer") const renderer = require("../renderer/common") -const SEARCH_RESULT_CLASS = "search-result" -const SELECTED_SEARCH_RESULT_ID = "selected-search-result" +const shared = require("./searchShared") + const CANCEL_VALUE = "search-dialog-cancel" -const RESULT_START_TAG = `` +const RESULT_START_TAG = `` const END_TAG = "" let _document @@ -130,12 +130,12 @@ exports.highlightTerm = () => { termRegex, RESULT_START_TAG + _term + END_TAG, ) - const searchResultElements = _document.getElementsByClassName(SEARCH_RESULT_CLASS) + const searchResultElements = _document.getElementsByClassName(shared.SEARCH_RESULT_CLASS) _searchResultCount = searchResultElements.length for (let i = 0; i < _searchResultCount; i++) { const searchResult = searchResultElements[i] if (i === _searchIndex) { - searchResult.setAttribute("id", SELECTED_SEARCH_RESULT_ID) + searchResult.setAttribute("id", shared.SELECTED_SEARCH_RESULT_ID) } else { searchResult.removeAttribute("id") } @@ -147,7 +147,7 @@ exports.scrollToResult = () => { return } - const resultElement = _document.getElementById(SELECTED_SEARCH_RESULT_ID) + const resultElement = _document.getElementById(shared.SELECTED_SEARCH_RESULT_ID) const resultElementPosition = renderer.elementYPosition(resultElement) const contentElement = renderer.contentElement() @@ -166,9 +166,9 @@ exports.deactivate = deactivate // For testing -exports.SEARCH_RESULT_CLASS = SEARCH_RESULT_CLASS +exports.SEARCH_RESULT_CLASS = shared.SEARCH_RESULT_CLASS -exports.SELECTED_SEARCH_RESULT_ID = SELECTED_SEARCH_RESULT_ID +exports.SELECTED_SEARCH_RESULT_ID = shared.SELECTED_SEARCH_RESULT_ID exports.CANCEL_VALUE = CANCEL_VALUE diff --git a/app/lib/search/searchShared.js b/app/lib/search/searchShared.js new file mode 100644 index 0000000..facd5be --- /dev/null +++ b/app/lib/search/searchShared.js @@ -0,0 +1,3 @@ +exports.SEARCH_RESULT_CLASS = "search-result" + +exports.SELECTED_SEARCH_RESULT_ID = "selected-search-result" diff --git a/app/main.js b/app/main.js index aab8376..943cc1a 100644 --- a/app/main.js +++ b/app/main.js @@ -218,6 +218,7 @@ function createMainMenu() { { label: "&Find...", accelerator: "CmdOrCtrl+F", + id: search.FIND_MENU_ID, click() { search.start() }, diff --git a/test/integrationSpec.js b/test/integrationSpec.js index 3012d9d..7a057ce 100644 --- a/test/integrationSpec.js +++ b/test/integrationSpec.js @@ -85,9 +85,9 @@ function hasUnblockedContentMessage() { } async function elementIsHidden(page, elementPath) { - const locator = await page.locator(elementPath) + const locator = page.locator(elementPath) await locator.waitFor({ state: "hidden" }) - return locator.isHidden() + return await locator.isHidden() } describe("Integration tests with single app instance", () => { @@ -404,6 +404,73 @@ describe("Integration tests with their own app instance each", () => { }) }) + describe("Search dialog", () => { + const search = require("../app/lib/search/searchMain") + + const searchResultClass = `class="${search.SEARCH_RESULT_CLASS}"` + const selectedSearchResultId = `id="${search.SELECTED_SEARCH_RESULT_ID}"` + + async function assertDialogIsClosed() { + assert.isTrue(await elementIsHidden(page, mocking.elements.searchDialog.path)) + } + + async function opendDialog() { + await clickMenuItem(app, search.FIND_MENU_ID) + assert.isTrue(await page.locator(mocking.elements.searchDialog.path).isVisible()) + } + + async function confirmDialog() { + await page.locator(mocking.elements.searchDialog.okButton.path).click() + await assertDialogIsClosed() + } + + async function getContent() { + return await page.locator(mocking.elements.content.path).innerHTML() + } + + async function enterSearchTerm(term) { + await page.locator(mocking.elements.searchDialog.inputField.path).fill(term) + } + + it("can be canceled", async () => { + await opendDialog() + await page.locator(mocking.elements.searchDialog.cancelButton.path).click() + await assertDialogIsClosed() + }) + + it("changes nothing after confirming without input", async () => { + await opendDialog() + await confirmDialog() + + const content = await getContent() + assert.notInclude(content, searchResultClass) + assert.notInclude(content, selectedSearchResultId) + }) + + it("highlights and scrolls to search term", async () => { + // It would be cleaner, if this were two tests, but every integration test takes + // quite some time. + + const contentLocator = page.locator( + `${mocking.elements.content.path} > p:first-of-type`, + ) + const orig = await contentLocator.boundingBox() + + await opendDialog() + await enterSearchTerm("multi markdown table") + await confirmDialog() + await page.locator(`#${search.SELECTED_SEARCH_RESULT_ID}`).waitFor() + + const content = await getContent() + assert.include(content, searchResultClass) + assert.include(content, selectedSearchResultId) + + const changed = await contentLocator.boundingBox() + assert.strictEqual(changed.x, orig.x) + assert.notStrictEqual(changed.y, orig.y) + }) + }) + describe("Keyboard handling", () => { it("has focus on content", async () => { const contentLocator = page.locator( diff --git a/test/mocking.js b/test/mocking.js index b1b8669..f1a17a7 100644 --- a/test/mocking.js +++ b/test/mocking.js @@ -381,6 +381,18 @@ exports.elements = { rawText: { path: "#raw-text", }, + searchDialog: { + path: "#search-dialog", + inputField: { + path: "#search-input", + }, + okButton: { + path: "#search-ok-button", + }, + cancelButton: { + path: "#search-cancel-button", + }, + }, } exports.mainWindow = {