Skip to content

Commit

Permalink
Port integration tests to Playwright
Browse files Browse the repository at this point in the history
Spectron will be no longer maintained.
See electron-userland/spectron#1045
for further information.
  • Loading branch information
luxuereal committed Nov 26, 2021
1 parent 0269620 commit fe262f6
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 108 deletions.
2 changes: 2 additions & 0 deletions app/lib/contentBlocking/contentBlockingRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ function unblockURL(url) {
changeInfoElementVisiblity(false)
electron.ipcRenderer.send(ipc.messages.allContentUnblocked)
}

console.log(`Unblocked: ${url}`)
}

function unblockAll() {
Expand Down
1 change: 1 addition & 0 deletions app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ function createWindow() {
{ type: "separator" },
{
label: "Switch &Theme",
id: "switch-theme",
click() {
_applicationSettings.theme = electron.nativeTheme.shouldUseDarkColors
? _applicationSettings.LIGHT_THEME
Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
206 changes: 105 additions & 101 deletions test/integrationSpec.js
Original file line number Diff line number Diff line change
@@ -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"', () => {
Expand Down Expand Up @@ -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
)
})
Expand All @@ -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"))
})
})
})
9 changes: 6 additions & 3 deletions test/mocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand Down

0 comments on commit fe262f6

Please sign in to comment.