Add playwright integration testing to the app (PR #1397)

This PR allows us to code playwright integration tests that can potentially replace some of our manual tests and allow us automated testing of the app itself.

Current technical limitations:
* No app level keyboard simulation support (e.g, zoom in, zoom out, etc.)
* No code coverage support, so even though we are testing the app, the code coverage does not reflect this fact
This commit is contained in:
Adam Weeden 2022-04-20 22:03:49 -04:00 committed by GitHub
parent c42c63a8b0
commit e664bc6af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1769 additions and 367 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1

53
.github/manual-test vendored
View File

@ -60,10 +60,6 @@ node ./lib/cli.js 'https://npmjs.com/' \
"$tmp_dir"
printf '\n***** SMOKE TEST 1: Test checklist *****
- Injected js: should show an alert saying hello
- Injected css: should make npmjs all blue
- Internal links open internally
- External links open in browser
- Context menu -> Open Link In New Window works
- MAC ONLY: Context menu -> Open Link In New Tab works
- Keyboard shortcuts: {back, forward, zoom in/out/zero} work
@ -73,52 +69,15 @@ request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf "\n***** SMOKE TEST 2: Setting up test and building app... *****\n"
tmp_dir=$(mktemp -d -t nativefier-manual-test-auth-XXXXX)
name="nativefier-smoke-test-2"
# Removing for now as httpbin is not presently up
# node ./lib/cli.js 'http://httpbin.org/basic-auth/foo/bar' \
node ./lib/cli.js 'https://authenticationtest.com/HTTPAuth/' \
--basic-auth-username user \
--basic-auth-password pass \
--name "$name" \
"$tmp_dir"
printf '\n***** SMOKE TEST 2: Test checklist *****
- Was successfully logged in via HTTP Basic Auth. Should see a "Login Success" and a green banner.
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 3: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-auth-prompt-XXXXX)
name='nativefier-smoke-test-3'
# node ./lib/cli.js 'http://httpbin.org/basic-auth/foo/bar' \
node ./lib/cli.js 'https://authenticationtest.com/HTTPAuth/' \
--name "$name" \
"$tmp_dir"
printf '\n***** SMOKE TEST 3: Test checklist *****
- Should get a login window. Log in with username="user" and password="pass".
- Post login, you should see a "Login Success" and a green banner.
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 4: Setting up test and building app... *****\n'
printf '\n***** SMOKE TEST 2: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-tray-XXXXX)
name='nativefier-smoke-test-4'
name='nativefier-smoke-test-2'
node ./lib/cli.js 'https://google.com/' \
--name "$name" \
--tray \
"$tmp_dir"
printf '\n***** SMOKE TEST 4: Test checklist *****
printf '\n***** SMOKE TEST 2: Test checklist *****
- Should have an app with a tray icon
- Console: no Electron runtime deprecation warnings/error logged'
@ -127,15 +86,15 @@ request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 5: Setting up test and building app... *****\n'
printf '\n***** SMOKE TEST 3: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-start-in-tray-XXXXX)
name='nativefier-smoke-test-5'
name='nativefier-smoke-test-3'
node ./lib/cli.js 'https://google.com/' \
--name "$name" \
--tray start-in-tray \
"$tmp_dir"
printf '\n***** SMOKE TEST 5: Test checklist *****
printf '\n***** SMOKE TEST 3: Test checklist *****
- Should have an app that does not show a window initially,
but will have a tray icon that will show the window.
- Console: no Electron runtime deprecation warnings/error logged'

View File

@ -39,8 +39,12 @@ jobs:
package-lock.json
app/package-lock.json
# Will also (through `prepare` hook): 1. install ./app, and 2. build
- run: npm ci --no-fund
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund
# Only run linter once, for faster CI. Align the versions of Node here with above and publish.yml.
- if: matrix.platform == 'ubuntu-latest' && matrix.node-version == '17'
run: npm run lint
- run: npm test
- run: npm run test -- --testPathIgnorePatterns ".*playwright.*"
- if: matrix.platform != 'ubuntu-latest' # Doesn't work on non-GUI ubuntu
run: npm run test:playwright

2
.gitignore vendored
View File

@ -8,6 +8,8 @@ app/dist/*
built-tests
# commit a placeholder to keep the app/lib directory
app/inject
!app/inject/_placeholder
!app/lib/.placeholder
dist

206
app/npm-shrinkwrap.json generated
View File

@ -64,9 +64,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz",
"integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==",
"version": "16.11.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz",
"integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw==",
"dev": true
},
"node_modules/ansi-regex": {
@ -270,16 +270,20 @@
"dev": true
},
"node_modules/define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
"integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
"dev": true,
"optional": true,
"dependencies": {
"object-keys": "^1.0.12"
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
@ -296,9 +300,9 @@
"dev": true
},
"node_modules/electron": {
"version": "18.0.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-18.0.3.tgz",
"integrity": "sha512-QRUZkGL8O/8CyDmTLSjBeRsZmGTPlPVeWnnpkdNqgHYYaOc/A881FKMiNzvQ9Cj0a+rUavDdwBUfUL82U3Ay7w==",
"version": "18.0.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-18.0.4.tgz",
"integrity": "sha512-xfsozNpFr3WzeM1EFlw2qqiqXbCrgQNBJJMlcC4/DUYVpkF8364SZenX7FFFA42NmwXiOEahkvvho/u7UrAcGg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -517,6 +521,28 @@
"node": ">=6 <7 || >=8"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true,
"optional": true
},
"node_modules/get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"optional": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@ -548,19 +574,19 @@
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"optional": true,
"dependencies": {
"lru-cache": "^7.4.0"
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0"
"node": ">=10"
}
},
"node_modules/global-tunnel-ng": {
@ -623,6 +649,45 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"devOptional": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"optional": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
"dev": true,
"optional": true,
"dependencies": {
"get-intrinsic": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
@ -723,13 +788,16 @@
}
},
"node_modules/lru-cache": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz",
"integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=12"
"node": ">=10"
}
},
"node_modules/matcher": {
@ -1186,6 +1254,13 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"optional": true
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@ -1231,9 +1306,9 @@
}
},
"@types/node": {
"version": "16.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz",
"integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==",
"version": "16.11.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.27.tgz",
"integrity": "sha512-C1pD3kgLoZ56Uuy5lhfOxie4aZlA3UMGLX9rXteq4WitEZH6Rl80mwactt9QG0w0gLFlN/kLBTFnGXtDVWvWQw==",
"dev": true
},
"ansi-regex": {
@ -1389,13 +1464,14 @@
"dev": true
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
"integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
"dev": true,
"optional": true,
"requires": {
"object-keys": "^1.0.12"
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
},
"detect-node": {
@ -1412,9 +1488,9 @@
"dev": true
},
"electron": {
"version": "18.0.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-18.0.3.tgz",
"integrity": "sha512-QRUZkGL8O/8CyDmTLSjBeRsZmGTPlPVeWnnpkdNqgHYYaOc/A881FKMiNzvQ9Cj0a+rUavDdwBUfUL82U3Ay7w==",
"version": "18.0.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-18.0.4.tgz",
"integrity": "sha512-xfsozNpFr3WzeM1EFlw2qqiqXbCrgQNBJJMlcC4/DUYVpkF8364SZenX7FFFA42NmwXiOEahkvvho/u7UrAcGg==",
"dev": true,
"requires": {
"@electron/get": "^1.13.0",
@ -1591,6 +1667,25 @@
"universalify": "^0.1.0"
}
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true,
"optional": true
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"optional": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
}
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@ -1616,13 +1711,13 @@
},
"dependencies": {
"semver": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"optional": true,
"requires": {
"lru-cache": "^7.4.0"
"lru-cache": "^6.0.0"
}
}
}
@ -1675,6 +1770,33 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"devOptional": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"optional": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-property-descriptors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
"dev": true,
"optional": true,
"requires": {
"get-intrinsic": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"optional": true
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
@ -1759,11 +1881,14 @@
"dev": true
},
"lru-cache": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz",
"integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
},
"matcher": {
"version": "3.0.0",
@ -2123,6 +2248,13 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"optional": true
},
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",

View File

@ -4,8 +4,9 @@ import {
NewWindowWebContentsEvent,
} from 'electron';
import contextMenu from 'electron-context-menu';
import log from 'loglevel';
import { nativeTabsSupported, openExternal } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { setupNativefierWindow } from '../helpers/windowEvents';
import { createNewWindow } from '../helpers/windowHelpers';
import {

View File

@ -1,9 +1,9 @@
import * as path from 'path';
import * as log from 'loglevel';
import { BrowserWindow, ipcMain } from 'electron';
import * as log from '../helpers/loggingHelper';
export async function createLoginWindow(
loginCallback: (username?: string, password?: string) => void,
parent?: BrowserWindow,

View File

@ -3,14 +3,17 @@ import * as path from 'path';
import { ipcMain, BrowserWindow, Event } from 'electron';
import windowStateKeeper from 'electron-window-state';
import log from 'loglevel';
import { initContextMenu } from './contextMenu';
import { createMenu } from './menu';
import {
getAppIcon,
getCounterValue,
isOSX,
nativeTabsSupported,
} from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { IS_PLAYWRIGHT } from '../helpers/playwrightHelpers';
import { onNewWindow, setupNativefierWindow } from '../helpers/windowEvents';
import {
clearCache,
@ -18,8 +21,6 @@ import {
getDefaultWindowOptions,
hideWindow,
} from '../helpers/windowHelpers';
import { initContextMenu } from './contextMenu';
import { createMenu } from './menu';
import {
OutputOptions,
outputOptionsToWindowOptions,
@ -77,6 +78,11 @@ export async function createMainWindow(
...getDefaultWindowOptions(outputOptionsToWindowOptions(options)),
});
// Just load about:blank to start, gives playwright something to latch onto initially for testing.
if (IS_PLAYWRIGHT) {
await mainWindow.loadURL('about:blank');
}
mainWindowState.manage(mainWindow);
// after first run, no longer force maximize to be true

View File

@ -8,9 +8,9 @@ import {
MenuItem,
MenuItemConstructorOptions,
} from 'electron';
import * as log from 'loglevel';
import { cleanupPlainText, isOSX, openExternal } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import {
clearAppData,
getCurrentURL,

View File

@ -1,7 +1,7 @@
import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron';
import log from 'loglevel';
import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers';
import * as log from '../helpers/loggingHelper';
import { OutputOptions } from '../../../shared/src/options/model';
export function createTrayIcon(

View File

@ -3,7 +3,8 @@ import * as os from 'os';
import * as path from 'path';
import { BrowserWindow, OpenExternalOptions, shell } from 'electron';
import * as log from 'loglevel';
import * as log from '../helpers/loggingHelper';
export const INJECT_DIR = path.join(__dirname, '..', 'inject');

View File

@ -1,8 +1,8 @@
import * as fs from 'fs';
import log from 'loglevel';
import * as path from 'path';
import { isOSX, isWindows, isLinux } from './helpers';
import * as log from './loggingHelper';
type fsError = Error & { code: string };

View File

@ -0,0 +1,82 @@
// This helper allows logs to either be printed to the console as they would normally or if
// the USE_LOG_FILE environment variable is set (such as through our playwright tests), then
// the logs can be diverted from the command line to a log file, so that they can be displayed
// later (such as at the end of a playwright test run to help diagnose potential failures).
// Use this instead of loglevel whenever logging messages inside the app.
import * as fs from 'fs';
import * as path from 'path';
import loglevel from 'loglevel';
import { safeGetEnv } from './playwrightHelpers';
const USE_LOG_FILE = safeGetEnv('USE_LOG_FILE') === '1';
const LOG_FILE_DIR = safeGetEnv('LOG_FILE_DIR') ?? process.cwd();
const LOG_FILENAME = path.join(LOG_FILE_DIR, `${new Date().getTime()}.log`);
const logLevelNames = ['TRACE', 'DEBUG', 'INFO ', 'WARN ', 'ERROR'];
function _logger(
logFunc: (...args: unknown[]) => void,
level: loglevel.LogLevelNumbers,
...args: unknown[]
): void {
if (USE_LOG_FILE && loglevel.getLevel() >= level) {
for (const arg of args) {
try {
const lines =
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
JSON.stringify(arg, null, 2)?.split('\n') ?? `${arg}`.split('\n');
for (const line of lines) {
fs.appendFileSync(
LOG_FILENAME,
`${new Date().getTime()} ${logLevelNames[level]} ${line}\n`,
);
}
} catch {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fs.appendFileSync(LOG_FILENAME, `${logLevelNames[level]} ${arg}\n`);
}
}
} else {
logFunc(...args);
}
}
export function debug(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.debug, loglevel.levels.DEBUG, ...args);
}
export function error(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.error, loglevel.levels.ERROR, ...args);
}
export function info(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.info, loglevel.levels.INFO, ...args);
}
export function log(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.info, loglevel.levels.INFO, ...args);
}
export function setLevel(
level: loglevel.LogLevelDesc,
persist?: boolean,
): void {
loglevel.setLevel(level, persist);
}
export function trace(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.trace, loglevel.levels.TRACE, ...args);
}
export function warn(...args: unknown[]): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
_logger(loglevel.warn, loglevel.levels.WARN, ...args);
}

View File

@ -0,0 +1,6 @@
export const IS_PLAYWRIGHT = safeGetEnv('PLAYWRIGHT_TEST') === '1';
export const PLAYWRIGHT_CONFIG = safeGetEnv('PLAYWRIGHT_CONFIG');
export function safeGetEnv(key: string): string | undefined {
return key in process.env ? process.env[key] : undefined;
}

View File

@ -5,10 +5,9 @@ import {
NewWindowWebContentsEvent,
WebContents,
} from 'electron';
import log from 'loglevel';
import { WindowOptions } from '../../../shared/src/options/model';
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
import * as log from './loggingHelper';
import {
blockExternalURL,
createAboutBlankWindow,
@ -17,6 +16,7 @@ import {
sendParamsOnDidFinishLoad,
setProxyRules,
} from './windowHelpers';
import { WindowOptions } from '../../../shared/src/options/model';
export function onNewWindow(
options: WindowOptions,

View File

@ -1,3 +1,5 @@
import path from 'path';
import {
dialog,
BrowserWindow,
@ -8,10 +10,9 @@ import {
OnResponseStartedListenerDetails,
} from 'electron';
import log from 'loglevel';
import path from 'path';
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
import * as log from './loggingHelper';
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
const ZOOM_INTERVAL = 0.1;

View File

@ -12,17 +12,22 @@ import electron, {
Event,
} from 'electron';
import electronDownload from 'electron-dl';
import * as log from 'loglevel';
import { createLoginWindow } from './components/loginWindow';
import {
createMainWindow,
saveAppArgs,
APP_ARGS_FILE_PATH,
createMainWindow,
} from './components/mainWindow';
import { createTrayIcon } from './components/trayIcon';
import { isOSX, removeUserAgentSpecifics } from './helpers/helpers';
import { inferFlashPath } from './helpers/inferFlash';
import * as log from './helpers/loggingHelper';
import {
IS_PLAYWRIGHT,
PLAYWRIGHT_CONFIG,
safeGetEnv,
} from './helpers/playwrightHelpers';
import { setupNativefierWindow } from './helpers/windowEvents';
import {
OutputOptions,
@ -34,17 +39,21 @@ if (require('electron-squirrel-startup')) {
app.exit();
}
if (process.argv.indexOf('--verbose') > -1) {
if (process.argv.indexOf('--verbose') > -1 || safeGetEnv('VERBOSE') === '1') {
log.setLevel('DEBUG');
process.traceDeprecation = true;
process.traceProcessWarnings = true;
process.argv.slice(1);
}
let mainWindow: BrowserWindow;
const appArgs = JSON.parse(
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
) as OutputOptions;
const appArgs =
IS_PLAYWRIGHT && PLAYWRIGHT_CONFIG
? (JSON.parse(PLAYWRIGHT_CONFIG) as OutputOptions)
: (JSON.parse(
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
) as OutputOptions);
log.debug('appArgs', appArgs);
// Do this relatively early so that we can start storing appData with the app
@ -182,7 +191,7 @@ const setDockBadge = isOSX()
app.on('window-all-closed', () => {
log.debug('app.window-all-closed');
if (!isOSX() || appArgs.fastQuit) {
if (!isOSX() || appArgs.fastQuit || IS_PLAYWRIGHT) {
app.quit();
}
});
@ -243,7 +252,7 @@ if (appArgs.widevine) {
app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => {
log.debug('app.activate', { event, hasVisibleWindows });
if (isOSX()) {
if (isOSX() && !IS_PLAYWRIGHT) {
// this is called when the dock is clicked
if (!hasVisibleWindows) {
mainWindow.show();

1286
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,7 @@
"relock": "rm -rf ./node_modules/ ./app/node_modules/ ./npm-shrinkwrap.json ./app/npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out; cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
"test:integration": "jest --testRegex '.*integration-test.js'",
"test:manual": "npm run build && bash .github/manual-test",
"test:playwright": "jest --testRegex '.*playwright-test.js'",
"test:unit": "jest",
"test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false",
"test:withlog": "LOGLEVEL=trace npm run test",
@ -78,10 +79,12 @@
"@types/tmp": "^0.2.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"electron": "^18.0.3",
"eslint": "^8.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.0.6",
"playwright": "^1.20.1",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"ts-loader": "^9.2.3",
@ -95,6 +98,14 @@
},
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"./app/dist/**/*.js",
"./lib/**/*.js",
"./shared/lib/**/*.js"
],
"coveragePathIgnorePatterns": [
"[.-]test.js$"
],
"moduleNameMapper": {
"^electron$": "<rootDir>/app/dist/mocks/electron.js"
},
@ -103,19 +114,18 @@
],
"testEnvironment": "node",
"testPathIgnorePatterns": [
"<rootDir>/src.*",
"<rootDir>/node_modules.*",
"<rootDir>/app/node_modules.*",
"<rootDir>/app/src.*",
"<rootDir>/app/lib.*",
"<rootDir>/app/node_modules.*"
"<rootDir>/src.*"
],
"watchPathIgnorePatterns": [
"<rootDir>/src.*",
"<rootDir>/tsconfig-base.json",
"<rootDir>/app/src.*",
"<rootDir>/app/lib.*",
"<rootDir>/app/src.*",
"<rootDir>/app/tsconfig.json",
"<rootDir>/shared/tsconfig.json"
"<rootDir>/shared/tsconfig.json",
"<rootDir>/src.*",
"<rootDir>/tsconfig-base.json"
]
},
"prettier": {

View File

@ -3,6 +3,7 @@ import * as path from 'path';
export const DEFAULT_APP_NAME = 'APP';
// Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together,
// and update package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION
// and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION
export const DEFAULT_ELECTRON_VERSION = '18.0.3';
// https://atom.io/download/atom-shell/index.json

View File

@ -5,3 +5,5 @@ if (process.env.LOGLEVEL) {
} else {
log.disableAll();
}
process.traceDeprecation = true;

401
src/playwright-test.ts Normal file
View File

@ -0,0 +1,401 @@
import { once } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { Shell } from 'electron';
import {
_electron,
ConsoleMessage,
Dialog,
ElectronApplication,
Page,
} from 'playwright';
import { getTempDir } from './helpers/helpers';
import { NativefierOptions } from '../shared/src/options/model';
const INJECT_DIR = path.join(__dirname, '..', 'app', 'inject');
const log = console;
function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
describe('Application launch', () => {
jest.setTimeout(60000);
let app: ElectronApplication;
let appClosed = true;
const appMainJSPath = path.join(__dirname, '..', 'app', 'lib', 'main.js');
const DEFAULT_CONFIG: NativefierOptions = {
targetUrl: 'https://npmjs.com',
};
const logFileDir = getTempDir('playwright');
const metaOrAlt = process.platform === 'darwin' ? 'Meta' : 'Alt';
const metaOrCtrl = process.platform === 'darwin' ? 'Meta' : 'Control';
const spawnApp = async (
playwrightConfig: NativefierOptions = { ...DEFAULT_CONFIG },
awaitFirstWindow = true,
preventNavigation = false,
): Promise<Page | undefined> => {
const consoleListener = (consoleMessage: ConsoleMessage): void => {
const consoleMethods: Record<string, (...args: unknown[]) => unknown> = {
debug: log.debug.bind(console),
error: log.error.bind(console),
info: log.info.bind(console),
log: log.log.bind(console),
trace: log.trace.bind(console),
warn: log.warn.bind(console),
};
Promise.all(consoleMessage.args().map((x) => x.jsonValue()))
.then((args) => {
if (consoleMessage.type() in consoleMethods) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
consoleMethods[consoleMessage.type()]('window.console', args);
} else {
log.log('window.console', args);
}
})
.catch(() => log.log('window.console', consoleMessage));
};
app = await _electron.launch({
args: [appMainJSPath],
env: {
LOG_FILE_DIR: logFileDir,
PLAYWRIGHT_TEST: '1',
PLAYWRIGHT_CONFIG: JSON.stringify(playwrightConfig),
USE_LOG_FILE: '1',
VERBOSE: '1',
},
});
app.on('window', (page: Page) => {
page.on('console', consoleListener);
if (preventNavigation) {
// Prevent page navigation so we can have a reliable test
page
.route('*', (route): void => {
log.info(`Preventing route: ${route.request().url()}`);
route.abort().catch((error) => {
log.error('ERROR', error);
});
})
.catch((error) => {
log.error('ERROR', error);
});
}
});
app.on('close', () => (appClosed = true));
appClosed = false;
if (!awaitFirstWindow) {
return undefined;
}
const window = await app.firstWindow();
// Wait for our initial page to finish loading, otherwise some tests will break
let waited = 0;
while (
window.url() === 'about:blank' &&
playwrightConfig.targetUrl !== 'about:blank' &&
waited < 2000
) {
waited += 100;
await sleep(100);
}
return window;
};
beforeEach(() => {
nukeInjects();
nukeLogs(logFileDir);
});
afterEach(async () => {
if (app && !appClosed) {
await app.close();
}
if (process.env.DEBUG) {
showLogs(logFileDir);
}
});
test('shows an initial window', async () => {
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
expect(app.windows()).toHaveLength(1);
expect(await mainWindow.title()).toBe('npm');
});
test('can inject some CSS', async () => {
const fuschia = 'rgb(255, 0, 255)';
createInject(
'inject.css',
`* { background-color: ${fuschia} !important; }`,
);
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
const headerStyle = await mainWindow.$eval('header', (el) =>
window.getComputedStyle(el),
);
expect(headerStyle.backgroundColor).toBe(fuschia);
await mainWindow.click('#nav-products-link');
await mainWindow.waitForLoadState('domcontentloaded');
const headerStylePostNavigate = await mainWindow.$eval('header', (el) =>
window.getComputedStyle(el),
);
expect(headerStylePostNavigate.backgroundColor).toBe(fuschia);
});
test('can inject some JS', async () => {
const alertMsg = 'hello world from inject';
createInject(
'inject.js',
`setTimeout(() => {alert("${alertMsg}"); }, 5000);`, // Buy ourselves 5 seconds to get the dialog handler setup
);
const mainWindow = (await spawnApp(
{ ...DEFAULT_CONFIG },
true,
true,
)) as Page;
const [dialogPromise] = (await once(
mainWindow,
'dialog',
)) as unknown as Promise<Dialog>[];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const dialog: Dialog = await dialogPromise;
await dialog.dismiss();
expect(dialog.message()).toBe(alertMsg);
});
test('can open internal links', async () => {
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
await mainWindow.click('#nav-products-link');
await mainWindow.waitForLoadState('domcontentloaded');
expect(app.windows()).toHaveLength(1);
});
test('tries to open external links', async () => {
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
// Install the mock first
await app.evaluate(({ shell }: { shell: Shell }) => {
// @ts-expect-error injecting into shell so that this promise
// can be accessed outside of this anonymous function's scope
// Not my favorite thing to do, but I could not find another way
process.openExternalPromise = new Promise((resolve) => {
shell.openExternal = async (url: string): Promise<void> => {
resolve(url);
return Promise.resolve();
};
});
});
// Click, but don't await it - Playwright waits for stuff that does not happen when Electron does openExternal.
mainWindow
.click('#footer > div:nth-child(2) > ul > li:nth-child(2) > a')
.catch((err: unknown) => {
expect(err).toBeUndefined();
});
// Go pull out our value returned by our hacky global promise
const openExternalUrl = await app.evaluate('process.openExternalPromise');
expect(openExternalUrl).not.toBe('https://www.npmjs.com/');
expect(openExternalUrl).not.toBe(DEFAULT_CONFIG.targetUrl);
});
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
test.skip('keyboard shortcuts: zoom', async () => {
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
const defaultZoom: number | undefined = await app.evaluate(
({ BrowserWindow }) =>
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
);
expect(defaultZoom).toBeDefined();
await mainWindow.keyboard.press(`${metaOrCtrl}+Equal`);
const postZoomIn = await app.evaluate(
({ BrowserWindow }): number | undefined =>
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
);
expect(postZoomIn).toBeGreaterThan(defaultZoom as number);
await mainWindow.keyboard.press(`${metaOrCtrl}+0`);
const postZoomReset = await app.evaluate(
({ BrowserWindow }): number | undefined =>
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
);
expect(postZoomReset).toEqual(defaultZoom);
await mainWindow.keyboard.press(`${metaOrCtrl}+Minus`);
const postZoomOut: number | undefined = await app.evaluate(
({ BrowserWindow }) =>
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
);
expect(postZoomOut).toBeLessThan(defaultZoom as number);
});
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
test.skip('keyboard shortcuts: back and forward', async () => {
const mainWindow = (await spawnApp()) as Page;
await mainWindow.waitForLoadState('domcontentloaded');
await Promise.all([
mainWindow.click('#nav-products-link'),
mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' }),
]);
// Go back
// console.log(`${metaOrAlt}+ArrowLeft`);
await mainWindow.keyboard.press(`${metaOrAlt}+ArrowLeft`);
await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' });
const backUrl = await mainWindow.evaluate(() => window.location.href);
expect(backUrl).toBe(DEFAULT_CONFIG.targetUrl);
// Go forward
// console.log(`${metaOrAlt}+ArrowRight`);
await mainWindow.keyboard.press(`${metaOrAlt}+ArrowRight`);
await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' });
const forwardUrl = await mainWindow.evaluate(() => window.location.href);
expect(forwardUrl).not.toBe(DEFAULT_CONFIG.targetUrl);
});
test('no errors thrown in console', async () => {
await spawnApp({ ...DEFAULT_CONFIG }, false);
const mainWindow = await app.firstWindow();
mainWindow.addListener('console', (consoleMessage: ConsoleMessage) => {
try {
expect(consoleMessage.type()).not.toBe('error');
} catch {
// Do it this way so we'll see the whole message, not just
// expect('error').not.toBe('error')
// which isn't particularly useful
expect({
message: 'console.error called unexpectedly with',
consoleMessage,
}).toBeUndefined();
}
});
// Give the app 5 seconds to spin up and ensure no errors happened
await new Promise((resolve) => setTimeout(resolve, 5000));
});
test('basic auth', async () => {
const mainWindow = (await spawnApp({
targetUrl: 'http://httpbin.org/basic-auth/foo/bar',
basicAuthUsername: 'foo',
basicAuthPassword: 'bar',
})) as Page;
await mainWindow.waitForLoadState('networkidle');
const documentText = await mainWindow.evaluate<string>(
'document.documentElement.innerText',
);
const documentJSON = JSON.parse(documentText) as {
authenticated: boolean;
user: string;
};
expect(documentJSON).toEqual({
authenticated: true,
user: 'foo',
});
});
test('basic auth without pre-providing', async () => {
const mainWindow = (await spawnApp({
targetUrl: 'http://httpbin.org/basic-auth/foo/bar',
})) as Page;
await mainWindow.waitForLoadState('load');
// Give the app a few seconds to open the login window
await new Promise((resolve) => setTimeout(resolve, 5000));
const appWindows = app.windows();
expect(appWindows).toHaveLength(2);
const loginWindow = appWindows.filter((x) => x !== mainWindow)[0];
await loginWindow.waitForLoadState('domcontentloaded');
const usernameField = await loginWindow.$('#username-input');
expect(usernameField).not.toBeNull();
const passwordField = await loginWindow.$('#password-input');
expect(passwordField).not.toBeNull();
const submitButton = await loginWindow.$('#submit-form-button');
expect(submitButton).not.toBeNull();
await usernameField?.fill('foo');
await passwordField?.fill('bar');
await submitButton?.click();
await mainWindow.waitForLoadState('networkidle');
const documentText = await mainWindow.evaluate<string>(
'document.documentElement.innerText',
);
const documentJSON = JSON.parse(documentText) as {
authenticated: boolean;
user: string;
};
expect(documentJSON).toEqual({
authenticated: true,
user: 'foo',
});
});
});
function createInject(filename: string, contents: string): void {
fs.writeFileSync(path.join(INJECT_DIR, filename), contents);
}
function nukeInjects(): void {
if (!fs.existsSync(INJECT_DIR)) {
return;
}
const injected = fs
.readdirSync(INJECT_DIR)
.filter((x) => x !== '_placeholder');
injected.forEach((x) => fs.unlinkSync(path.join(INJECT_DIR, x)));
}
function nukeLogs(logFileDir: string): void {
const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log'));
logs.forEach((x) => fs.unlinkSync(path.join(logFileDir, x)));
}
function showLogs(logFileDir: string): void {
const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log'));
for (const logFile of logs) {
log.log(fs.readFileSync(path.join(logFileDir, logFile)).toString());
}
}