Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: playwright e2e tests form electron application #1644

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ version: 2.1
orbs:
node: circleci/node@5.2.0
jobs:
test-e2e:
working_directory: ~/tidepool-org/chrome-uploader
parallelism: 1
docker:
- image: cimg/node:18.17.1-browsers
steps:
- run:
name: Install nvm and node
command: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
source ~/.nvm/nvm.sh
nvm install v18.17.1
nvm alias default v18.17.1
- checkout
- run: git submodule sync
- run: git submodule update --init
- run: echo 'export PATH=${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin' >> $BASH_ENV
- restore_cache:
key: dependency-cache-web-{{ checksum "package.json" }}
- run: yarn config set cache-folder ~/.cache/yarn
- run: yarn --frozen-lockfile
- save_cache:
key: dependency-cache-web-{{ checksum "package.json" }}
paths:
- ~/.cache/yarn
- ./node_modules
- run: yarn build
- run:
name: Run E2E Tests
command: yarn test-e2e
build-macos:
resource_class: macos.m1.medium.gen1
working_directory: ~/tidepool-org/chrome-uploader
Expand Down Expand Up @@ -178,11 +208,18 @@ workflows:
filters:
tags:
only: /^v.*/
requires:
- test-e2e
- build-web:
filters:
tags:
only: /^v.*/
requires:
- test-e2e
- build-windows:
filters:
tags:
only: /^v.*/
requires:
- test-e2e
- test-e2e
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ROLLBAR_POST_TOKEN=

API_URL=http://localhost:3000
UPLOAD_URL=http://localhost:3000
DATA_URL=http://localhost:3000
BLIP_URL=http://localhost:3000
DEBUG_ERROR=true
REDUX_LOG=false
REDUX_DEV_UI=false
E2E_USER_EMAIL=
E2E_USER_PASSWORD=
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ _book/
web/

\.vscode/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
videos
2 changes: 1 addition & 1 deletion app/components/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ export const Login = () => {
);
};

