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 4df06bcd8a..70c056bafa 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,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 78f0528e38..b126bb1bd1 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -15,33 +15,38 @@ * == BSD2 LICENSE == */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import styles from '../../styles/components/Login.module.less'; import { useDispatch, useSelector } from 'react-redux'; - import actions from '../actions/'; const asyncActions = actions.async; const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); - +const i18n = remote.getGlobal('i18n'); import { keycloak } from '../keycloak'; export const Login = () => { const dispatch = useDispatch(); const disabled = useSelector((state) => Boolean(state.unsupported)); const errorMessage = useSelector((state) => state.loginErrorMessage); - const forgotPasswordUrl = useSelector( - (state) => state.blipUrls.forgotPassword - ); - const isLoggingIn = useSelector( - (state) => state.working.loggingIn.inProgress - ); + const forgotPasswordUrl = useSelector((state) => state.blipUrls.forgotPassword); + const isLoggingIn = useSelector((state) => state.working.loggingIn.inProgress); const keycloakConfig = useSelector((state) => state.keycloakConfig); const keycloakInitializing = keycloakConfig.url && !keycloakConfig.initialized; const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [remember, setRemember] = useState(true); + const [loginUrl, setLoginUrl] = useState(''); + + useEffect(() => { + if (keycloakConfig.initialized) { + try { + setLoginUrl(keycloak.createLoginUrl()); + } catch (error) { + console.error('Error creating Keycloak login URL:', error); + } + } + }, [keycloakConfig]); const renderForgotPasswordLink = () => { return ( @@ -52,7 +57,7 @@ export const Login = () => { }; const renderButton = () => { - var text = i18n.t('Log in'); + let text = i18n.t('Log in'); if (isLoggingIn) { text = i18n.t('Logging in...'); @@ -68,6 +73,7 @@ export const Login = () => { className={styles.button} onClick={handleLogin} disabled={isLoggingIn || disabled || keycloakInitializing} + data-testUrl={loginUrl ? loginUrl.toString() : ''} > {text} @@ -77,7 +83,7 @@ export const Login = () => { const handleLogin = (e) => { e.preventDefault(); if (keycloakConfig.initialized) { - window.open(keycloak.createLoginUrl(), '_blank'); + window.open(loginUrl, '_blank'); } else { dispatch(asyncActions.doLogin({ username, password }, { remember })); } diff --git a/app/main.dev.js b/app/main.dev.js index ae1fd4380d..8b680ceeef 100755 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -149,6 +149,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 }, @@ -261,7 +262,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) ); @@ -652,17 +653,22 @@ const handleIncomingUrl = (url) => { const { webContents } = mainWindow; // 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 { + // if (webContents.getURL().includes(baseURL)) { + // webContents.send('newHash', requestHash); + // } else { webContents.loadURL(`${baseURL}${requestHash}`); - } - return; + // } + 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) { @@ -678,7 +684,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 99d4dac3bf..266e0e06ef 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", @@ -131,7 +132,9 @@ "@babel/register": "7.23.7", "@babel/runtime-corejs2": "7.23.9", "@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", @@ -147,6 +150,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..97abc0a17f --- /dev/null +++ b/test/e2e/electron.test.js @@ -0,0 +1,96 @@ +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'); + + const login = window.getByRole('button', { name: 'Log in' }); + url = await login.getAttribute('data-testurl'); + 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 94a943ea76..e924c75f01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2388,6 +2388,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" @@ -2673,6 +2680,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" @@ -5917,6 +5931,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" @@ -7320,6 +7339,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" @@ -7328,11 +7352,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.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ftdi-js/-/ftdi-js-0.3.0.tgz#25d878ba781b89a908f98532682c17ee8e582740" @@ -11555,7 +11574,21 @@ pl2303@0.0.9: dependencies: usb "2.4.3" -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== @@ -11564,6 +11597,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"