diff --git a/app/lib/contentBlocking/contentBlockingRenderer.js b/app/lib/contentBlocking/contentBlockingRenderer.js index dccbbc7..5058d61 100644 --- a/app/lib/contentBlocking/contentBlockingRenderer.js +++ b/app/lib/contentBlocking/contentBlockingRenderer.js @@ -72,6 +72,8 @@ function unblockURL(url) { changeInfoElementVisiblity(false) electron.ipcRenderer.send(ipc.messages.allContentUnblocked) } + + console.log(`Unblocked: ${url}`) } function unblockAll() { diff --git a/app/main.js b/app/main.js index 005daec..796bf85 100644 --- a/app/main.js +++ b/app/main.js @@ -309,6 +309,7 @@ function createWindow() { { type: "separator" }, { label: "Switch &Theme", + id: "switch-theme", click() { _applicationSettings.theme = electron.nativeTheme.shouldUseDarkColors ? _applicationSettings.LIGHT_THEME diff --git a/package.json b/package.json index 04cde29..52e89c3 100644 --- a/package.json +++ b/package.json @@ -89,14 +89,12 @@ }, "devDependencies": { "chai": "4.3.4", - "chai-as-promised": "7.1.1", "electron": "16.0.1", "electron-builder": "22.14.5", - "lodash": "4.17.21", + "lodash.clonedeep": "4.5.0", "mocha": "9.1.3", + "playwright": "1.16.3", "prettier": "2.4.1", - "spectron": "15.0.0", - "spectron-menu-addon-v2": "1.0.1", "tslib": "2.3.1" } } diff --git a/test/integrationSpec.js b/test/integrationSpec.js index f9d1932..683547b 100644 --- a/test/integrationSpec.js +++ b/test/integrationSpec.js @@ -1,93 +1,88 @@ +const fs = require("fs/promises") const path = require("path") -const chai = require("chai") -const chaiAsPromised = require("chai-as-promised") -const electron = require("electron") -const menuAddon = require("spectron-menu-addon-v2").default +const assert = require("chai").assert +const electronPath = require("electron") +const playwright = require("playwright") const mocking = require("./mocking") -const assert = chai.assert +const electron = playwright._electron const defaultDocumentFile = "testfile_utf8.md" const defaultDocumentPath = path.join(__dirname, "documents", defaultDocumentFile) let app -let client +let page -// Based on https://stackoverflow.com/a/39914235/13949398 (What is the JavaScript version of sleep()?) -function sleep(ms) { - // console.debug(`sleep ${ms} ${process.hrtime()}`) // For debugging - return new Promise(resolve => setTimeout(resolve, ms)) +const consoleMessages = [] + +function clearMessages() { + consoleMessages.length = 0 +} + +function addMessage(msg) { + consoleMessages.push(msg) } async function startApp(documentPath) { - const app = menuAddon.createApplication({ - path: electron, - args: [path.join(__dirname, ".."), documentPath, "--test"], + clearMessages() + await fs.rm(mocking.dataDir, { force: true, recursive: true }) + + const app = await electron.launch({ + args: [path.join(__dirname, ".."), documentPath, "--test", mocking.dataDir], + executablePath: electronPath, }) - chaiAsPromised.transferPromiseness = app.transferPromiseness - return app.start() -} -async function stopApp(app) { - if (app && app.isRunning()) { - await app.stop() - } -} + const page = await app.firstWindow() + page.on("console", msg => addMessage(msg.text())) + page.on("crash", () => assert.fail("Crash happened")) + page.on("pageerror", error => assert.fail(`Page error: ${error}`)) + page.setDefaultTimeout(2000) + await page.waitForSelector("div") // Wait until the window is actually loaded -async function wait(predicate, tries, timeout) { - tries = tries || 10 - timeout = timeout || 100 - for (let i = 0; i < tries; i++) { - if (await predicate()) { - return true - } - await sleep(timeout) - } - return false + return [app, page] } -async function containsConsoleMessage(message) { - let hasFoundMessage = false - ;(await client.getMainProcessLogs()).forEach(log => { - if (log.toLowerCase().includes(message)) { - hasFoundMessage = true - } - }) - return hasFoundMessage +async function clickMenuItemById(app, id) { + app.evaluate( + ({ Menu }, menuId) => Menu.getApplicationMenu().getMenuItemById(menuId).click(), + id + ) } -async function checkUnblockedMessage() { - return await containsConsoleMessage("unblocked") +function containsConsoleMessage(message) { + return !!consoleMessages.find(msg => msg.toLowerCase().includes(message)) } -async function elementIsVisible(element) { - return (await element.getCSSProperty("display")).value !== "none" +function hasUnblockedContentMessage() { + return containsConsoleMessage("unblocked") } -global.before(() => chai.use(chaiAsPromised)) +async function elementIsHidden(page, elementPath) { + return ( + (await page.waitForSelector(elementPath, { + state: "hidden", + })) === null + ) +} describe("Integration tests with single app instance", () => { - before(async () => { - app = await startApp(defaultDocumentPath) - client = app.client - }) + before(async () => ([app, page] = await startApp(defaultDocumentPath))) - after(async () => await stopApp(app)) + after(async () => await app.close()) - it("opens a window", async () => { - client.waitUntilWindowLoaded() - await assert.eventually.equal(client.getWindowCount(), 1) + it("opens a window", () => { + assert.exists(page) }) it("has file name in title bar", async () => { - await assert.eventually.include(client.getTitle(), defaultDocumentFile) + assert.include(await page.title(), defaultDocumentFile) }) it("displays blocked content banner", async () => { - const elem = await client.$(mocking.elements.blockedContentArea.path) - assert.equal(await elem.getAttribute("hidden"), null) + const elem = await page.$(mocking.elements.blockedContentArea.path) + assert.isTrue(await elem.isVisible()) }) describe('Library "storage"', () => { @@ -155,18 +150,33 @@ describe("Integration tests with single app instance", () => { }) describe("Main menu", () => { + async function searchMenuItem(menuItemPath) { + return await app.evaluate(({ Menu }, itemPath) => { + let menu = Menu.getApplicationMenu() + let item + for (const label of itemPath) { + item = menu.items.find(item => item.label === label) + menu = item.submenu + } + return { + label: item.label, // For debugging + enabled: item.enabled, + } + }, menuItemPath) + } + function assertMenu(menu, itemPath) { for (const [_, currentItem] of Object.entries(menu)) { const currentItemLabel = currentItem.label const currentItemPath = [...itemPath, currentItemLabel] describe(`Menu item "${currentItemLabel}"`, () => { it("exists", async () => { - assert.notEqual((await menuAddon.getMenuItem(...currentItemPath)).label, "") + assert.exists(await searchMenuItem(currentItemPath)) }) it(`is ${currentItem.isEnabled ? "enabled" : "disabled"}`, async () => { assert.equal( - (await menuAddon.getMenuItem(...currentItemPath)).enabled, + (await searchMenuItem(currentItemPath)).enabled, currentItem.isEnabled ) }) @@ -178,82 +188,76 @@ describe("Integration tests with single app instance", () => { }) } } + assertMenu(mocking.elements.mainMenu, []) }) describe("Raw text", () => { it("is invisible", async () => { - await assert.eventually.isFalse( - elementIsVisible(await client.$(mocking.elements.rawText.path)) - ) + assert.isTrue(await elementIsHidden(page, mocking.elements.rawText.path)) }) }) }) describe("Integration tests with their own app instance each", () => { - beforeEach(async () => { - app = await startApp(defaultDocumentPath) - client = app.client - }) + beforeEach(async () => ([app, page] = await startApp(defaultDocumentPath))) - afterEach(async () => await stopApp(app)) + afterEach(async () => await app.close()) describe("Blocked content", () => { describe("UI element", () => { it("disappears at click on X", async () => { - ;(await client.$(mocking.elements.blockedContentArea.closeButton.path)).click() - await assert.eventually.isFalse( - elementIsVisible(await client.$(mocking.elements.blockedContentArea.path)) + const blockedContentArea = mocking.elements.blockedContentArea + const blockedContentAreaElement = await page.waitForSelector( + blockedContentArea.path ) - }) - - it("unblocks content", async () => { - const blockedContentElement = await client.$( - mocking.elements.blockedContentArea.path + const blockedContentCloseButtonElement = await page.waitForSelector( + blockedContentArea.closeButton.path ) - blockedContentElement.click() - await assert.eventually.isTrue(wait(checkUnblockedMessage)) + + await blockedContentCloseButtonElement.click() + assert.isFalse(await blockedContentAreaElement.isVisible()) }) - }) - describe("Menu item", () => { it("unblocks content", async () => { - const viewMenu = mocking.elements.mainMenu.view - const viewMenuLabel = viewMenu.label - const unblockMenuLabel = viewMenu.sub.unblock.label - - await menuAddon.clickMenu(viewMenuLabel, unblockMenuLabel) - const blockedConetentMenuItem = await menuAddon.getMenuItem( - viewMenuLabel, - unblockMenuLabel + const blockedContentArea = mocking.elements.blockedContentArea + const blockedContentAreaElement = await page.waitForSelector( + blockedContentArea.path + ) + const blockedContentTextContainerElement = await page.waitForSelector( + blockedContentArea.textContainer.path ) - await assert.eventually.isTrue(wait(checkUnblockedMessage)) - assert.isFalse(blockedConetentMenuItem.enabled) + await blockedContentTextContainerElement.click() + assert.isFalse(await blockedContentAreaElement.isVisible()) + assert.isTrue(hasUnblockedContentMessage()) }) }) - }) - describe("Raw text", () => { - it("can be activated", async () => { - const viewMenu = mocking.elements.mainMenu.view + describe("Menu item", () => { + it("unblocks content", async () => { + const contentBlocking = require("../app/lib/contentBlocking/contentBlockingMain") + const unblockContentMenuId = contentBlocking.UNBLOCK_CONTENT_MENU_ID - await menuAddon.clickMenu(viewMenu.label, viewMenu.sub.rawText.label) + await clickMenuItemById(app, unblockContentMenuId) - await assert.eventually.isTrue( - elementIsVisible(await client.$(mocking.elements.rawText.path)) - ) - await assert.eventually.isFalse( - elementIsVisible(await client.$("//div[@class='markdown-body']")) - ) + assert.isTrue(await elementIsHidden(page, mocking.elements.blockedContentArea.path)) + assert.isFalse( + await app.evaluate( + ({ Menu }, menuId) => + Menu.getApplicationMenu().getMenuItemById(menuId).enabled, + unblockContentMenuId + ) + ) + assert.isTrue(hasUnblockedContentMessage()) + }) }) }) describe("Theme switching", () => { it("can be done", async () => { - const viewMenu = mocking.elements.mainMenu.view - await menuAddon.clickMenu(viewMenu.label, viewMenu.sub.switchTheme.label) - await assert.eventually.isFalse(containsConsoleMessage("error")) + await clickMenuItemById(app, "switch-theme") + assert.isFalse(containsConsoleMessage("error")) }) }) }) diff --git a/test/mocking.js b/test/mocking.js index f537449..719d5a2 100644 --- a/test/mocking.js +++ b/test/mocking.js @@ -260,13 +260,16 @@ exports.elements = { }, }, blockedContentArea: { - path: "//div[@id='blocked-content-info']", + path: "#blocked-content-info", + textContainer: { + path: "#blocked-content-info-text-container", + }, closeButton: { - path: "//span[@id='blocked-content-info-close-button']", + path: "#blocked-content-info-close-button", }, }, rawText: { - path: "//div[@id='raw-text']", + path: "#raw-text", }, }