export default Login;
export default Login;
23 changes: 16 additions & 7 deletions app/main.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function createWindow() {
height: 769,
resizable: resizable,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false, // so that we can access process from app.html
},
Expand Down Expand Up @@ -277,7 +278,7 @@ operating system, as soon as possible.`,

let selectedPort;
for (let i = 0; i < serialPortFilter.length; i++) {
selectedPort = portList.find((element) =>
selectedPort = portList.find((element) =>
serialPortFilter[i].usbVendorId === parseInt(element.vendorId, 10) &&
serialPortFilter[i].usbProductId === parseInt(element.productId, 10)
);
Expand Down Expand Up @@ -691,17 +692,25 @@ const handleIncomingUrl = (url) => {
if (requestURL.pathname.includes('keycloak-redirect') || requestURL.pathname.includes('upload-redirect')) {
if(mainWindow){
const { webContents } = mainWindow;
const requestHash = requestURL.hash;
const newUrl = `${baseURL}${requestHash}`;
if(webContents.getURL() !== newUrl){
webContents.loadURL(newUrl);
}
// redirecting from the app html to app html with hash breaks devtools
// just send and append the hash if we're already in the app html
// if (webContents.getURL().includes(baseURL)) {
// webContents.send('newHash', requestHash);
// } else {
const requestHash = requestURL.hash;
webContents.loadURL(`${baseURL}${requestHash}`);
// }
return;
}

}
};

ipcMain.handle('handle-incoming-url', async (event, url) => {
console.log('handle-incoming-url called with URL:', url);
handleIncomingUrl(url);
});

const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
Expand All @@ -717,7 +726,7 @@ if (!gotTheLock) {
return handleIncomingUrl(url);
}
});

// Protocol handler for osx
app.on('open-url', (event, url) => {
event.preventDefault();
Expand Down
12 changes: 12 additions & 0 deletions app/preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { ipcRenderer } = require('electron');

console.log('Preload script is running');

window.electron = {
handleIncomingUrl: (url) => {
console.log('handleIncomingUrl called with URL:', url);
return ipcRenderer.invoke('handle-incoming-url', url);
}
};

console.log('Exposed handleIncomingUrl method to window.electron');
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"serve-docs": "./node_modules/.bin/gitbook serve",
"test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 jest",
"test-all": "npm run lint && npm run test && npm run build",
"test-e2e": "playwright test electron.test.js",
"lint": "node ./node_modules/eslint/bin/eslint.js --cache --format=node_modules/eslint-formatter-pretty .",
"lint-fix": "npm run lint -- --fix",
"build-main": "yarn build-main-quiet --progress --profile --colors",
Expand Down Expand Up @@ -143,7 +144,9 @@
"@babel/runtime-corejs2": "7.23.9",
"@electron/notarize": "2.2.1",
"@jest-runner/electron": "3.0.1",
"@playwright/test": "^1.42.1",
"@tidepool/direct-io": "3.0.2",
"@types/node": "^20.11.30",
"aws-sdk": "2.1544.0",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "26.6.3",
Expand All @@ -160,6 +163,7 @@
"cross-env": "7.0.3",
"css-loader": "5.2.7",
"difflet": "1.0.1",
"dotenv": "^16.4.5",
"drivelist": "11.1.0",
"electron": "27.3.0",
"electron-builder": "24.9.1",
Expand Down
47 changes: 47 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: './test/e2e',
/* Run tests in files in parallel */
// globalTimeout: 10000,
timeout: 60000,
expect: {
timeout: 20000
},

fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
// reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
video: 'on-first-retry'
},
projects: [
{
name: 'Mocked',
use: {
/* Mock the network */
mode: 'default'
}
}
]
});

Empty file removed test/e2e.js
Empty file.
100 changes: 100 additions & 0 deletions test/e2e/electron.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const { test, expect, chromium } = require('@playwright/test');
const { startElectron } = require('./utils/electron');
require('dotenv').config();
// @ts-check
test.describe('Home screen', () => {
/** @type {import('@playwright/test').Page} */
let window;
/** @type {import('@playwright/test').ElectronApplication} */
let electronApp;

/** @type {import('@playwright/test').ChromiumBrowser} */
let browser;

// HOOKS
test.beforeEach(async () => {
electronApp = await startElectron();
window = await electronApp.firstWindow();
});

test.afterEach(async () => {
await electronApp.close();
});

// TESTS
test('has correct links', async () => {
expect(await window.getByRole('link', { name: 'Get Support' }).getAttribute('href'))
.toBe('http://support.tidepool.org/');
expect(await window.getByRole('link', { name: 'Privacy and Terms of Use' })
.getAttribute('href')).toBe('http://tidepool.org/legal/');
});

test('hovered links have correct colors', async () => {
const links = ['Get Support', 'Privacy and Terms of Use'];

for (let linkText of links) {
const linkElement = window.locator('a').getByText(linkText);
const colorBefore = await linkElement.evaluate((e) => {
return window.getComputedStyle(e).getPropertyValue('color');
});
expect(colorBefore).toBe('rgb(151, 151, 151)');

await linkElement.hover();
const color = await linkElement.evaluate((e) => {
return window.getComputedStyle(e).getPropertyValue('color');
});
expect(color).toBe('rgb(98, 124, 255)');
}
});

test('has correct title', async () => {
expect(await window.title()).toBe('Tidepool Uploader');
});

test('can login with patient account', async () => {
let url;
await new Promise(async (resolve) => {
await window.waitForSelector('body');
await window.waitForLoadState('domcontentloaded');

await window.getByRole('button', { name: 'Log in' }).click();

// eslint-disable-next-line max-len
const urlPattern = /\/realms\/qa2\/protocol\/openid-connect\/auth\?client_id=tidepool-uploader-sso/;
url = (await window.waitForRequest(request => urlPattern.test(request.url()))).url();

console.log('[Electron][Auth URL] ', url);
electronApp.close();
resolve();
}).then(async () => {
browser = await chromium.launch();
console.log('[Chromium] Started 🎉');
const page = await browser.newPage();
await page.goto(url);
await page.getByPlaceholder('Email').waitFor('visible', { timeout: 10000 });
await page.getByPlaceholder('Email').fill(process.env.E2E_USER_EMAIL);
await page.getByRole('button', { name: 'Next' }).click();
await page.getByPlaceholder('Password').waitFor('visible', { timeout: 10000 });
await page.getByPlaceholder('Password').fill('tidepool');
await page.getByRole('button', { name: 'Log In' }).click();

console.log('[Chromium] Clicked Log In button');
console.log('[Chromium] Waiting for the next page');
const href = await page.getByRole('link', { name: 'Launch Uploader' }).getAttribute('href');
console.log(href);
await browser.close();

return href;
}).then(async (href) => {

electronApp = await startElectron();
window = await electronApp.firstWindow();

await window.waitForLoadState('domcontentloaded');
await window.evaluate((url) => {
window.electron.handleIncomingUrl(url);
}, href);
await expect(window.getByRole('heading', { name: 'Choose devices' })).toBeVisible();
});
});
});
17 changes: 17 additions & 0 deletions test/e2e/utils/electron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

const { _electron: electron, } = require('@playwright/test');

/**
* Launches Electron using Playwright.
* @returns {Promise<import('@playwright/test').ElectronApplication>}
The Electron application instance.
*/
async function startElectron () {
return await electron.launch({
args: ['./app/main.prod.js'],

// recordVideo: { dir: './videos' },
});
}

exports.startElectron = startElectron;
9 changes: 0 additions & 9 deletions test/example.js

This file was deleted.

Loading