Skip to content

Commit

Permalink
feat: replace puppeteer with playwright for e2e tests (#1802)
Browse files Browse the repository at this point in the history
* replace puppeteer with playwright
* style(e2e): more friendly waitForText

Co-authored-by: Marcin Rataj <lidel@lidel.org>
  • Loading branch information
yusefnapora and lidel committed Jun 14, 2021
1 parent 84f4337 commit f2bcd1c
Show file tree
Hide file tree
Showing 16 changed files with 4,207 additions and 2,272 deletions.
6,227 changes: 4,056 additions & 2,171 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Expand Up @@ -104,6 +104,7 @@
"devDependencies": {
"@babel/core": "^7.11.1",
"@olizilla/lol": "2.0.0",
"@playwright/test": "^1.12.1",
"@storybook/addon-a11y": "^5.3.19",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-knobs": "^5.3.19",
Expand Down Expand Up @@ -133,10 +134,12 @@
"ipfs": "^0.54.4",
"ipfsd-ctl": "^7.2.0",
"is-pull-stream": "0.0.0",
"jest-puppeteer": "^4.4.0",
"jest": "^26.6.3",
"jest-playwright-preset": "^1.6.1",
"jest-process-manager": "^0.3.1",
"multihashing-async": "^1.0.0",
"npm-run-all": "^4.1.5",
"puppeteer": "^8.0.0",
"playwright-chromium": "^1.12.1",
"run-script-os": "^1.1.1",
"shx": "^0.3.2",
"typescript": "^4.1.3",
Expand Down
14 changes: 7 additions & 7 deletions test/e2e/explore.test.js
@@ -1,16 +1,16 @@
/* global webuiUrl, ipfs, page, describe, it, expect, beforeAll */
/* global webuiUrl, ipfs, page, describe, it, expect, beforeAll, waitForText */

const fs = require('fs')

describe('Explore screen', () => {
beforeAll(async () => {
await page.goto(webuiUrl + '#/explore', { waitUntil: 'networkidle0' })
await page.goto(webuiUrl + '#/explore', { waitUntil: 'networkidle' })
})

it('should have Project Apollo Archive as one of examples', async () => {
await page.waitForSelector('a[href="#/explore/QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D"]')
await expect(page).toMatch('Project Apollo Archive')
await expect(page).toMatch('QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D')
await waitForText('Project Apollo Archives')
await waitForText('QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D')
})

it('should open arbitrary CID', async () => {
Expand All @@ -21,11 +21,11 @@ describe('Explore screen', () => {
await expect(result.cid.toString()).toStrictEqual(cid)

// open inspector
await page.goto(webuiUrl + `#/explore/${cid}`, { waitUntil: 'networkidle0' })
await page.goto(webuiUrl + `#/explore/${cid}`, { waitUntil: 'networkidle' })
await page.waitForSelector(`a[href="#/explore/${cid}"]`)
// expect node type
await expect(page).toMatch('Raw Block')
await waitForText('Raw Block')
// expect cid details
await expect(page).toMatch('base32 - cidv1 - raw - sha2-256~256~46532C71D1B730E168548410DDBB4186A2C3C0659E915B19D47F373EC6C5174A')
await waitForText('base32 - cidv1 - raw - sha2-256~256~46532C71D1B730E168548410DDBB4186A2C3C0659E915B19D47F373EC6C5174A')
})
})
41 changes: 22 additions & 19 deletions test/e2e/files.test.js
@@ -1,51 +1,51 @@
/* global webuiUrl, ipfs, page, describe, it, expect, beforeAll */
/* global webuiUrl, ipfs, page, describe, it, beforeAll, waitForText */

const { fixtureData } = require('./fixtures')
const all = require('it-all')
const filesize = require('filesize')

describe('Files screen', () => {
beforeAll(async () => {
await page.goto(webuiUrl + '#/files', { waitUntil: 'networkidle0' })
await page.goto(webuiUrl + '#/files', { waitUntil: 'networkidle' })
})

const button = 'button[id="import-button"]'

it('should have the active Add menu', async () => {
await page.waitForSelector(button, { visible: true })
await page.click(button)
await page.waitForSelector('#add-file', { visible: true })
await expect(page).toMatch('File')
await expect(page).toMatch('Folder')
await expect(page).toMatch('From IPFS')
await expect(page).toMatch('New folder')
await page.click(button)
await page.waitForSelector(button, { state: 'visible' })
await page.click(button, { force: true })
await page.waitForSelector('#add-file', { state: 'visible' })
await waitForText('File')
await waitForText('Folder')
await waitForText('From IPFS')
await waitForText('New folder')
await page.click(button, { force: true })
})

it('should allow for a successful import of two files', async () => {
await page.waitForSelector(button, { visible: true })
await page.waitForSelector(button, { state: 'visible' })
await page.click(button)
await page.waitForSelector('#add-file', { visible: true })
await page.waitForSelector('#add-file', { state: 'visible' })

const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.waitForEvent('filechooser'),
page.click('button[id="add-file"]') // menu button that triggers file selection
])

// select a single static text file via fileChooser
const file1 = fixtureData('file.txt')
const file2 = fixtureData('file2.txt')
await fileChooser.accept([file1.path, file2.path])
await fileChooser.setFiles([file1.path, file2.path])

// expect file with matching filename to be added to the file list
await page.waitForSelector('.File')
await expect(page).toMatch('file.txt')
await expect(page).toMatch('file2.txt')
await waitForText('file.txt')
await waitForText('file2.txt')

// expect valid CID to be present on the page
const [result1, result2] = await all(ipfs.addAll([file1.data, file2.data]))
await expect(page).toMatch(result1.cid.toString())
await expect(page).toMatch(result2.cid.toString())
await waitForText(result1.cid.toString())
await waitForText(result2.cid.toString())

// expect human readable sizes in format from ./src/lib/files.js#humanSize
// → this ensures metadata was correctly read for each item in the MFS
Expand All @@ -55,7 +55,10 @@ describe('Files screen', () => {
round: b >= 1073741824 ? 1 : 0
}) : '-')
for await (const file of ipfs.files.ls('/')) {
await expect(page).toMatch(human(file.size))
// the text matcher used by waitForText is particular about whitespace. When the file size is rendered, it uses a `&nbsp;` element, which translates to unicode character 0xa0.
// If we try to match a plain space, it will fail, so we replace space with `\u00a0` here.
const expected = human(file.size).replace(' ', '\u00a0')
await waitForText(expected)
}
})
})
9 changes: 9 additions & 0 deletions test/e2e/jest-playwright.config.js
@@ -0,0 +1,9 @@
const debug = process.env.DEBUG === 'true'
const ci = process.env.TRAVIS === 'true' || process.env.CI === 'true'

// TODO: figure out what options to use
module.exports = {
launchOptions: {
headless: (!debug || ci) // show browser window when in debug mode
}
}
9 changes: 0 additions & 9 deletions test/e2e/jest-puppeteer.config.js

This file was deleted.

6 changes: 5 additions & 1 deletion test/e2e/jest.config.js
@@ -1,7 +1,11 @@

process.env.JEST_PLAYWRIGHT_CONFIG = './jest-playwright.config.js'

module.exports = {
preset: 'jest-puppeteer',
preset: 'jest-playwright-preset',
testRegex: './*\\.test\\.js$',
testEnvironment: './setup/test-environment.js',
testTimeout: 30 * 1000,
globalSetup: './setup/global-init.js',
setupFilesAfterEnv: ['./setup/global-after-env.js'],
globalTeardown: './setup/global-teardown.js'
Expand Down
14 changes: 7 additions & 7 deletions test/e2e/navigation.test.js
@@ -1,4 +1,4 @@
/* global webuiUrl, waitForTitle, page, describe, it, expect, beforeAll */
/* global webuiUrl, waitForTitle, page, describe, it, beforeAll, waitForText */

const scrollLinkContainer = async () => {
const linkContainer = '[role="menubar"]'
Expand All @@ -12,29 +12,29 @@ const scrollLinkContainer = async () => {

describe('Navigation menu', () => {
beforeAll(async () => {
await page.goto(webuiUrl + '#/blank', { waitUntil: 'networkidle0' })
await page.goto(webuiUrl + '#/blank', { waitUntil: 'networkidle' })
})

it('should work for Status page', async () => {
const link = 'a[href="#/"]'
await page.waitForSelector(link)
await expect(page).toMatch('Status')
await waitForText('Status')
await page.click(link)
await waitForTitle('Status | IPFS')
})

it('should work for Files page', async () => {
const link = 'a[href="#/files"]'
await page.waitForSelector(link)
await expect(page).toMatch('Files')
await waitForText('Files')
await page.click(link)
await waitForTitle('/ | Files | IPFS')
})

it('should work for Explore page', async () => {
const link = 'a[href="#/explore"]'
await page.waitForSelector(link)
await expect(page).toMatch('Explore')
await waitForText('Explore')
await scrollLinkContainer()
await page.click(link)
await waitForTitle('Explore | IPLD')
Expand All @@ -43,7 +43,7 @@ describe('Navigation menu', () => {
it('should work for Peers page', async () => {
const link = 'a[href="#/peers"]'
await page.waitForSelector(link)
await expect(page).toMatch('Peers')
await waitForText('Peers')
await scrollLinkContainer()
await page.click(link)
await waitForTitle('Peers | IPFS')
Expand All @@ -52,7 +52,7 @@ describe('Navigation menu', () => {
it('should work for Settings page', async () => {
const link = 'a[href="#/settings"]'
await page.waitForSelector(link)
await expect(page).toMatch('Settings')
await waitForText('Settings')
await scrollLinkContainer()
await page.click(link)
await waitForTitle('Settings | IPFS')
Expand Down
14 changes: 7 additions & 7 deletions test/e2e/peers.test.js
@@ -1,4 +1,4 @@
/* global webuiUrl, ipfs page, describe, it, expect, beforeAll, afterAll */
/* global webuiUrl, ipfs, page, describe, it, beforeAll, afterAll, waitForText */

const { createController } = require('ipfsd-ctl')

Expand All @@ -18,15 +18,15 @@ describe('Peers screen', () => {
peeraddr = addresses.find((ma) => ma.toString().startsWith('/ip4/127.0.0.1')).toString()
// connect to peer to have something in the peer table
await ipfs.swarm.connect(peeraddr)
await page.goto(webuiUrl + '#/peers', { waitUntil: 'networkidle0' })
await page.goto(webuiUrl + '#/peers', { waitUntil: 'networkidle' })
})

it('should have a clickable "Add connection" button', async () => {
const addConnection = 'Add connection'
await expect(page).toMatch(addConnection)
await expect(page).toClick('button', { text: addConnection })
await waitForText(addConnection)
await page.click(`text=${addConnection}`)
await page.waitForSelector('div[role="dialog"]')
await expect(page).toMatch('Insert the peer address you want to connect to')
await waitForText('Insert the peer address you want to connect to')
})

it('should confirm connection after "Add connection" ', async () => {
Expand All @@ -36,11 +36,11 @@ describe('Peers screen', () => {
await page.keyboard.type('\n')
// expect connection confirmation
await page.waitForSelector('.bg-green', { visible: true })
await expect(page).toMatch('Successfully connected to the provided peer')
await waitForText('Successfully connected to the provided peer')
})

it('should have a peer from a "Local Network"', async () => {
await expect(page).toMatch('Local Network')
await waitForText('Local Network')
})

afterAll(async () => {
Expand Down
48 changes: 29 additions & 19 deletions test/e2e/remote-api.test.js
@@ -1,4 +1,4 @@
/* global ipfs, webuiUrl, page, describe, it, expect, beforeAll */
/* global ipfs, webuiUrl, page, describe, it, expect, beforeAll, waitForText */

const { createController } = require('ipfsd-ctl')
const getPort = require('get-port')
Expand Down Expand Up @@ -105,11 +105,11 @@ const switchIpfsApiEndpointViaLocalStorage = async (endpoint) => {
}

const switchIpfsApiEndpointViaSettings = async (endpoint) => {
await expect(page).toClick('a[href="#/settings"]')
await page.click('a[href="#/settings"]')
const selector = 'input[id="api-address"]'
await page.waitForSelector(selector, { visible: true })
await expect(page).toFill(selector, endpoint)
await page.type(selector, '\n')
await expect(page).toHaveSelector(selector)
await page.fill(selector, endpoint)
await page.press(selector, 'Enter')
await waitForIpfsApiEndpoint(endpoint)
}

Expand All @@ -118,7 +118,8 @@ const waitForIpfsApiEndpoint = async (endpoint) => {
try {
// unwrap port if JSON config is passed
const json = JSON.parse(endpoint)
endpoint = json.port || endpoint
const uri = new URL(json.url)
endpoint = uri.port || endpoint
} catch (_) {}
try {
// unwrap port if inlined basic auth was passed
Expand All @@ -128,10 +129,11 @@ const waitForIpfsApiEndpoint = async (endpoint) => {
endpoint = uri.port || endpoint
}
} catch (_) {}
await page.waitForFunction(`localStorage.getItem('ipfsApi') && localStorage.getItem('ipfsApi').includes('${endpoint}')`)
// await page.waitForFunction(`localStorage.getItem('ipfsApi') && localStorage.getItem('ipfsApi').includes('${endpoint}')`)
await page.waitForFunction(endpoint => window.localStorage.getItem('ipfsApi') && window.localStorage.getItem('ipfsApi').includes(endpoint), endpoint)
return
}
await page.waitForFunction('localStorage.getItem(\'ipfsApi\') === null')
await page.waitForFunction(() => window.localStorage.getItem('ipfsApi') === null)
}

const basicAuthConnectionConfirmation = async (user, password, proxyPort) => {
Expand All @@ -140,6 +142,7 @@ const basicAuthConnectionConfirmation = async (user, password, proxyPort) => {
await expectHttpApiAddressOnStatusPage('Custom JSON configuration')
// confirm webui is actually connected to expected node :^)
await expectPeerIdOnStatusPage(ipfsd.api)

// (2) go to Settings and confirm API string includes expected JSON config
const apiOptions = JSON.stringify({
url: `http://127.0.0.1:${proxyPort}/`,
Expand All @@ -152,28 +155,31 @@ const basicAuthConnectionConfirmation = async (user, password, proxyPort) => {

const expectPeerIdOnStatusPage = async (api) => {
const { id } = await api.id()
await expect(page).toMatch(id)
await waitForText(id)
}

const expectHttpApiAddressOnStatusPage = async (value) => {
await expect(page).toClick('a[href="#/"]')
await page.waitForSelector('a[href="#/"]')
await page.click('a[href="#/"]')
await page.reload() // instant addr update for faster CI
await page.waitForSelector('summary', { visible: true })
await expect(page).toClick('summary', { text: 'Advanced' })
const apiAddressOnStatus = await page.waitForSelector('div[id="http-api-address"]', { visible: true })
await expect(apiAddressOnStatus).toMatch(String(value))
await page.waitForSelector('summary', { state: 'visible' })
await page.click('summary')
await page.waitForSelector('div[id="http-api-address"]', { state: 'visible' })
await waitForText(String(value))
}

const expectHttpApiAddressOnSettingsPage = async (value) => {
await expect(page).toClick('a[href="#/settings"]')
await page.waitForSelector('input[id="api-address"]', { visible: true })
await expect(page).toHaveSelector('a[href="#/settings"]')
await page.click('a[href="#/settings"]')
await page.waitForSelector('input[id="api-address"]', { state: 'visible' })
const apiAddrInput = await page.$('#api-address')
const apiAddrValue = await page.evaluate(x => x.value, apiAddrInput)
// if API address is defined as JSON, match objects
try {
const json = JSON.parse(apiAddrValue)
const expectedJson = JSON.parse(value)
return await expect(json).toMatchObject(expectedJson)
await expect(json).toMatchObject(expectedJson)
return
} catch (_) {}
// else, match strings (Multiaddr or URL)
await expect(apiAddrValue).toMatch(String(value))
Expand Down Expand Up @@ -221,8 +227,12 @@ describe('API @ URL', () => {
})

describe('API with CORS and Basic Auth', () => {
afterEach(async () => {
await switchIpfsApiEndpointViaLocalStorage(null)
})

it('should work when localStorage[ipfsApi] is set to URL with inlined Basic Auth credentials', async () => {
await switchIpfsApiEndpointViaLocalStorage(`http://${user}:${password}@127.0.0.1:${proxyPort}`)
await switchIpfsApiEndpointViaLocalStorage(`http://${user}:${password}@127.0.0.1:${proxyPort}/`)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})

Expand All @@ -238,7 +248,7 @@ describe('API with CORS and Basic Auth', () => {
})

it('should work when URL with inlined credentials are entered at the Settings page', async () => {
const basicAuthApiAddr = `http://${user}:${password}@127.0.0.1:${proxyPort}`
const basicAuthApiAddr = `http://${user}:${password}@127.0.0.1:${proxyPort}/`
await switchIpfsApiEndpointViaSettings(basicAuthApiAddr)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})
Expand Down

0 comments on commit f2bcd1c

Please sign in to comment.