diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ef0c6bcc0..d92e27872b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 @@ -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 diff --git a/.env.example b/.env.example new file mode 100755 index 0000000000..4e46bcafe7 --- /dev/null +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 65285681a9..14812d058e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,8 @@ _book/ web/ \.vscode/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +videos \ No newline at end of file diff --git a/app/components/Login.js b/app/components/Login.js index 0e691961a4..533b59a75f 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -142,4 +142,4 @@ export const Login = () => { ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/app/main.dev.js b/app/main.dev.js index b3efbef5d0..a08c2d85c6 100755 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -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 }, @@ -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) ); @@ -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) { @@ -717,7 +726,7 @@ if (!gotTheLock) { return handleIncomingUrl(url); } }); - + // Protocol handler for osx app.on('open-url', (event, url) => { event.preventDefault(); diff --git a/app/preload.js b/app/preload.js new file mode 100644 index 0000000000..3d47e16089 --- /dev/null +++ b/app/preload.js @@ -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'); \ No newline at end of file diff --git a/package.json b/package.json index 5acf1126ae..740646b21b 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..9ccd319a19 --- /dev/null +++ b/playwright.config.js @@ -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' + } + } + ] +}); + diff --git a/test/e2e.js b/test/e2e.js deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/test/e2e/electron.test.js b/test/e2e/electron.test.js new file mode 100644 index 0000000000..005c3d6323 --- /dev/null +++ b/test/e2e/electron.test.js @@ -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(); + }); + }); +}); diff --git a/test/e2e/utils/electron.js b/test/e2e/utils/electron.js new file mode 100644 index 0000000000..28bb28b7e7 --- /dev/null +++ b/test/e2e/utils/electron.js @@ -0,0 +1,17 @@ + +const { _electron: electron, } = require('@playwright/test'); + +/** + * Launches Electron using Playwright. + * @returns {Promise} + The Electron application instance. + */ +async function startElectron () { + return await electron.launch({ + args: ['./app/main.prod.js'], + + // recordVideo: { dir: './videos' }, + }); +} + +exports.startElectron = startElectron; \ No newline at end of file diff --git a/test/example.js b/test/example.js deleted file mode 100755 index c42692573e..0000000000 --- a/test/example.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable func-names */ -import { expect } from 'chai'; - - -describe('description', () => { - test('should have description', () => { - expect(1 + 2).to.equal(3); - }); -}); diff --git a/yarn.lock b/yarn.lock index 8af98ea4d1..8cf8eb8088 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2324,6 +2324,13 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@^1.42.1": + version "1.42.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.42.1.tgz#9eff7417bcaa770e9e9a00439e078284b301f31c" + integrity sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ== + dependencies: + playwright "1.42.1" + "@polka/url@^1.0.0-next.24": version "1.0.0-next.24" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" @@ -2609,6 +2616,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.11.30": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -5946,6 +5960,11 @@ dotenv-expand@^5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + dotenv@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" @@ -7414,6 +7433,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2, fsevents@^2.1.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@^1.2.7: version "1.2.13" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" @@ -7422,11 +7446,6 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - ftdi-js@0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/ftdi-js/-/ftdi-js-0.4.1.tgz#ba0f7f970908bac9a2d39deec99fa44e7dbd63ec" @@ -11809,7 +11828,21 @@ pl2303@0.1.0: dependencies: usb "2.11.0" -plist@3.1.0, plist@^3.0.4, plist@^3.0.5: +playwright-core@1.42.1: + version "1.42.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.42.1.tgz#13c150b93c940a3280ab1d3fbc945bc855c9459e" + integrity sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA== + +playwright@1.42.1: + version "1.42.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.42.1.tgz#79c828b51fe3830211137550542426111dc8239f" + integrity sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg== + dependencies: + playwright-core "1.42.1" + optionalDependencies: + fsevents "2.3.2" + +plist@3.1.0, plist@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== @@ -11818,6 +11851,14 @@ plist@3.1.0, plist@^3.0.4, plist@^3.0.5: base64-js "^1.5.1" xmlbuilder "^15.1.1" +plist@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" + integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== + dependencies: + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + plugin-error@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"