Merge branch 'master' into update/new-window-to-setwindowopenhandler

This commit is contained in:
Adam Weeden 2023-08-03 18:36:13 -04:00
commit ba8c4e85cd
54 changed files with 5862 additions and 4588 deletions

1
.env Normal file
View File

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

View File

@ -13,7 +13,7 @@ Please include the following in your new issue:
## Pull Requests
See [here](https://github.com/nativefier/nativefier#development) for instructions on how to set up a development environment.
See [here](https://github.com/nativefier/nativefier/blob/master/HACKING.md) for instructions on how to set up a development environment.
We follow the [Airbnb Style Guide](https://github.com/airbnb/javascript), please make sure tests and lints pass when you submit your pull request.

93
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,93 @@
name: Bug Report
description: File a bug report
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. An incomplete bug report is a useless bug report.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md).
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my bug is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases).
required: true
- type: input
id: nativefier-command
attributes:
label: Nativefier command
description: "Your ***full*** nativefier command, on a ***public*** site."
placeholder: nativefier --verbose --some-option https://mysite.com
validations:
required: true
- type: textarea
id: steps-to-repro
attributes:
label: Steps to reproduce
placeholder: |
1. I did this...
2. And then that...
3. Finally, I clicked here.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
placeholder: What you expected to happen.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
placeholder: What happened instead.
validations:
required: true
- type: textarea
id: debug-info
attributes:
label: Debug info
placeholder: |
- Logs of your full build command, with the `--verbose` flag. Put them in a ```code block``` !
- If the bug happens at app run time, the in-app DevTools console logs (open it with F12)
- Error messages, screenshots, screencasts, anything relevant!
validations:
required: false
- type: input
id: nativefier-version
attributes:
label: Nativefier version
placeholder: "nativefier --version"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
placeholder: "node --version"
validations:
required: true
- type: input
id: npm-version
attributes:
label: npm version
placeholder: "npm --version"
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: "For example: Windows 10 build 1809"
validations:
required: true

View File

@ -1,65 +0,0 @@
---
name: Bug report
about: Report something broken
labels: bug
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible bug report is a useless bug report.
=========================================================
Incomprehensible / incomplete bug reports will be closed.
=========================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/nativefier/nativefier/blob/master/API.md
- [ ] I checked the [Troubleshooting section of the README](https://github.com/nativefier/nativefier/blob/master/README.md#troubleshooting).
- [ ] I searched existing issues, open & closed. Yes, my bug is new.
- [ ] I'm using the latest version available at https://github.com/nativefier/nativefier/releases
**Bug description**
A clear and concise description of what the bug is.
**Steps to reproduce**
Give your ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs too>
```
**Expected behavior**
What you expected to happen.
**Actual behavior**
What happened instead.
**Debug info**
- Console logs of your `nativefier` build command, with `--verbose` flag
- If the bug happens at app run time, the in-app DevTools console logs (open it with F12)
- Error messages
- Screenshots
- Anything else relevant!
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Is it a regression? If yes, what's the last working / first broken version?
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

45
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Feature request
description: Suggest an idea for Nativefier
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request 🙂! Help us help you, **fill this form thoroughly**. An incomplete feature request is a useless feature request.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md), no existing option fits my needs.
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my feature request is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases). Yes, the feature I'm requesting isn't in it.
required: true
validations:
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem statement
description: A clear and concise description of what your feature would be.
placeholder: |
For example:
Nativefier should XYZ, ... details details details...
Existing option --something is not what I want, because ...
validations:
required: true
- type: textarea
id: motivation-and-context
attributes:
label: Motivation & context
placeholder: |
What makes you want this feature?
Where does it come from?
validations:
required: true

View File

@ -1,50 +0,0 @@
---
name: Feature request
about: Suggest an idea for Nativefier
labels: feature-request
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible feature request is a useless feature request.
==============================================================
Incomprehensible / incomplete feature requests will be closed.
==============================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/nativefier/nativefier/blob/master/API.md , no existing option fits my needs.
- [ ] I searched existing issues, open & closed. Yes, my feature request is new.
- [ ] I'm using the latest version available at https://github.com/nativefier/nativefier/releases
**Problem statement**
A clear and concise description of what the problem is. For example: *Nativefier should [...]. I need it because [...]. Existing options [...] are not exactly what I want, because [...]*
If related to a Nativefier config, provide your ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your feature request>
```
**Suggested solution**
A clear and concise description of what you want to happen.
**Alternative solutions**
A clear and concise description of workarounds you've considered/tried.
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

View File

@ -1,54 +0,0 @@
---
name: Question
about: Ask for help
labels: question
---
<!-- Help us help you, and take the time to fill this template 🙂.
An incomprehensible question is a useless question.
=======================================================
Incomprehensible / incomplete questions will be closed.
=======================================================
-->
**Homework**
- [ ] I looked at `nativefier --help` and https://github.com/nativefier/nativefier/blob/master/API.md
- [ ] I searched existing issues, open & closed. Yes, my question is new.
- [ ] I'm using the latest version available at https://github.com/nativefier/nativefier/releases
**Your question**
Your question, expressed clearly and concisely.
**Steps to reproduce**
If you already have a Nativefier command you're struggling with, paste ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your question>
```
**Debug info**
If applicable,
- Console logs of your attempted `nativefier` build command, with `--verbose` flag
- Error messages
- Screenshots
- Anything else relevant!
**Context**
- Nativefier: (for example: 9.1.0)
- Node.js: (for example: 14.6.0)
- Npm: (for example: 6.14.7)
- OS: (for example: Windows 10 build 1809)
- Additional context: (for example: "I'm behind a proxy, with configuration X and protocol Y")

78
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Question
description: Ask for help
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. A cryptic question is a question unlikely to be answered.
- type: checkboxes
id: homework
attributes:
label: Homework
options:
- label: I took the time to write a good, descriptive issue title
required: true
- label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md).
required: true
- label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds.
required: true
- label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my question is new.
required: true
- label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases).
required: true
validations:
required: false
- type: textarea
id: question
attributes:
label: Your question
description: Your question, expressed clearly and concisely.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: "If you already have a Nativefier command you're struggling with, paste ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:"
value: |
```
nativefier --verbose --some-option https://mysite.com
<paste your verbose build logs, if relevant to your question>
```
validations:
required: false
- type: textarea
id: debug-info
attributes:
label: Debug info
placeholder: |
Error messages, screenshots, screencasts, anything relevant!
- type: input
id: nativefier-version
attributes:
label: Nativefier version
placeholder: "nativefier --version"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
placeholder: "node --version"
validations:
required: true
- type: input
id: npm-version
attributes:
label: npm version
placeholder: "npm --version"
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: "For example: Windows 10 build 1809"
validations:
required: true

75
.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,18 +86,40 @@ 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'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"
# ------------------------------------------------------------------------------
printf '\n***** SMOKE TEST 4: Setting up test and building app... *****\n'
tmp_dir=$(mktemp -d -t nativefier-manual-test-get-media-devices)
name='nativefier-smoke-test-4'
node ./lib/cli.js 'https://meet.jit.si/nativefier-test' \
--name "$name" \
"$tmp_dir"
printf '\n***** SMOKE TEST 4: Test checklist *****
- Join the Jitsi meeting and try to share your screen
(third button from the left in the bottom bar)
- An overlay should appear where you can select a screen/window to share
This presently does not work in MacOS as you would have to give the app
"Screen Recording" permissions, but you can''t for an app in the temp directory.
- After selecting a screen, a thumbnail of the shared screen should appear on
the top right
- Console: no Electron runtime deprecation warnings/error logged'
launch_app "$tmp_dir" "$name"
request_feedback "$tmp_dir"

View File

@ -8,22 +8,59 @@ on:
branches:
- master
# - Bumping the *minimum* required Node version? You must bump:
# 1. package.json -> engines.node
# 2. package.json -> devDependencies.@types/node
# 3. tsconfig.json -> {target, lib}
# 4. .github/workflows/ci.yml -> node-version
# - Bumping the *maximum* tested Node version? You must bump also: publish.yml
jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 20
uses: actions/setup-node@v2
with:
node-version: 20
cache: 'npm'
cache-dependency-path: |
npm-shrinkwrap.json
app/npm-shrinkwrap.json
package-lock.json
app/package-lock.json
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run lint
playwright:
runs-on: windows-latest # Doesn't work on headless ubuntu, and is slow on mac
steps:
- uses: actions/checkout@v2
- name: Use Node.js 20
uses: actions/setup-node@v2
with:
node-version: 20
cache: 'npm'
cache-dependency-path: |
npm-shrinkwrap.json
app/npm-shrinkwrap.json
package-lock.json
app/package-lock.json
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:playwright
timeout-minutes: 5
# Useful to debug PlayWright tests failing in CI
# env:
# DEBUG: pw:browser*
tests:
strategy:
matrix:
node-version:
# Align the top Node version here with: 1. linter conditions later below, 2. publish.yml.
- '17'
# Bumping the minimum required Node version? You must bump:
# 1. package.json -> engines.node
# 2. package.json -> devDependencies.@types/node
# 3. tsconfig.json -> {target, lib}
# 4. .github/workflows/ci.yml -> node-version
#
# Here in ci.yml, we want to always run the oldest version we require in
# package.json -> engines.node, to be sure Nativefier runs on this minimum
- '12'
- '20'
- '16' # the oldest we require in package.json -> engines.node, to check we run on this minimum
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
@ -38,9 +75,7 @@ jobs:
app/npm-shrinkwrap.json
package-lock.json
app/package-lock.json
# Will also (through `prepare` hook): 1. install ./app, and 2. build
- 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
- env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:noplaywright

View File

@ -4,26 +4,36 @@ on:
types:
- created
jobs:
playwright:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm
with:
node-version: '20' # Align the version of Node here with ci.yml.
registry-url: 'https://registry.npmjs.org'
- run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:playwright
timeout-minutes: 5
build:
needs: playwright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v1
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm
with:
# Align the version of Node here with ci.yml.
node-version: '17.x'
node-version: '20' # Align the version of Node here with ci.yml.
registry-url: 'https://registry.npmjs.org'
# Will also (through `prepare` hook): 1. install ./app, and 2. build
- run: npm ci --no-fund
- run: npm test
- run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build
- run: npm run test:noplaywright
- run: npm run lint
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
docker:
needs: build
needs: [ playwright, build ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

3
.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
@ -63,3 +65,4 @@ nativefier*.tgz
.actrc
tsconfig.tsbuildinfo
scripts

13
API.md
View File

@ -158,7 +158,8 @@ The processor architecture to target when building.
```
(See https://nodejs.org/api/os.html#os_os_arch)
- Please note: On M1 Macs, unless an arm64 version of brew is used to install nodejs, the version installed will be an `x64` version run through Rosetta, and will result in an `x64` app being generated. If this is not desired, either specify `-a arm64` to build for M1, or re-install node with an arm64 version of brew. See https://github.com/nativefier/nativefier/issues/1089
- Can be overridden by specifying one of: `ia32`, `x64`, `armv7l`, `arm64`.
- Can be overridden by specifying one of: `x64`, `armv7l`, `arm64`, or `universal`
- When specifying `universal` you must be building for the `darwin`, `mas`, `mac`, or `osx` platforms. This will generate a universal (M1 and x86) app.
Note: careful to not conflate _platform_ with _architecture_. If you want for example a Linux or Mac build, it's `--platform` you are looking for. See its documentation for details.
@ -475,7 +476,7 @@ Prevents application from being run multiple times. If such an attempt occurs th
--title-bar-style <value>
```
_[New in 7.6.4]_ (macOS only) Sets the style for the app's title bar. See more details at electron's [Frameless Window](https://github.com/electron/electron/blob/master/docs/api/frameless-window.md#alternatives-on-macos) documentation.
_[New in 7.6.4]_ (macOS only) Sets the style for the app's title bar. See more details at electron's [Frameless Window](https://www.electronjs.org/pt/docs/latest/api/frameless-window) documentation.
Consider injecting a custom CSS (via `--inject`) for better integration. Specifically, the CSS should specify a draggable region. For instance, if the target website has a `<header>` element, you can make it draggable like so.
@ -795,6 +796,8 @@ Current known internal login pages:
- `login.live.com` , `login.microsoftonline.com`
- `okta.com`
- `twitter.com/oauth/authenticate`
- `workspaceair.com`
- `securid.com`
Note: While .com is specified, for most of these we try to match even on non-US
based domains such as `.co.uk` as well
@ -1161,7 +1164,7 @@ const request = {
};
electron.ipcRenderer.send('session-interaction', request);
electon.ipcRenderer.on('session-interaction-reply', (event, result) => {
electron.ipcRenderer.on('session-interaction-reply', (event, result) => {
console.log('session-interaction-reply', event, result.value);
});
```
@ -1194,7 +1197,7 @@ const request = {
};
electron.ipcRenderer.send('session-interaction', request);
electon.ipcRenderer.on('session-interaction-reply', (event, result) => {
electron.ipcRenderer.on('session-interaction-reply', (event, result) => {
console.log('session-interaction-reply', event, result.id, result.value);
});
```
@ -1230,7 +1233,7 @@ const request = {
};
electron.ipcRenderer.send('session-interaction', request);
electon.ipcRenderer.on('session-interaction-reply', (event, result) => {
electron.ipcRenderer.on('session-interaction-reply', (event, result) => {
console.log('session-interaction-reply', event, result);
});
```

View File

@ -9,23 +9,60 @@ Below you'll find a list of build commands contributed by the Nativefier communi
---
## Google apps
## General recipes
(This example documents Google Sheets, but is applicable to other Google apps,
e.g. Google Calendar)
### Videos dont play
Some sites like [HBO Max](https://github.com/nativefier/nativefier/issues/1153) and [Udemy](https://github.com/nativefier/nativefier/issues/1147) host videos using [DRM](https://en.wikipedia.org/wiki/Digital_rights_management).
For those, try passing the [`--widevine`](API.md#widevine) option.
### Settings cached between app rebuilds
You might be surprised to see settings persist after rebuilding your app.
This occurs because the app cache lives separately from the app.
Try deleting your app's cache, found at `<your_app_name_lower_case>-nativefier-<random_id>` in your OSs "App Data" directory (Linux: `$XDG_CONFIG_HOME` or `~/.config` , MacOS: `~/Library/Application Support/` , Windows: `%APPDATA%` or `C:\Users\yourprofile\AppData\Roaming`)
### Window size and position
This allows the last set window size and position to be remembered and applied
after your app is restarted. Note: PR welcome for a built-in fix for that :) .
```sh
nativefier 'https://open.google.com/'
--inject window.js
```
Note: [Inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject)
the following javascript as `windows.js` to prevent the window size and position to reset.
```javascript
function storeWindowPos() {
window.localStorage.setItem('windowX', window.screenX);
window.localStorage.setItem('windowY', window.screenY);
}
window.moveTo(window.localStorage.getItem('windowX'), window.localStorage.getItem('windowY'));
setInterval(storeWindowPos, 250);
```
---
## Site-specific recipes
### Google apps
Lying about the User Agent is required, else Google Login will notice your
"Chrome" isn't a real Chrome, and will: 1. Refuse login, 2. Break notifications.
This example documents Google Sheets, but is applicable to other Google apps,
e.g. Google Calendar, GMail, etc. If `firefox` doesnt work, try `safari` .
```sh
nativefier 'https://docs.google.com/spreadsheets' \
--user-agent firefox
```
Note: lying about the User Agent is required, else Google will notice your
"Chrome" isn't a real Chrome, and will:
1. Refuse login
2. Break notifications
## Outlook
### Outlook
```sh
nativefier 'https://outlook.office.com/mail'
@ -36,7 +73,7 @@ nativefier 'https://outlook.office.com/mail'
Note: `--browserwindow-options` is needed to allow pop-outs when creating/editing an email.
## Udemy
### Udemy
```sh
nativefier 'https://www.udemy.com/'
@ -47,7 +84,7 @@ nativefier 'https://www.udemy.com/'
Note: most videos will work, but to play some DRMed videos you must pass `--widevine` AND [sign the app](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362).
## HBO Max
### HBO Max
```sh
nativefier 'https://play.hbomax.com/'
@ -58,7 +95,7 @@ nativefier 'https://play.hbomax.com/'
Note: as for Udemy, `--widevine` + [app signing](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362) is necessary.
## WhatsApp
### WhatsApp
```sh
nativefier 'https://web.whatsapp.com/'
@ -77,7 +114,30 @@ if ('serviceWorker' in navigator) {
}
```
## Spotify
Another option to see WhatsApp or WhatsApp Business more macOS-like (macos only):
```sh
nativefier https://web.whatsapp.com --name 'WhatsApp Business' --counter true --darwin-dark-mode-support true --title-bar-style hidden --inject whatsappmacos.css
```
with this `whatsappmacos.css` to make the window draggable, and move the user avatar to the right:
```css
header > div:first-child {
flex: 0 0 auto;
margin-right: 15px;
}
div#app > div.os-mac > span:first-child {
position: fixed;
top: 0;
z-index: 1000;
width: 100%;
height: 59px;
pointer-events: none;
-webkit-app-region: drag;
}
```
### Spotify
```sh
nativefier 'https://open.spotify.com/'
@ -150,3 +210,91 @@ a[href='/download'] {
display: none;
}
```
### Notion
You can use Notion pages with Nativefier without much hassle, but Notion itself does not present an easy way to use HTML buttons. As such, if you want to use Notion Pages as a quick way to make dashboards and interactive panels, you will be restricted to only plain links and standard components.
With Nativefier you can now extend Notion's functionality and possibilities by adding HTML buttons that can call other javascript functions, since it enables you to inject custom Javascript and CSS.
```sh
nativefier 'YOUR_NOTION_PAGE_SHARE_URL'
--inject notion.js
--inject notion.css
```
Notes:
- You can inject the notion.js and notion.css files by copying them to the resources/app/inject folder of your nativefier app.
- In your Notion page, use [notionbutton]BUTTON_TEXT|BUTTON_ACTION[/notionbutton], where BUTTON_TEXT is the text contained in your button and BUTTON_ACTION is the action which will be called in your JS function.
```javascript
/* notion.js */
// First, we replace all placeholders in our Notion page to add our interactive buttons to it.
window.onload =
setTimeout(function(){
let htmlCode = document.body.getElementsByTagName("*");
for (let i = 0; i <= htmlCode.length; i++) {
if(htmlCode[i] && htmlCode[i].innerHTML){
let match = htmlCode[i].innerHTML.match(/\[notionbutton\]([\s\S]*?)\[\/notionbutton\]/);
if (match && typeof match == 'object'){
let btnarray = match['1'].split("|");
let btn_text = btnarray[0];
let btn_action = btnarray[1];
htmlCode[i].innerHTML = htmlCode[i].innerHTML.replace(match['0'], "<button class=\"btn-notion\" btnaction=\"" + btn_action + "\" >"+btn_text+"</button>");
}
}
}
let buttons = document.querySelectorAll(".btn-notion");
for (let j=0; j <= buttons.length; j++){
if(buttons[j].hasAttribute("btnaction")){
buttons[j].onclick = function () { runAction(buttons[j].getAttribute("btnaction")) };
}
}
}, 3000);
// And then we define your action below, according to our needs
function runAction(action) {
switch(action){
case '1':
alert('Nice One!');
break;
default:
alert('Hello World!');
}
}
```
After that, set your css file as follows:
```css
.notion-topbar{ /* hiding notion's default navigation bar for a more "app" feeling */
display:none;
}
.btn-notion{ /* defining some style for our buttons */
background-color:#FFC300;
color: #333333;
}
.notion-selectable.notion-page-block.notion-collection-item span{
pointer-events: auto !important; /* notion prevents clicks on items inside databases. Use this to remove that. */
}
```
### Microsoft Teams
You can get an almost macOS look-alike using this:
```sh
nativefier https://teams.microsoft.com --name 'Microsoft Teams' --counter true --darwin-dark-mode-support true --title-bar-style hidden --internal-urls "(.*)" --inject teamsapp.css
```
Note that the `--internal-urls` argument is necessary to login.
Inject the following `teamsapp.css` file to hide the download button at the bottom left and the Office 365 apps waffle button at the top left:
```css
get-app-button.ts-sym.app-bar-link {
display: none;
}
button#ts-waffle-button {
display: none;
}
```

View File

@ -1,8 +1,165 @@
51.0.0 / 2023-08-03
===================
**[BREAKING]**
* Update Electron to 21 + Node to 16 (#1550)
* Update link to Development Guide (#1544)
50.1.1 / 2023-03-27
===================
* Fix shrinkwrap versions back to lockfileVersion 1 (node 12)
* Fix typo "electon" -> "electron" (#1492)
50.1.0 / 2023-03-24
===================
* Update outdated shrinkwrap files
* Add getDisplayMedia and PipeWire support (#1477)
50.0.1 / 2022-11-07
===================
* Windows: Fix "Maximize window visual glitch" (fix #1447) (PR #1448)
* External URL protocols: add zoommtg as no-confirmation (PR #1463)
* CATALOG.md: MS Teams CSS inject (PR #1469), WhatsApp native macOS look CSS (PR #1468)
* Bump default Electron from 19.0.17 to 19.1.4, with security fixe
* CI: test on 12 and **19**, now that 19 is out
* Upgrade CLI & App dependencies
50.0.0 / 2022-09-17
===================
**[BREAKING]** Add validation to opening external URLs in desktop handler (fix #1459)
This will, for security, refuse loading of certain external of two kinds.
One: using dubious URL schemes, two: including nasty characters.
Blocking URLs will be accompanied by a window explaining what's going on,
and linking to a discussion thread where you can report false positives.
Hopefully not _BREAKING_ much (the behavior should now be aligned with
what browsers do), but web weirdness happens. Shout and we'll tweak.
Also,
* Fix double-navigation to pages (fix #1452)
* Upgrade cli+app dependencies
* Bump default Electron to 19.0.17 (from .14), with security fixes
- https://github.com/electron/electron/releases/tag/v19.0.15
- https://github.com/electron/electron/releases/tag/v19.0.16
- https://github.com/electron/electron/releases/tag/v19.0.17
49.0.1 / 2022-08-28
===================
* Bump default Electron to 19.0.14 (from .10), with security fixes
- https://github.com/electron/electron/releases/tag/v19.0.11
- https://github.com/electron/electron/releases/tag/v19.0.12
- https://github.com/electron/electron/releases/tag/v19.0.13
- https://github.com/electron/electron/releases/tag/v19.0.14
* macOS: Move handling of "Universal" apps to electron-packager instead of our own thing (PR #1443)
* Upgrade cli+app dependencies
49.0.0 / 2022-07-30
===================
**[BREAKING]** 49.0.0 doesn't have more breaking changes than 48.0.0, but I'm
releasing a new major release anyway to signal one particularly noteworthy
breaking change in Electron 19 that I failed to pass along to you in 48.0.0:
**The `ia32` arch (a.k.a. `i386` or `x86/32bit`) is no longer supported.**
People still running Nativefier apps on old ia32 machines, feel free to keep
passing a flag `--electron-version 18.x.y` *while it works*. Note however that
we won't be testing it, and future Nativefier versions may depend on upcoming
Electron APIs that will crash your electron18-app-packaged-by-future-Nativefier.
The deprecation is an upstream Electron decision, and there's nothing we will
do about it. Thx @TheCleric for the catch.
Also,
* macOS: Fix "main window cannot be activated" (fix #1415, PR #1417)
* Bump default Electron from 19.0.9 to [19.0.10](https://github.com/electron/electron/releases/tag/v19.0.10)
* Fix loud axios "fetch" warning (https://github.com/nativefier/gitcloud-client/pull/4)
* Fix playwright tests on Linux (#1440)
* Docker: upgraded base node-alpine image from 12 to LTS (currently 16)
48.0.0 / 2022-07-24
===================
* **[BREAKING]** Bump default Electron to 19.0.9 (from 18.3.5)
As usual, we did our best to adapt to Electron breaking changes, but
patches welcome to fix regressions. If unable to submit a patch,
feel free to revert to Nativefier 47.2.1, or pass `-e 18.3.5` for a
_temporary_ downgrade (it will work for a while, but not forever).
Official release notes: https://www.electronjs.org/blog/electron-19-0
Detailed release notes:
- https://github.com/electron/electron/releases/tag/v19.0.0
- https://github.com/electron/electron/releases/tag/v19.0.1
- https://github.com/electron/electron/releases/tag/v19.0.2
- https://github.com/electron/electron/releases/tag/v19.0.3
- https://github.com/electron/electron/releases/tag/v19.0.4
- https://github.com/electron/electron/releases/tag/v19.0.5
- https://github.com/electron/electron/releases/tag/v19.0.6
- https://github.com/electron/electron/releases/tag/v19.0.7
- https://github.com/electron/electron/releases/tag/v19.0.8
- https://github.com/electron/electron/releases/tag/v19.0.9
* CATALOG.md: add a new recipe for using interactive buttons on Notion (PR #1430)
* GitHub Issues: switch from "Issue templates" to new & better "Issue forms" (fix #1258) (PR #1425)
* Maintenance: upgrade Jest, fix PlayWright tests
47.2.1 / 2022-06-27
===================
* macOS: fix incorrect "Back" keyboard shortcut (fix #1426)
* Bump default Electron to 18.3.5 (from 18.3.1), with security fixes:
https://github.com/electron/electron/releases/tag/v18.3.2
https://github.com/electron/electron/releases/tag/v18.3.3
https://github.com/electron/electron/releases/tag/v18.3.4
https://github.com/electron/electron/releases/tag/v18.3.5
* Update dependencies
47.2.0 / 2022-05-30
===================
* Handle `open-url` event: support "deep-linking" e.g. for mailto links (PR #1418, fix #1412)
* Bump default Electron to 18.3.1 (from 18.2.0), with security fixes:
https://github.com/electron/electron/releases/tag/v18.2.1
https://github.com/electron/electron/releases/tag/v18.2.2
https://github.com/electron/electron/releases/tag/v18.2.3
https://github.com/electron/electron/releases/tag/v18.2.4
https://github.com/electron/electron/releases/tag/v18.3.0
https://github.com/electron/electron/releases/tag/v18.3.1
* Update dependencies
* Docs: {API, README, CATALOG}.md cleanups
47.1.3 / 2022-05-02
===================
* Auto-internal URLs: add VMWare Workspace ONE + SecurID (PR #1391, fix #1390)
* `--counter`: accept colon character; useful for time-tracking apps with hour:min in title (PR #1378)
* Windows: correctly set notifications name - not electron.app.YOURAPPNAME (PR #1394)
* macOS: support "universal" architecture (fix #1384 #1398, PR #1386)
* macOS: fix "Open In New Tab" (fix #1260, PR #1385)
* macOS: Change "Paste and Match Style" shortcut to match Apple's HIG guidelines (PR #1387, fix #404)
* macOS: Bump minimum macOS version from 10.9 to 10.10 (see #1404)
This has been effectively been the case since a long time, it was just misdocumented.
Thus, not really a breaking change, and not major-bumping.
* CATALOG.md: add a new "General recipes" section, with one to restore app position/size (PR #1349)
* CI: Add integration testing to the app, using Playwright (PR #1397)
* CI: Speed it up by parallelize tasks
* CI: Bump max tested version of Node for CI/Publish from 17 to 18
* Update dependencies
* Bump default Electron to 18.2.0 (from 18.0.3), with security fixes:
https://github.com/electron/electron/releases/tag/v18.0.4
https://github.com/electron/electron/releases/tag/v18.1.0
https://github.com/electron/electron/releases/tag/v18.2.0
47.0.0 / 2022-04-10
===================
* Bump default Electron to 18.0.3 (from 16.2.2)
* **[BREAKING]** Bump default Electron to 18.0.3 (from 16.2.2)
As usual, we did our best to adapt to Electron breaking changes in 17/18,
but patches welcome to fix regressions. If unable to submit a patch, then
@ -91,7 +248,7 @@
46.0.0 / 2022-01-02
===================
* Upgrade Electron from 13.6.3 & Chrome 91 to 16.0.5 & Chrome 96 (PR #1288)
* **[BREAKING]** Upgrade Electron from 13.6.3 & Chrome 91 to 16.0.5 & Chrome 96 (PR #1288)
We did our best to adapt to [Electron breaking changes](https://www.electronjs.org/docs/latest/breaking-changes) in 14/15/16, but as usual,
patches welcome to address regressions. For detailed release notes, see
- https://github.com/electron/electron/releases/tag/v14.0.0

View File

@ -1,4 +1,4 @@
FROM node:12-alpine
FROM --platform=linux/amd64 node:lts-alpine
LABEL description="Alpine image to build Nativefier apps"
@ -31,8 +31,9 @@ RUN find ./icon-scripts ./src ./app -type f -print0 | xargs -0 dos2unix
# Run tests (to ensure we don't Docker build & publish broken stuff)
# Cleanup leftover files in this step to not waste Docker layer space
# Make sure nativefier is executable
RUN npm link \
&& npm test \
RUN npm i \
&& npm link \
&& npm run test:noplaywright \
&& rm -rf /tmp/nativefier* ~/.npm/_cacache ~/.cache/electron \
&& chmod +x $NPM_PACKAGES/bin/nativefier

View File

@ -124,11 +124,13 @@ When a new major [Electron release](https://github.com/electron/electron/release
1. Wait a few weeks to let it stabilize. Never upgrade Nativefier to a `.0.0`.
2. Thoroughly digest the new version's [breaking changes](https://www.electronjs.org/docs/breaking-changes)
(also via the [Releases page](https://github.com/electron/electron/releases), the content is different),
(also via the [Releases page](https://github.com/electron/electron/releases) and [the blog](https://www.electronjs.org/blog/), the content is different),
grepping our codebase for every changed API.
- If called for by the breaking changes, perform the necessary API changes
3. Bump `src/constants.ts` / `DEFAULT_ELECTRON_VERSION` & `DEFAULT_CHROME_VERSION`
and `app / package.json / devDeps / electron`
3. Bump
- `src/constants.ts` / `DEFAULT_ELECTRON_VERSION` & `DEFAULT_CHROME_VERSION`
- `package.json / devDeps / electron`
- `app / package.json / devDeps / electron`
4. On Windows, macOS, Linux, test for regression and crashes:
1. With `npm test` and `npm run test:manual`
2. With extra manual testing

110
README.md
View File

@ -31,8 +31,8 @@ Whatsapp Web ([HN thread](https://news.ycombinator.com/item?id=10930718)). Nativ
Install Nativefier globally with `npm install -g nativefier` . Requirements:
- macOS 10.9+ / Windows / Linux
- [Node.js](https://nodejs.org/) ≥ 12.9 and npm ≥ 6.9
- macOS 10.13+ / Windows / Linux
- [Node.js](https://nodejs.org/) ≥ 16.9 and npm ≥ 7.10
Optional dependencies:
@ -41,53 +41,52 @@ Optional dependencies:
- [Wine](https://www.winehq.org/) to build Windows apps from non-Windows platforms.
Be sure `wine` is in your `$PATH`.
<details>
<summary>Or install with Docker (click to expand)</summary>
- Pull the image from [Docker Hub](https://hub.docker.com/r/nativefier/nativefier): `docker pull nativefier/nativefier`
- ... or build it yourself: `docker build -t local/nativefier .`
(in this case, replace `nativefier/` in the below examples with `local/`)
By default, `nativefier --help` will be executed.
To build e.g. a Gmail app into `~/nativefier-apps`,
```bash
docker run --rm -v ~/nativefier-apps:/target/ nativefier/nativefier https://mail.google.com/ /target/
```
You can pass Nativefier flags, and mount volumes to pass local files. E.g. to use an icon,
```bash
docker run --rm -v ~/my-icons-folder/:/src -v $TARGET-PATH:/target nativefier/nativefier --icon /src/icon.png --name whatsApp -p linux -a x64 https://web.whatsapp.com/ /target/
```
</details>
<details>
<summary>Or install with Snap & AUR (click to expand)</summary>
These repos are *not* managed by Nativefier maintainers; use at your own risk.
If using them, for your security, please inspect the build script.
- [Snap](https://snapcraft.io/nativefier)
- [AUR](https://aur.archlinux.org/packages/nodejs-nativefier)
</details>
## Usage
To create a desktop app for medium.com, simply `nativefier 'medium.com'`
To create an app for medium.com, simply `nativefier 'medium.com'`
Nativefier will try to determine the app name, and well as lots of other options.
If desired, these options can be overwritten. For example, to override the name,
`nativefier --name 'My Medium App' 'medium.com'`
Nativefier will try to determine the app name, and well as other options that you
can override. For example, to override the name, `nativefier --name 'My Medium App' 'medium.com'`
**Read the [API docs](API.md) or run `nativefier --help`**
to learn about command-line flags usable to configure your app.
to learn about command-line flags and configure your app.
To have high-quality icons used by default for an app/domain, please
contribute to the [icon repository](https://github.com/nativefier/nativefier-icons).
## Troubleshooting
### Catalog
**See [CATALOG.md](CATALOG.md) for site-specific ideas & workarounds contributed by the community**.
See [CATALOG.md](CATALOG.md) for build commands & workarounds contributed by the community.
## Docker
Nativefier is also usable from Docker:
- Pull the image from [Docker Hub](https://hub.docker.com/r/nativefier/nativefier): `docker pull nativefier/nativefier`
- ... or build it yourself: `docker build -t local/nativefier .`
(in this case, replace `nativefier/` in the below examples with `local/`)
By default, `nativefier --help` will be executed.
To build e.g. a Gmail app into `~/nativefier-apps`,
```bash
docker run --rm -v ~/nativefier-apps:/target/ nativefier/nativefier https://mail.google.com/ /target/
```
You can pass Nativefier flags, and mount volumes to pass local files. E.g. to use an icon,
```bash
docker run --rm -v ~/my-icons-folder/:/src -v $TARGET-PATH:/target nativefier/nativefier --icon /src/icon.png --name whatsApp -p linux -a x64 https://web.whatsapp.com/ /target/
```
## Unofficial repositories
Nativefier is also available in various user-contributed software repos.
These are *not* managed by Nativefier maintainers; use at your own risk.
If using them, for your security, please inspect the build script.
- [Snap](https://snapcraft.io/nativefier)
- [AUR](https://aur.archlinux.org/packages/nodejs-nativefier)
If this doesnt help, go look at our [issue tracker](https://github.com/nativefier/nativefier/issues).
## Development
@ -97,31 +96,4 @@ Help welcome on [bugs](https://github.com/nativefier/nativefier/issues?q=is%3Aop
Docs: [Developer / build / hacking](HACKING.md), [API / flags](API.md),
[Changelog](CHANGELOG.md).
## License
[MIT](LICENSE.md).
## Troubleshooting
Generally, see [CATALOG.md](CATALOG.md) for ideas & workarounds, and search in
[existing issues](https://github.com/nativefier/nativefier/issues).
### Old/unsupported browser
Some sites intentionally block Nativefier (or similar) apps, e.g. [Google](https://github.com/nativefier/nativefier/issues/831) and [WhatsApp](https://github.com/nativefier/nativefier/issues/1112).
First, try setting the [`--user-agent`](https://github.com/nativefier/nativefier/blob/master/API.md#user-agent) to `firefox` or `safari`.
If still broken, see [CATALOG.md](CATALOG.md) + existing issues.
### Videos won't play
This issue comes up for certain sites like [HBO Max](https://github.com/nativefier/nativefier/issues/1153) and [Udemy](https://github.com/nativefier/nativefier/issues/1147).
First, try [`--widevine`](API.md#widevine).
If still broken, see [CATALOG.md](CATALOG.md) + existing issues.
### Settings cached between app rebuilds
This can occur because app cache lives separate from the app.
Try delete your app's cache, which is found at `<your_app_name_lower_case>-nativefier-<random_id>` in your OS's "App Data" directory (for Linux: `$XDG_CONFIG_HOME` or `~/.config` , for MacOS: `~/Library/Application Support/` , for Windows: `%APPDATA%` or `C:\Users\yourprofile\AppData\Roaming`)
License: [MIT](LICENSE.md).

762
app/npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,6 @@
"source-map-support": "^0.5.19"
},
"devDependencies": {
"electron": "^18.0.3"
"electron": "^21.4.4"
}
}

View File

@ -1,9 +1,14 @@
import { BrowserWindow, ContextMenuParams } from 'electron';
import {
BrowserWindow,
ContextMenuParams,
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 { createNewTab, createNewWindow } from '../helpers/windowHelpers';
import { createNewWindow } from '../helpers/windowHelpers';
import {
OutputOptions,
outputOptionsToWindowOptions,
@ -13,7 +18,7 @@ export function initContextMenu(
options: OutputOptions,
window?: BrowserWindow,
): void {
log.debug('initContextMenu', { options, window });
log.debug('initContextMenu');
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
contextMenu({
@ -43,12 +48,25 @@ export function initContextMenu(
items.push({
label: 'Open Link in New Tab',
click: () =>
createNewTab(
outputOptionsToWindowOptions(options),
setupNativefierWindow,
// Fire a new window event for a foreground tab
// Previously we called createNewTab directly, but it had incosistent and buggy behavior
// as it was mostly designed for running off of events. So this will create a new event
// for a foreground-tab for the event handler to grab and take care of instead.
(window as BrowserWindow).webContents.emit(
// event name
'new-window',
// event object
{
// Leave to the default for a NewWindowWebContentsEvent
newGuest: undefined,
...new Event('new-window'),
} as NewWindowWebContentsEvent,
// url
params.linkURL,
true,
window,
// frameName
window?.webContents.mainFrame.name ?? '',
// disposition
'foreground-tab',
),
});
}

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,
@ -19,6 +19,7 @@ export async function createLoginWindow(
webPreferences: {
nodeIntegration: true, // TODO work around this; insecure
contextIsolation: false, // https://github.com/electron/electron/issues/28017
sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default
},
});
await loginWindow.loadURL(

View File

@ -1,16 +1,25 @@
import * as fs from 'fs';
import * as path from 'path';
import { ipcMain, BrowserWindow, Event, HandlerDetails } from 'electron';
import {
desktopCapturer,
ipcMain,
BrowserWindow,
Event,
HandlerDetails,
} 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 +27,6 @@ import {
getDefaultWindowOptions,
hideWindow,
} from '../helpers/windowHelpers';
import { initContextMenu } from './contextMenu';
import { createMenu } from './menu';
import {
OutputOptions,
outputOptionsToWindowOptions,
@ -72,11 +79,19 @@ export async function createMainWindow(
// Whether the window should always stay on top of other windows. Default is false.
alwaysOnTop: options.alwaysOnTop,
titleBarStyle: options.titleBarStyle ?? 'default',
show: options.tray !== 'start-in-tray',
// Maximize window visual glitch on Windows fix
// We want a consistent behavior on all OSes, but Windows needs help to not glitch.
// So, we manually mainWindow.show() later, see a few lines below
show: options.tray !== 'start-in-tray' && process.platform !== 'win32',
backgroundColor: options.backgroundColor,
...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
@ -88,6 +103,9 @@ export async function createMainWindow(
if (options.tray === 'start-in-tray') {
mainWindow.hide();
} else if (process.platform === 'win32') {
// See other "Maximize window visual glitch on Windows fix" comment above.
mainWindow.show();
}
const windowOptions = outputOptionsToWindowOptions(options);
@ -128,15 +146,12 @@ export async function createMainWindow(
});
setupSessionInteraction(options, mainWindow);
setupSessionPermissionHandler(mainWindow);
if (options.clearCache) {
await clearCache(mainWindow);
}
if (options.targetUrl) {
await mainWindow.loadURL(options.targetUrl);
}
setupCloseEvent(options, mainWindow);
return mainWindow;
@ -207,6 +222,22 @@ function setupCounter(
});
}
function setupSessionPermissionHandler(window: BrowserWindow): void {
window.webContents.session.setPermissionCheckHandler(() => {
return true;
});
window.webContents.session.setPermissionRequestHandler(
(_webContents, _permission, callback) => {
callback(true);
},
);
ipcMain.handle('desktop-capturer-get-sources', () => {
return desktopCapturer.getSources({
types: ['screen', 'window'],
});
});
}
function setupNotificationBadge(
options: OutputOptions,
window: BrowserWindow,

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,
@ -45,7 +45,7 @@ export function createMenu(
options: OutputOptions,
mainWindow: BrowserWindow,
): void {
log.debug('createMenu', { options, mainWindow });
log.debug('createMenu', { options });
const menuTemplate = generateMenu(options, mainWindow);
injectBookmarks(menuTemplate);
@ -115,7 +115,10 @@ export function generateMenu(
},
{
label: 'Paste and Match Style',
accelerator: 'CmdOrCtrl+Shift+V',
// https://github.com/nativefier/nativefier/issues/404
// Apple's HIG lists this shortcut for paste and match style
// https://support.apple.com/en-us/HT209651
accelerator: isOSX() ? 'Option+Shift+Cmd+V' : 'Ctrl+Shift+V',
role: 'pasteAndMatchStyle',
},
{
@ -150,7 +153,7 @@ export function generateMenu(
submenu: [
{
label: 'Back',
accelerator: isOSX() ? 'CmdOrAlt+Left' : 'Alt+Left',
accelerator: isOSX() ? 'Cmd+Left' : 'Alt+Left',
click: goBack,
},
{

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

@ -1,9 +1,14 @@
import { shell } from 'electron';
jest.mock('./windowHelpers');
import {
linkIsInternal,
getCounterValue,
removeUserAgentSpecifics,
cleanupPlainText,
getCounterValue,
linkIsInternal,
openExternal,
removeUserAgentSpecifics,
} from './helpers';
import { showNavigationBlockedMessage } from './windowHelpers';
const internalUrl = 'https://medium.com/';
const internalUrlWww = 'https://www.medium.com/';
@ -200,6 +205,8 @@ const testLoginPages = [
'https://appleid.apple.com/auth/authorize',
'https://id.atlassian.com',
'https://auth.atlassian.com',
'https://vmware.workspaceair.com',
'https://vmware.auth.securid.com',
];
test.each(testLoginPages)(
@ -283,3 +290,59 @@ describe('cleanupPlainText', () => {
expect(cleanupPlainText(' this is a test ')).toBe('this is a test');
});
});
describe('openExternal', () => {
const mockShellOpenExternal: jest.SpyInstance = jest.spyOn(
shell,
'openExternal',
);
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
beforeEach(() => {
mockShellOpenExternal.mockReset();
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
});
afterAll(() => {
mockShellOpenExternal.mockRestore();
mockShowNavigationBlockedMessage.mockRestore();
});
test('https urls scheme should *not* be blocked', async () => {
await openExternal('https://whatever.foo');
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockShellOpenExternal).toHaveBeenCalled();
});
test('urls with whitelisted scheme should *not* be blocked', async () => {
await openExternal('ircs://irc.libera.chat/whatever');
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockShellOpenExternal).toHaveBeenCalled();
});
test('urls with non-allowlisted scheme *should* be blocked', async () => {
await openExternal('barf://whatever.foo');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
test('potentially-malicious urls *should* be blocked', async () => {
await openExternal('https://hello.com/wor%00ld');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
test('malformed urls *should* be blocked', async () => {
await openExternal('zombocom');
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockShellOpenExternal).not.toHaveBeenCalled();
});
});

View File

@ -3,10 +3,104 @@ 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';
import { showNavigationBlockedMessage } from './windowHelpers';
export const INJECT_DIR = path.join(__dirname, '..', 'inject');
/**
* Firefox's list of protocols for which opening an external handler is allowed without confirmation.
* Taken from Firefox's. Location might vary in codebase, search for one of them, e.g.
* https://searchfox.org/mozilla-central/search?q=%22xmpp%22&path=&case=false&regexp=false
*/
const URL_PROTOCOLS_NOCONFIRMATION_FIREFOX = [
'bitcoin:',
'ftp:',
'ftps:',
'geo:',
'im:',
'irc:',
'ircs:',
'magnet:',
'mailto:',
'matrix:',
'mms:',
'news:',
'nntp:',
'openpgp4fpr:',
'sftp:',
'sip:',
'sms:',
'smsto:',
'ssh:',
'tel:',
'urn:',
'webcal:',
'wtai:',
'xmpp:',
];
/**
* Our extension to Firefox's list. If extending this list too much, we should
* really add a confirmation modal (for now we just block), like browsers do.
* But for now, since nobody shouts at us for bluntly blocking anything else,
* let's keep rolling with it.
*/
const URL_PROTOCOLS_NOCONFIRMATION_EXTRA = ['zoommtg:'];
/**
* List of protocols for which opening an external handler is allowed without confirmation.
* Note: "without confirmation" is currently a lie. It was implemented this way
* as a way to know from user feedback what protocols would cause users to shout,
* but there wasn't much shouting happening, so we currently don't have a confirmation
* mechanism, we just bluntly block. That might need to change at some point.
*/
const URL_PROTOCOLS_NOCONFIRMATION = [
'http:',
'https:',
...URL_PROTOCOLS_NOCONFIRMATION_FIREFOX,
...URL_PROTOCOLS_NOCONFIRMATION_EXTRA,
];
const SHELL_SAFETY_FEEDBACK_STR =
'If you believe this URL should open, you might be right, and our validation might be excessive.' +
'Please share this error & URL at https://github.com/nativefier/nativefier/issues/1459';
export function isUrlShellSafe(
urlToGo: string,
): { blocked: false } | { blocked: true; reason: string } {
let url: URL;
try {
url = new URL(urlToGo.toLowerCase());
} catch (err: unknown) {
return {
blocked: true,
reason: `URL appears malformed. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
}
if (!URL_PROTOCOLS_NOCONFIRMATION.includes(url.protocol)) {
return {
blocked: true,
reason: `URL protocol is disallowed. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
}
// https://cwe.mitre.org/data/definitions/177.html
if (
urlToGo.includes('%00') ||
urlToGo.includes('%0a') ||
urlToGo.includes('%2e') ||
urlToGo.includes('%2f') ||
urlToGo.includes('%5c')
) {
return {
blocked: true,
reason: `URL might be malicious. ${SHELL_SAFETY_FEEDBACK_STR}`,
};
}
return { blocked: false };
}
/**
* Helper to print debug messages from the main process in the browser window
*/
@ -106,6 +200,8 @@ function isInternalLoginPage(url: string): boolean {
'twitter\\.[a-zA-Z\\.]*/oauth/authenticate', // Twitter
'appleid\\.apple\\.com/auth/authorize', // Apple
'(?:id|auth)\\.atlassian\\.[a-zA-Z]+', // Atlassian
'.*\\.workspaceair\\.com', // VMWare Workspace One SSO
'.*\\.securid\\.com', // SecurID for VMWare Workspace One SSO
];
// Making changes? Remember to update the tests in helpers.test.ts and in API.md
const regex = RegExp(internalLoginPagesArray.join('|'));
@ -163,14 +259,41 @@ export function nativeTabsSupported(): boolean {
return isOSX();
}
/**
* Open the given external protocol URL in the desktop's default manner
* (e.g. `mailto:` URLs in the user's default mail agent), with extra validation.
*/
export function openExternal(
url: string,
options?: OpenExternalOptions,
): Promise<void> {
log.debug('openExternal', { url, options });
const urlShellSafety = isUrlShellSafe(url);
log.debug('openExternal', { url, options, urlShellSafety });
if (urlShellSafety.blocked) {
return new Promise((resolve) => {
showNavigationBlockedMessage(
`Navigation blocked to ${url}\n\n${urlShellSafety.reason}`,
)
.then(() => resolve())
.catch((err: unknown) => {
throw err;
});
});
}
return shell.openExternal(url, options);
}
// Copy-pastaed as unable to get imports to work in preload.
// If modifying, update also app/src/preload.ts
export function isWayland(): boolean {
return (
isLinux() &&
(Boolean(process.env.WAYLAND_DISPLAY) ||
process.env.XDG_SESSION_TYPE === 'wayland')
);
}
export function removeUserAgentSpecifics(
userAgentFallback: string,
appName: string,

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

@ -29,7 +29,7 @@ const {
onWillPreventUnload: (event: unknown) => void;
} = jest.requireActual('./windowEvents');
import {
blockExternalURL,
showNavigationBlockedMessage,
createAboutBlankWindow,
createNewTab,
} from './windowHelpers';
@ -47,7 +47,8 @@ describe('onNewWindowHelper', () => {
targetUrl: originalURL,
zoom: 1.0,
};
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
const mockCreateAboutBlank: jest.SpyInstance =
createAboutBlankWindow as jest.Mock;
const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock;
@ -60,7 +61,7 @@ describe('onNewWindowHelper', () => {
const setupWindow = jest.fn();
beforeEach(() => {
mockBlockExternalURL
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
mockCreateAboutBlank.mockReset();
@ -72,7 +73,7 @@ describe('onNewWindowHelper', () => {
});
afterAll(() => {
mockBlockExternalURL.mockRestore();
mockShowNavigationBlockedMessage.mockRestore();
mockCreateAboutBlank.mockRestore();
mockCreateNewTab.mockRestore();
mockLinkIsInternal.mockRestore();
@ -87,7 +88,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
@ -101,7 +102,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(result.action).toEqual('deny');
});
@ -118,7 +119,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
@ -131,7 +132,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
@ -146,7 +147,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(result.action).toEqual('deny');
});
@ -168,7 +169,7 @@ describe('onNewWindowHelper', () => {
true,
undefined,
);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
@ -190,7 +191,7 @@ describe('onNewWindowHelper', () => {
false,
undefined,
);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
@ -202,7 +203,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
@ -214,7 +215,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('deny');
});
@ -226,7 +227,7 @@ describe('onNewWindowHelper', () => {
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
expect(mockCreateNewTab).not.toHaveBeenCalled();
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(result.action).toEqual('allow');
});
@ -237,13 +238,14 @@ describe('onWillNavigate', () => {
const internalURL = 'https://medium.com/topics/technology';
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
const mockBlockExternalURL: jest.SpyInstance = blockExternalURL as jest.Mock;
const mockShowNavigationBlockedMessage: jest.SpyInstance =
showNavigationBlockedMessage as jest.Mock;
const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock;
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
const preventDefault = jest.fn();
beforeEach(() => {
mockBlockExternalURL
mockShowNavigationBlockedMessage
.mockReset()
.mockReturnValue(Promise.resolve(undefined));
mockLinkIsInternal.mockReset().mockReturnValue(false);
@ -252,7 +254,7 @@ describe('onWillNavigate', () => {
});
afterAll(() => {
mockBlockExternalURL.mockRestore();
mockShowNavigationBlockedMessage.mockRestore();
mockLinkIsInternal.mockRestore();
mockOpenExternal.mockRestore();
});
@ -266,7 +268,7 @@ describe('onWillNavigate', () => {
const event = { preventDefault };
await onWillNavigate(options, event, internalURL);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).not.toHaveBeenCalled();
});
@ -279,12 +281,12 @@ describe('onWillNavigate', () => {
const event = { preventDefault };
await onWillNavigate(options, event, externalURL);
expect(mockBlockExternalURL).not.toHaveBeenCalled();
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(preventDefault).toHaveBeenCalledTimes(1);
});
test('external urls should be ignored if blockExternalUrls is true', async () => {
test('external urls should be blocked if blockExternalUrls is true', async () => {
const options = {
blockExternalUrls: true,
targetUrl: originalURL,
@ -292,7 +294,7 @@ describe('onWillNavigate', () => {
const event = { preventDefault };
await onWillNavigate(options, event, externalURL);
expect(mockBlockExternalURL).toHaveBeenCalledTimes(1);
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
expect(mockOpenExternal).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledTimes(1);
});

View File

@ -5,18 +5,18 @@ import {
WebContents,
HandlerDetails,
} 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,
createNewTab,
injectCSS,
sendParamsOnDidFinishLoad,
setProxyRules,
showNavigationBlockedMessage,
} from './windowHelpers';
import { WindowOptions } from '../../../shared/src/options/model';
type NewWindowHandlerResult = ReturnType<
Parameters<WebContents['setWindowOpenHandler']>[0]
@ -54,15 +54,24 @@ export function onNewWindowHelper(
)
) {
if (options.blockExternalUrls) {
blockExternalURL(details.url).catch((err: unknown) => {
log.error('blockExternalURL', err);
});
showNavigationBlockedMessage(
`Navigation to external URL blocked by options: ${details.url}`,
)
.then(() => {
// blockExternalURL(details.url).then(resolve).catch((err: unknown) => {
// log.error('blockExternalURL', err);
// });
})
.catch((err: unknown) => {
throw err;
});
return { action: 'deny' };
} else {
openExternal(details.url).catch((err: unknown) => {
log.error('openExternal', err);
});
return { action: 'deny' };
}
return { action: 'deny' };
}
// Normally the following would be:
// if (urlToGo.startsWith('about:blank'))...
@ -93,7 +102,7 @@ export function onWillNavigate(
event: Event,
urlToGo: string,
): Promise<void> {
log.debug('onWillNavigate', { options, event, urlToGo });
log.debug('onWillNavigate', urlToGo);
if (
!linkIsInternal(
options.targetUrl,
@ -105,7 +114,9 @@ export function onWillNavigate(
event.preventDefault();
if (options.blockExternalUrls) {
return new Promise((resolve) => {
blockExternalURL(urlToGo)
showNavigationBlockedMessage(
`Navigation to external URL blocked by options: ${urlToGo}`,
)
.then(() => resolve())
.catch((err: unknown) => {
throw err;

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;
@ -22,12 +23,14 @@ export function adjustWindowZoom(adjustment: number): void {
});
}
export function blockExternalURL(url: string): Promise<MessageBoxReturnValue> {
export function showNavigationBlockedMessage(
message: string,
): Promise<MessageBoxReturnValue> {
return new Promise((resolve, reject) => {
withFocusedWindow((focusedWindow) => {
dialog
.showMessageBox(focusedWindow, {
message: `Cannot navigate to external URL: ${url}`,
message,
type: 'error',
title: 'Navigation blocked',
})
@ -146,6 +149,7 @@ export function getDefaultWindowOptions(
nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com
preload: path.join(__dirname, 'preload.js'),
plugins: true,
sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default
webSecurity: !options.insecure,
zoomFactor: options.zoom,
// `contextIsolation` was switched to true in Electron 12, which:

View File

@ -12,39 +12,49 @@ 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 { setupNativefierWindow } from './helpers/windowEvents';
import {
OutputOptions,
outputOptionsToWindowOptions,
} from '../../shared/src/options/model';
isOSX,
isWayland,
isWindows,
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 { OutputOptions } from '../../shared/src/options/model';
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
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
@ -69,9 +79,18 @@ if (!appArgs.userAgentHonest) {
}
}
// this step is required to allow app names to be displayed correctly in notifications on windows
// https://www.electronjs.org/docs/latest/api/app#appsetappusermodelidid-windows
// https://www.electronjs.org/docs/latest/tutorial/notifications#windows
if (isWindows()) {
app.setAppUserModelId(app.getName());
}
const urlArgv = process.argv.filter((a) => a.startsWith('http'));
// Take in a URL on the command line as an override
if (process.argv.length > 1) {
const maybeUrl = process.argv[1];
if (urlArgv.length > 0) {
const maybeUrl = urlArgv[0];
try {
new URL(maybeUrl);
appArgs.targetUrl = maybeUrl;
@ -99,18 +118,23 @@ const fileDownloadOptions = { ...appArgs.fileDownloadOptions };
electronDownload(fileDownloadOptions);
if (appArgs.processEnvs) {
let processEnvs: Record<string, string> =
appArgs.processEnvs as unknown as Record<string, string>;
// This is compatibility if just a string was passed.
if (typeof appArgs.processEnvs === 'string') {
process.env.processEnvs = appArgs.processEnvs;
} else {
Object.keys(appArgs.processEnvs)
.filter((key) => key !== undefined)
.forEach((key) => {
// @ts-expect-error TS will complain this could be undefined, but we filtered those out
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
process.env[key] = appArgs.processEnvs[key];
});
try {
processEnvs = JSON.parse(appArgs.processEnvs) as Record<string, string>;
} catch {
// This wasn't JSON. Fall back to the old code
processEnvs = {};
process.env.processEnvs = appArgs.processEnvs;
}
}
Object.keys(processEnvs)
.filter((key) => key !== undefined)
.forEach((key) => {
process.env[key] = processEnvs[key];
});
}
if (typeof appArgs.flashPluginDir === 'string') {
@ -157,6 +181,10 @@ if (appArgs.basicAuthPassword) {
);
}
if (isWayland()) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
}
if (appArgs.lang) {
const langParts = appArgs.lang.split(',');
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
@ -182,7 +210,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();
}
});
@ -212,6 +240,15 @@ app.on('will-finish-launching', () => {
log.debug('app.will-finish-launching');
});
app.on('open-url', (event, url) => {
log.debug('app.open-url', { event, url });
event.preventDefault();
if (mainWindow) {
mainWindow.webContents.send('open-url', url);
}
});
if (appArgs.widevine) {
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
app.on('widevine-ready', (version: string, lastVersion: string) => {
@ -243,7 +280,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();
@ -383,6 +420,10 @@ async function onReady(): Promise<void> {
})
.catch((err) => log.error('dialog.showMessageBox ERROR', err));
}
if (appArgs.targetUrl) {
await mainWindow.loadURL(appArgs.targetUrl);
}
}
app.on(
@ -402,15 +443,14 @@ app.on(
},
);
app.on('browser-window-blur', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-blur', { event, window });
app.on('browser-window-blur', () => {
log.debug('app.browser-window-blur');
});
app.on('browser-window-created', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-created', { event, window });
setupNativefierWindow(outputOptionsToWindowOptions(appArgs), window);
app.on('browser-window-created', () => {
log.debug('app.browser-window-created');
});
app.on('browser-window-focus', (event: Event, window: BrowserWindow) => {
log.debug('app.browser-window-focus', { event, window });
app.on('browser-window-focus', () => {
log.debug('app.browser-window-focus');
});

View File

@ -144,10 +144,17 @@ class MockWebRequest {
class InternalEmitter extends EventEmitter {}
const mockShell = {
openExternal(url: string, options?: unknown): Promise<void> {
return new Promise((resolve) => resolve());
},
};
export {
MockDialog as dialog,
MockBrowserWindow as BrowserWindow,
MockSession as Session,
MockWebContents as WebContents,
MockWebRequest as WebRequest,
mockShell as shell,
};

View File

@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ipcRenderer } from 'electron';
@ -61,6 +62,235 @@ function setNotificationCallback(
window.Notification = newNotify;
}
async function getDisplayMedia(
sourceId: number | string,
): Promise<MediaStream> {
type OriginalVideoPropertyType = boolean | MediaTrackConstraints | undefined;
if (!window?.navigator?.mediaDevices) {
throw Error('window.navigator.mediaDevices is not present');
}
// Electron supports an outdated specification for mediaDevices,
// see https://www.electronjs.org/docs/latest/api/desktop-capturer/
const stream = await window.navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
},
} as unknown as OriginalVideoPropertyType,
});
return stream;
}
function setupScreenSharePickerStyles(id: string): void {
const screenShareStyles = document.createElement('style');
screenShareStyles.id = id;
screenShareStyles.innerHTML = `
.desktop-capturer-selection {
--overlay-color: hsla(0, 0%, 11.8%, 0.75);
--highlight-color: highlight;
--text-content-color: #fff;
--selection-button-color: hsl(180, 1.3%, 14.7%);
}
.desktop-capturer-selection {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: var(--overlay-color);
color: var(--text-content-color);
z-index: 10000000;
display: flex;
align-items: center;
justify-content: center;
}
.desktop-capturer-selection__close {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
padding: 1rem;
color: inherit;
position: absolute;
left: 1rem;
top: 1rem;
cursor: pointer;
}
.desktop-capturer-selection__scroller {
width: 100%;
max-height: 100vh;
overflow-y: auto;
}
.desktop-capturer-selection__list {
max-width: calc(100% - 100px);
margin: 50px;
padding: 0;
display: flex;
flex-wrap: wrap;
list-style: none;
overflow: hidden;
justify-content: center;
}
.desktop-capturer-selection__item {
display: flex;
margin: 4px;
}
.desktop-capturer-selection__btn {
display: flex;
flex-direction: column;
align-items: stretch;
width: 145px;
margin: 0;
border: 0;
border-radius: 3px;
padding: 4px;
background: var(--selection-button-color);
text-align: left;
transition: background-color .15s, box-shadow .15s;
}
.desktop-capturer-selection__btn:hover,
.desktop-capturer-selection__btn:focus {
background: var(--highlight-color);
}
.desktop-capturer-selection__thumbnail {
width: 100%;
height: 81px;
object-fit: cover;
}
.desktop-capturer-selection__name {
margin: 6px 0 6px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
@media (prefers-color-scheme: light) {
.desktop-capturer-selection {
--overlay-color: hsla(0, 0%, 90.2%, 0.75);
--text-content-color: hsl(0, 0%, 12.9%);
--selection-button-color: hsl(180, 1.3%, 85.3%);
}
}`;
document.head.appendChild(screenShareStyles);
}
function setupScreenSharePickerElement(
id: string,
sources: Electron.DesktopCapturerSource[],
): void {
const selectionElem = document.createElement('div');
selectionElem.classList.add('desktop-capturer-selection');
selectionElem.id = id;
selectionElem.innerHTML = `
<button class="desktop-capturer-selection__close" id="${id}-close" aria-label="Close screen share picker" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">
<path fill="currentColor" d="m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/>
</svg>
</button>
<div class="desktop-capturer-selection__scroller">
<ul class="desktop-capturer-selection__list">
${sources
.map(
({ id, name, thumbnail }) => `
<li class="desktop-capturer-selection__item">
<button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
<img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
<span class="desktop-capturer-selection__name">${name}</span>
</button>
</li>
`,
)
.join('')}
</ul>
</div>
`;
document.body.appendChild(selectionElem);
}
function setupScreenSharePicker(
resolve: (value: MediaStream | PromiseLike<MediaStream>) => void,
reject: (reason?: unknown) => void,
sources: Electron.DesktopCapturerSource[],
): void {
const baseElementsId = 'native-screen-share-picker';
const pickerStylesElementId = baseElementsId + '-styles';
setupScreenSharePickerElement(baseElementsId, sources);
setupScreenSharePickerStyles(pickerStylesElementId);
const clearElements = (): void => {
document.getElementById(pickerStylesElementId)?.remove();
document.getElementById(baseElementsId)?.remove();
};
document
.getElementById(`${baseElementsId}-close`)
?.addEventListener('click', () => {
clearElements();
reject('Screen share was cancelled by the user.');
});
document
.querySelectorAll('.desktop-capturer-selection__btn')
.forEach((button) => {
button.addEventListener('click', () => {
const id = button.getAttribute('data-id');
if (!id) {
log.error("Couldn't find `data-id` of element");
clearElements();
return;
}
const source = sources.find((source) => source.id === id);
if (!source) {
log.error(`Source with id "${id}" does not exist`);
clearElements();
return;
}
getDisplayMedia(source.id)
.then((stream) => {
resolve(stream);
})
.catch((err) => {
log.error('Error selecting desktop capture source:', err);
reject(err);
})
.finally(() => {
clearElements();
});
});
});
}
function setDisplayMediaPromise(): void {
// Since no implementation for `getDisplayMedia` exists in Electron we write our own.
if (!window?.navigator?.mediaDevices) {
return;
}
window.navigator.mediaDevices.getDisplayMedia = (): Promise<MediaStream> => {
return new Promise((resolve, reject) => {
const sources = ipcRenderer.invoke(
'desktop-capturer-get-sources',
) as Promise<Electron.DesktopCapturerSource[]>;
sources
.then(async (sources) => {
if (isWayland()) {
// No documentation is provided wether the first element is always PipeWire-picked or not
// i.e. maybe it's not deterministic, we are only taking a guess here.
const stream = await getDisplayMedia(sources[0].id);
resolve(stream);
} else {
setupScreenSharePicker(resolve, reject, sources);
}
})
.catch((err) => {
reject(err);
});
});
};
}
function injectScripts(): void {
const needToInject = fs.existsSync(INJECT_DIR);
if (!needToInject) {
@ -95,13 +325,28 @@ function notifyNotificationClick(): void {
// @ts-expect-error TypeScript thinks these are incompatible but they aren't
setNotificationCallback(notifyNotificationCreate, notifyNotificationClick);
setDisplayMediaPromise();
ipcRenderer.on('params', (event, message: string) => {
log.debug('ipcRenderer.params', { event, message });
const appArgs = JSON.parse(message) as OutputOptions;
const appArgs: unknown = JSON.parse(message) as OutputOptions;
log.info('nativefier.json', appArgs);
});
ipcRenderer.on('debug', (event, message: string) => {
log.debug('ipcRenderer.debug', { event, message });
});
// Copy-pastaed as unable to get imports to work in preload.
// If modifying, update also app/src/helpers/helpers.ts
function isWayland(): boolean {
return (
isLinux() &&
(Boolean(process.env.WAYLAND_DISPLAY) ||
process.env.XDG_SESSION_TYPE === 'wayland')
);
}
function isLinux(): boolean {
return os.platform() === 'linux';
}

7205
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,14 @@
{
"name": "nativefier",
"version": "47.0.0",
"version": "51.0.0",
"description": "Wrap web apps natively",
"license": "MIT",
"author": "Goh Jia Hao",
"engines_README": "Bumping the minimum required Node version? You must bump: 1. package.json -> engines.node, 2. package.json -> devDependencies.@types/node , 3. tsconfig.json -> {target, lib} , 4. .github/workflows/ci.yml -> node-version",
"engines_READMEforEnginesNode": "Here in engines.node, we require a version as old as possible, for Nativefier to be easily installable using the stock Node.js shipped by conservative Linux distros. It's a balancing act between this, and our own dependencies requiring more a recent Node; as much as possible, try to keep supporting Debian stable; https://packages.debian.org/search?suite=stable&keywords=nodejs",
"engines": {
"node": ">= 12.9.0",
"npm": ">= 6.9.0"
"node": ">= 16.16.0",
"npm": ">= 8.11.0"
},
"keywords": [
"desktop",
@ -43,18 +43,23 @@
"lint": "eslint shared app src --ext .ts",
"list-outdated-deps": "npm out; cd app && npm out; true",
"prepare": "cd app && npm ci && cd .. && npm run build",
"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'",
"relock:cli": "rm -rf ./node_modules/ ./npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
"relock:app": "rm -rf ./app/node_modules/ ./app/npm-shrinkwrap.json && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
"relock": "npm run relock:cli; npm run relock:app",
"test:integration": "jest --testRegex=integration-test",
"test:manual": "npm run build && bash .github/manual-test",
"test:playwright": "jest --detectOpenHandles --testRegex=playwright-test",
"test:noplaywright": "jest --testPathIgnorePatterns=playwright",
"test:unit": "jest",
"test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false",
"test:watch:unit": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false --testPathIgnorePatterns=integration --testPathIgnorePatterns=playwright",
"test:withlog": "LOGLEVEL=trace npm run test",
"test": "jest --testRegex '[-.]test\\.js$'",
"test": "jest",
"watch": "npx concurrently \"npm:*:watch\""
},
"dependencies": {
"axios": "^0.26.0",
"electron-packager": "^15.2.0",
"axios": "^1.1.3",
"electron-packager": "^15.5.1",
"fs-extra": "^10.0.0",
"gitcloud": "^0.2.3",
"hasbin": "^1.2.3",
@ -70,17 +75,20 @@
"@types/debug": "^4.1.6",
"@types/fs-extra": "^9.0.13",
"@types/hasbin": "^1.2.0",
"@types/jest": "^27.0.1",
"@types/jest": "^28.1.6",
"@types/ncp": "^2.0.5",
"@types/node": "14.14.20",
"@types/node": "^16.0.0",
"@types/page-icon": "^0.3.4",
"@types/tmp": "^0.2.1",
"@types/yargs": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"electron": "^21.4.4",
"eslint": "^8.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.0.6",
"jest": "^28.1.3",
"playwright": "^1.24.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"ts-loader": "^9.2.3",
@ -94,6 +102,14 @@
},
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"./app/dist/**/*.js",
"./lib/**/*.js",
"./shared/lib/**/*.js"
],
"coveragePathIgnorePatterns": [
"[.-]test.js$"
],
"moduleNameMapper": {
"^electron$": "<rootDir>/app/dist/mocks/electron.js"
},
@ -102,19 +118,22 @@
],
"testEnvironment": "node",
"testPathIgnorePatterns": [
"<rootDir>/src.*",
"<rootDir>/node_modules.*",
"<rootDir>/app/node_modules.*",
"<rootDir>/app/src.*",
"<rootDir>/app/lib.*",
"<rootDir>/app/node_modules.*"
"<rootDir>/src.*",
".+\\.d\\.ts",
".+\\.js\\.map"
],
"testRegex": "test\\.js",
"testTimeout": 15000,
"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

@ -9,6 +9,7 @@ export type TitleBarValue =
export type TrayValue = 'true' | 'false' | 'start-in-tray';
export interface ElectronPackagerOptions extends electronPackager.Options {
arch: string;
portable: boolean;
platform?: string;
targetUrl: string;
@ -130,7 +131,7 @@ export type RawOptions = {
alwaysOnTop?: boolean;
appCopyright?: string;
appVersion?: string;
arch?: string | string[];
arch?: string;
asar?: boolean | CreateOptions;
backgroundColor?: string;
basicAuthPassword?: string;

View File

@ -125,10 +125,21 @@ function trimUnprocessableOptions(options: AppOptions): void {
}
}
function getOSRunHelp(platform?: string): string {
if (platform === 'win32') {
return `the contained .exe file.`;
} else if (platform === 'linux') {
return `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (platform === 'darwin') {
return `the app bundle.`;
}
return '';
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildNativefierApp(
rawOptions: RawOptions,
): Promise<string | undefined> {
): Promise<string> {
// early-suppress potential logging before full options handling
if (rawOptions.quiet) {
log.setLevel('silent');
@ -190,6 +201,8 @@ export async function buildNativefierApp(
convertIconIfNecessary(options);
await copyIconsIfNecessary(options, tmpPath);
options.packager.quiet = !rawOptions.verbose;
log.info(
"\nPackaging... This will take a few seconds, maybe minutes if the requested Electron isn't cached yet...",
);
@ -245,14 +258,7 @@ export async function buildNativefierApp(
appPath = finalOutDirectory;
}
let osRunHelp = '';
if (options.packager.platform === 'win32') {
osRunHelp = `the contained .exe file.`;
} else if (options.packager.platform === 'linux') {
osRunHelp = `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`;
} else if (options.packager.platform === 'darwin') {
osRunHelp = `the app bundle.`;
}
const osRunHelp = getOSRunHelp(options.packager.platform);
log.info(
`App built to ${appPath}, move to wherever it makes sense for you and run ${osRunHelp}`,
);

View File

@ -17,6 +17,19 @@ import { buildNativefierApp } from './main';
import { RawOptions } from '../shared/src/options/model';
import { parseJson } from './utils/parseUtils';
// @types/yargs@17.x started pretending yargs.argv can be a promise:
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8e17f9ca957a06040badb53ae7688fbb74229ccf/types/yargs/index.d.ts#L73
// Dunno in which case it happens, but it doesn't for us! So, having to await
// (and end up having to flag sync code as async) would be useless and annoying.
// So, copy-pastaing and axing the Promise half of yargs's type definition,
// to have a *non*-promise type. Maybe that's wrong. If it is, this type should
// be dropped, and extra async-ness should be added where needed.
type YargsArgvSync<T> = {
[key in keyof yargs.Arguments<T> as
| key
| yargs.CamelCaseKey<key>]: yargs.Arguments<T>[key];
};
export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
const sanitizedArgs = sanitizeArgs(argv);
const args = yargs(sanitizedArgs)
@ -533,7 +546,7 @@ export function initArgs(argv: string[]): yargs.Argv<RawOptions> {
// We must access argv in order to get yargs to actually process args
// Do this now to go ahead and get any errors out of the way
args.argv;
args.argv as YargsArgvSync<RawOptions>;
return args as yargs.Argv<RawOptions>;
}
@ -543,7 +556,7 @@ function decorateYargOptionGroup(value: string): string {
}
export function parseArgs(args: yargs.Argv<RawOptions>): RawOptions {
const parsed = { ...args.argv };
const parsed = { ...(args.argv as YargsArgvSync<RawOptions>) };
// In yargs, the _ property of the parsed args is an array of the positional args
// https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_
// So try to extract the targetUrl and outputDirectory from these

View File

@ -2,20 +2,23 @@ import * as path from 'path';
export const DEFAULT_APP_NAME = 'APP';
// Update both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together,
// and update app / package.json / devDeps / electron to value of DEFAULT_ELECTRON_VERSION
export const DEFAULT_ELECTRON_VERSION = '18.0.3';
// Upgrade both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, and
// - upgrade app / package.json / "devDependencies" / "electron"
// - upgrade package.json / "devDependencies" / "electron"
// Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron
export const DEFAULT_ELECTRON_VERSION = '21.4.4';
// https://atom.io/download/atom-shell/index.json
export const DEFAULT_CHROME_VERSION = '100.0.4896.75';
// https://www.electronjs.org/releases/stable
export const DEFAULT_CHROME_VERSION = '106.0.5249.199';
// Update each of these periodically
// https://product-details.mozilla.org/1.0/firefox_versions.json
export const DEFAULT_FIREFOX_VERSION = '99.0';
export const DEFAULT_FIREFOX_VERSION = '116.0';
// https://en.wikipedia.org/wiki/Safari_version_history
export const DEFAULT_SAFARI_VERSION = {
majorVersion: 15,
version: '15.0',
majorVersion: 65,
version: '16.5.2',
webkitVersion: '605.1.15',
};

View File

@ -27,6 +27,17 @@ export function hasWine(): boolean {
return hasbin.sync('wine');
}
// I tried to place this (and the other is* functions) in
// a new shared helpers, but alas eslint gets real confused
// about the type signatures and thinks they're all any.
// TODO: Figure out a way to refactor duplicate code from
// src/helpers/helpers.ts and app/src/helpers/helpers.ts
// into the shared module
export function isLinux(): boolean {
return os.platform() === 'linux';
}
export function isOSX(): boolean {
return os.platform() === 'darwin';
}

View File

@ -18,7 +18,7 @@ type ElectronRelease = {
files: string[];
};
const ELECTRON_VERSIONS_URL = 'https://atom.io/download/atom-shell/index.json';
const ELECTRON_VERSIONS_URL = 'https://releases.electronjs.org/releases.json';
export async function getChromeVersionForElectronVersion(
electronVersion: string,

View File

@ -3,11 +3,8 @@ import * as os from 'os';
import * as log from 'loglevel';
// Ideally we'd get this list directly from electron-packager, but it's not
// accessible in the package without importing its private js files, which felt
// dirty. So if those change, we'll update these as well.
// https://electron.github.io/electron-packager/master/interfaces/electronpackager.options.html#platform
// https://electron.github.io/electron-packager/master/interfaces/electronpackager.options.html#arch
export const supportedArchs = ['ia32', 'x64', 'armv7l', 'arm64'];
// possible to convert a literal type to an array of strings in current TypeScript
export const supportedArchs = ['x64', 'armv7l', 'arm64', 'universal'];
export const supportedPlatforms = [
'darwin',
'linux',

View File

@ -1,4 +1,4 @@
import axios, { AxiosResponse } from 'axios';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { inferTitle } from './inferTitle';
@ -14,7 +14,8 @@ test('it returns the correct title', async () => {
status: 200,
statusText: 'OK',
headers: {},
config: {},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
config: {} as unknown as InternalAxiosRequestConfig<unknown>,
};
axiosGetMock.mockResolvedValue(mockedResponse);
const result = await inferTitle('someurl');

View File

@ -2,7 +2,7 @@ import axios from 'axios';
import * as log from 'loglevel';
const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36';
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15';
export async function inferTitle(url: string): Promise<string> {
const { data } = await axios.get<string>(url, {

View File

@ -21,12 +21,12 @@ async function checkApp(
appRoot: string,
inputOptions: RawOptions,
): Promise<void> {
const arch = inputOptions.arch ? (inputOptions.arch as string) : inferArch();
const arch = inputOptions.arch ? inputOptions.arch : inferArch();
if (inputOptions.out !== undefined) {
expect(
path.join(
inputOptions.out,
`Google-${inputOptions.platform as string}-${arch}`,
`npm-${inputOptions.platform as string}-${arch}`,
),
).toBe(appRoot);
}
@ -34,7 +34,7 @@ async function checkApp(
let relativeResourcesDir = 'resources';
if (inputOptions.platform === 'darwin') {
relativeResourcesDir = path.join('Google.app', 'Contents', 'Resources');
relativeResourcesDir = path.join('npm.app', 'Contents', 'Resources');
}
const appPath = path.join(appRoot, relativeResourcesDir, 'app');
@ -47,7 +47,7 @@ async function checkApp(
expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl);
// Test name inferring
expect(nativefierConfig?.name).toBe('Google');
expect(nativefierConfig?.name).toBe('npm');
// Test icon writing
const iconFile =
@ -118,11 +118,11 @@ describe('Nativefier', () => {
out: tempDirectory,
overwrite: true,
platform,
targetUrl: 'https://google.com/',
targetUrl: 'https://npmjs.com/',
};
const appPath = await buildNativefierApp(options);
expect(appPath).not.toBeUndefined();
await checkApp(appPath as string, options);
await checkApp(appPath, options);
},
);
});
@ -164,11 +164,9 @@ describe('Nativefier upgrade', () => {
// Exhaustive integration testing here would be neat, but takes too long.
// -> For now, only testing a subset of platforms/archs
// { platform: 'win32', arch: 'x64' },
// { platform: 'win32', arch: 'ia32' },
// { platform: 'darwin', arch: 'arm64' },
// { platform: 'linux', arch: 'x64' },
// { platform: 'linux', arch: 'armv7l' },
// { platform: 'linux', arch: 'ia32' },
])(
'can upgrade a Nativefier app for platform/arch: %s',
async (baseAppOptions) => {
@ -179,15 +177,15 @@ describe('Nativefier upgrade', () => {
globalShortcuts: shortcuts,
out: tempDirectory,
overwrite: true,
targetUrl: 'https://google.com/',
targetUrl: 'https://npmjs.com/',
...baseAppOptions,
};
const appPath = await buildNativefierApp(options);
expect(appPath).not.toBeUndefined();
await checkApp(appPath as string, options);
await checkApp(appPath, options);
const upgradeOptions: RawOptions = {
upgrade: appPath as string,
upgrade: appPath,
overwrite: true,
};
@ -195,7 +193,7 @@ describe('Nativefier upgrade', () => {
options.electronVersion = DEFAULT_ELECTRON_VERSION;
options.userAgent = baseAppOptions.userAgent;
expect(upgradeAppPath).not.toBeUndefined();
await checkApp(upgradeAppPath as string, options);
await checkApp(upgradeAppPath, options);
},
);
});

View File

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

View File

@ -60,6 +60,7 @@ describe('fields', () => {
zoom: 1,
},
packager: {
arch: process.arch,
dir: '',
platform: process.platform,
portable: false,

View File

@ -78,7 +78,7 @@ test('short userAgent parameter is passed with an electronVersion', async () =>
);
const params = {
packager: { electronVersion: '12.0.0', platform: 'darwin' },
packager: { electronVersion: '16.0.0', platform: 'darwin' },
nativefier: { userAgent: 'edge' },
};
@ -86,5 +86,5 @@ test('short userAgent parameter is passed with an electronVersion', async () =>
expect(parsedUserAgent).not.toBe(params.nativefier.userAgent);
expect(parsedUserAgent).toContain('102.0.0');
expect(getChromeVersionForElectronVersion).toHaveBeenCalledWith('12.0.0');
expect(getChromeVersionForElectronVersion).toHaveBeenCalledWith('16.0.0');
});

View File

@ -81,7 +81,10 @@ async function edgeUserAgent(
async function firefoxUserAgent(platform: string): Promise<string> {
const firefoxVersion = await getLatestFirefoxVersion();
return `Mozilla/5.0 (${platform}; rv:${firefoxVersion}) Gecko/20100101 Firefox/${firefoxVersion}`;
return `Mozilla/5.0 (${platform}; rv:${firefoxVersion}) Gecko/20100101 Firefox/${firefoxVersion}`.replace(
'10_15_7',
'10.15',
);
}
async function safariUserAgent(platform: string): Promise<string> {

View File

@ -60,6 +60,7 @@ const mockedAsyncConfig: AppOptions = {
zoom: 1,
},
packager: {
arch: process.arch,
dir: '',
platform: process.platform,
portable: false,

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

@ -0,0 +1,431 @@
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, isLinux } 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);
});
}
/**
* Debugging this? Run your playwright tests in debug mode:
* DEBUG='pw:browser*' npm run test:playwright
*/
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({
// Workaround for the following errors in some linux distros:
// pw:browser [pid=24716][err] [24718:0100/000000.660708:ERROR:zygote_linux.cc(650)] write: Broken pipe (32) +16ms
// pw:browser [pid=24719][err] [24719:0725/114519.722060:FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/parallels/Dev/nativefier/node_modules/electron/dist/chrome-sandbox is owned by root and has mode 4755. +61ms
args: isLinux()
? ['--no-sandbox', '--disable-setuid-sandbox', appMainJSPath]
: [appMainJSPath],
env: {
LOG_FILE_DIR: logFileDir,
PLAYWRIGHT_TEST: '1',
PLAYWRIGHT_CONFIG: JSON.stringify({
...playwrightConfig,
// disableGpu and process.env.DISPLAY forwarding solve the following errors on Linux:
// pw:browser [pid=286188][err] [286188:0724/102939.938248:ERROR:ozone_platform_x11.cc(248)] Missing X server or $DISPLAY +77ms
// pw:browser [pid=286188][err] [286188:0724/102939.938299:ERROR:env.cc(225)] The platform failed to initialize. Exiting. +2ms
disableGpu: isLinux() ? true : undefined,
processEnvs:
isLinux() && process.env.DISPLAY
? JSON.stringify({ DISPLAY: process.env.DISPLAY })
: undefined,
} as NativefierOptions),
USE_LOG_FILE: '1',
VERBOSE: '1',
},
timeout: 60000,
});
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-pricing-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-pricing-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-pricing-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: { ...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: 'https://authenticationtest.com/HTTPAuth/',
basicAuthUsername: 'user',
basicAuthPassword: 'pass',
})) as Page;
await mainWindow.waitForLoadState('networkidle');
const documentText = await mainWindow.evaluate<string>(
'document.documentElement.innerText',
);
expect(documentText).toContain('Success');
expect(documentText).not.toContain('Failure');
});
test('basic auth - bad login', async () => {
const mainWindow = (await spawnApp({
targetUrl: 'https://authenticationtest.com/HTTPAuth/',
basicAuthUsername: 'userbad',
basicAuthPassword: 'passbad',
})) as Page;
await mainWindow.waitForLoadState('networkidle');
const documentText = await mainWindow.evaluate<string>(
'document.documentElement.innerText',
);
expect(documentText).not.toContain('Success');
expect(documentText).toContain('Failure');
});
test('basic auth without pre-providing', async () => {
const mainWindow = (await spawnApp({
targetUrl: 'https://authenticationtest.com/HTTPAuth/',
})) 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');
await loginWindow.waitForLoadState('load');
const usernameField = await loginWindow.$('#username-input');
expect(usernameField).not.toBeNull();
await usernameField?.fill('user');
const passwordField = await loginWindow.$('#password-input');
expect(passwordField).not.toBeNull();
await passwordField?.fill('pass');
const submitButton = await loginWindow.$('#submit-form-button');
expect(submitButton).not.toBeNull();
// "Why is this here?" you may be asking yourself.
// Because for some reason, on some linux boxes,
// the click function will not work until this is done.
// Why? I do not have access to the dark incantation
// that would allow me to know such information.
log.log({ submitButton });
await submitButton?.click();
await mainWindow.waitForEvent('load');
const documentText = await mainWindow.evaluate<string>(
'document.documentElement.innerText',
);
expect(documentText).toContain('Success');
expect(documentText).not.toContain('Failure');
});
});
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());
}
}