mirror of https://github.com/jiahaog/Nativefier
Merge branch 'master' into update/new-window-to-setwindowopenhandler
This commit is contained in:
commit
ba8c4e85cd
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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")
|
|
@ -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")
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
13
API.md
|
@ -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);
|
||||
});
|
||||
```
|
||||
|
|
176
CATALOG.md
176
CATALOG.md
|
@ -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 don’t 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 OS’s "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` doesn’t 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;
|
||||
}
|
||||
```
|
||||
|
|
161
CHANGELOG.md
161
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
110
README.md
|
@ -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 doesn’t 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).
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,6 +20,6 @@
|
|||
"source-map-support": "^0.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^18.0.3"
|
||||
"electron": "^21.4.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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®exp=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,
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
104
app/src/main.ts
104
app/src/main.ts
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`,
|
||||
);
|
||||
|
|
17
src/cli.ts
17
src/cli.ts
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,3 +5,5 @@ if (process.env.LOGLEVEL) {
|
|||
} else {
|
||||
log.disableAll();
|
||||
}
|
||||
|
||||
process.traceDeprecation = true;
|
||||
|
|
|
@ -60,6 +60,7 @@ describe('fields', () => {
|
|||
zoom: 1,
|
||||
},
|
||||
packager: {
|
||||
arch: process.arch,
|
||||
dir: '',
|
||||
platform: process.platform,
|
||||
portable: false,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -60,6 +60,7 @@ const mockedAsyncConfig: AppOptions = {
|
|||
zoom: 1,
|
||||
},
|
||||
packager: {
|
||||
arch: process.arch,
|
||||
dir: '',
|
||||
platform: process.platform,
|
||||
portable: false,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue